#!/usr/bin/python3
# -*- coding: utf-8 -*-

from __future__ import annotations

import re
import subprocess
from pathlib import Path

from sbo_create.handlers.files import FileHandler
from sbo_create.models import PROG, AppContext, colorize, confirm


class GitHandler:
    """Handle git operations: sync and submit to the SBo repository."""

    def __init__(self, ctx: AppContext, files: FileHandler) -> None:
        self.ctx = ctx
        self.files = files

    def _require_repo_path(self) -> Path:
        """Return the resolved SBo repo path, or exit with a hard error if unconfigured.

        Returns:
            Absolute resolved Path to the local SBo repository.
        """
        repo_path_str: str = self.ctx.maintainer.sbo_repo_path
        if not repo_path_str:
            print(colorize(
                f"{PROG}: SBo repo path not configured. Run '{PROG} maintainer'.", 'red'))
            raise SystemExit(1)
        repo_path: Path = Path(repo_path_str).expanduser().resolve()
        if not repo_path.is_dir():
            print(colorize(f"{PROG}: SBo repo path not found: {repo_path}", 'red'))
            raise SystemExit(1)
        return repo_path

    def _resolve_repo_path(self) -> Path:
        """Return the resolved SBo repo path, or open the web submission page if unconfigured.

        If no repo path is configured, attempts to open https://slackbuilds.org/submit/
        with xdg-open and exits. If the path is configured but the directory does not
        exist, exits with a hard error.

        Returns:
            Absolute resolved Path to the local SBo repository.
        """
        repo_path_str: str = self.ctx.maintainer.sbo_repo_path
        if not repo_path_str:
            print("No SBo repo configured. Opening submission page...")
            try:
                subprocess.run(['xdg-open', 'https://slackbuilds.org/submit/'], check=False)
            except FileNotFoundError:
                print(colorize('https://slackbuilds.org/submit/', 'cyan'))
            raise SystemExit(0)
        repo_path: Path = Path(repo_path_str).expanduser().resolve()
        if not repo_path.is_dir():
            print(colorize(f"{PROG}: SBo repo path not found: {repo_path}", 'red'))
            raise SystemExit(1)
        return repo_path

    def _branch_behind_count(self, repo_path: Path) -> int:
        """Return how many commits the current branch is behind the main branch.

        Args:
            repo_path: Path to the git repository root.

        Returns:
            Integer count of commits the current branch is behind main (0 if unknown).
        """
        main: str = self.ctx.maintainer.git_main_branch or 'master'
        result = subprocess.run(
            ['git', 'rev-list', '--count', f'HEAD..{main}'],
            cwd=repo_path, capture_output=True, text=True, check=False,
        )
        try:
            return int(result.stdout.strip())
        except ValueError:
            return 0

    def _resolve_push_cmd(self, repo_path: Path) -> list[str]:
        """Return the git push command, adding --set-upstream if no upstream is configured.

        Args:
            repo_path: Path to the git repository root.

        Returns:
            List of command tokens for the git push operation.
        """
        git_branch: str = self.ctx.maintainer.git_branch
        upstream = subprocess.run(
            ['git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'],
            cwd=repo_path, capture_output=True, check=False,
        )
        if upstream.returncode != 0 and git_branch:
            return ['git', 'push', '--set-upstream', 'origin', git_branch]
        return self.ctx.maintainer.git_push_cmd.split()

    @staticmethod
    def _changed_files_in_pkg(repo_path: Path, category: str, name: str) -> list[tuple[str, str]]:
        """Return (status, filename) pairs for changed files in a package directory.

        Runs 'git status --short' scoped to the given package directory and returns
        one entry per changed file. Status is a single character: M (modified),
        A (added), D (deleted), or ? (untracked).

        Args:
            repo_path: Path to the git repository root.
            category: SBo category (e.g. 'desktop').
            name: Package name (e.g. 'sun').

        Returns:
            List of (status, filename) tuples, ordered as git reports them.
        """
        result = subprocess.run(
            ['git', 'status', '--short', '--', f'{category}/{name}/'],
            cwd=repo_path, capture_output=True, text=True, check=False,
        )
        entries: list[tuple[str, str]] = []
        for line in result.stdout.splitlines():
            if len(line) < 4:
                continue
            raw_status: str = line[:2].strip() or '?'
            status: str = raw_status[0] if raw_status != '??' else '?'
            filename: str = Path(line[3:].strip()).name
            entries.append((status, filename))
        return entries

    @staticmethod
    def _detect_packages_from_git(repo_path: Path) -> list[tuple[str, str]]:
        """Return unique (name, category) pairs for all modified files in the git repo.

        Runs 'git diff --name-only' to get the list of unstaged changes and extracts
        the first two path components (category/name). Each unique category/name
        combination is returned only once, preserving first-seen order.

        Args:
            repo_path: Path to the git repository root.

        Returns:
            List of (name, category) tuples for each modified package directory.
        """
        result = subprocess.run(
            ['git', 'diff', '--name-only'],
            cwd=repo_path, capture_output=True, text=True, check=False,
        )
        seen: dict[tuple[str, str], None] = {}
        for line in result.stdout.splitlines():
            parts = line.split('/')
            if len(parts) >= 2:
                key = (parts[1], parts[0])  # (name, category)
                seen[key] = None
        return list(seen.keys())

    @staticmethod
    def _strip_a_flag(cmd: str) -> list[str]:
        """Remove the -a flag from a git commit command string.

        Args:
            cmd: The raw git commit command string (e.g. 'git commit -am').

        Returns:
            List of command tokens with the -a flag removed.
        """
        parts = cmd.split()
        result: list[str] = []
        for part in parts:
            if part.startswith('-') and not part.startswith('--') and 'a' in part:
                stripped = part.replace('a', '', 1)
                if stripped != '-':
                    result.append(stripped)
            else:
                result.append(part)
        return result

    def sync_repo(self) -> None:
        """Sync the local SBo git repository to a clean working branch.

        Executes the following git steps in order: checkout the main branch,
        pull the latest changes, delete the configured working branch if it
        exists, and recreate it fresh from the current HEAD. Exits with an error
        if any git command fails or git_branch is not configured.
        """
        repo_path: Path = self._require_repo_path()
        main: str = self.ctx.maintainer.git_main_branch or 'master'
        branch: str = self.ctx.maintainer.git_branch
        if not branch:
            print(colorize(
                f"{PROG}: git_branch not configured. Run '{PROG} maintainer'.", 'red'))
            raise SystemExit(1)

        branch_exists: bool = subprocess.run(
            ['git', 'rev-parse', '--verify', branch],
            cwd=repo_path, capture_output=True, check=False,
        ).returncode == 0

        steps: list[list[str]] = [
            ['git', 'checkout', main],
            ['git', 'pull'],
            *([['git', 'branch', '-D', branch]] if branch_exists else []),
            ['git', 'checkout', '-B', branch],
        ]

        print(f"Syncing SBo repository ({repo_path}):\n")
        for cmd in steps:
            print(f"  {colorize(' '.join(cmd), 'cyan')}")
            result = subprocess.run(cmd, cwd=repo_path, check=False)
            if result.returncode != 0:
                print(colorize(f"{PROG}: '{' '.join(cmd)}' failed.", 'red'))
                raise SystemExit(1)

        print(colorize(f"\nBranch '{branch}' is ready.", 'green'))
        raise SystemExit(0)

    def log_branch(self) -> None:
        """Show packages committed on the current branch since the last public SBo update.

        Runs 'git log <main>..HEAD --format=%s' and parses commit subjects of the
        form 'category/prgnam: Updated for version X.Y.Z.' into a formatted table.
        Unrecognised commit subjects are printed as-is.
        """
        repo_path: Path = self._require_repo_path()
        main: str = self.ctx.maintainer.git_main_branch or 'master'
        branch: str = self.ctx.maintainer.git_branch or 'current branch'

        result = subprocess.run(
            ['git', 'log', f'{main}..HEAD', '--format=%s'],
            cwd=repo_path, capture_output=True, text=True, check=False,
        )
        subjects: list[str] = [s for s in result.stdout.splitlines() if s.strip()]
        if not subjects:
            print(f"No commits ahead of '{colorize(main, 'cyan')}'.")
            raise SystemExit(0)

        entries: list[tuple[str, str]] = []
        for subject in subjects:
            m = re.match(r'^(\S+/\S+):\s+.*?[Vv]ersion\s+(\S+?)\.?\s*$', subject)
            if m:
                entries.append((m.group(1), m.group(2)))
            else:
                pkg_m = re.match(r'^(\S+/\S+):\s*(.*)', subject)
                if pkg_m:
                    entries.append((pkg_m.group(1), pkg_m.group(2).rstrip('.')))
                else:
                    entries.append((subject, ''))

        print(f"Commits on {colorize(branch, 'cyan')} (since {main}):\n")
        for pkg, ver in entries:
            line: str = f"  {colorize(pkg, 'yellow')}"
            if ver:
                line += f"  {ver}"
            print(line)
        print(colorize(f"\n  {len(entries)} package(s) updated.", 'green'))
        raise SystemExit(0)

    def submit_package(self) -> None:  # pylint: disable=too-many-locals,too-many-statements,too-many-branches
        """Submit the package to SBo via git commit and optional push.

        Detects changed packages from 'git diff --name-only', prompts for commit
        messages, stages each package directory with 'git add', commits with the
        configured GIT_COMMIT_CMD (with the -a flag stripped to avoid double-staging),
        and optionally pushes. Warns if the working branch is behind main and
        offers to abort. Falls back to opening the SBo submission web page if no
        repo path is configured.
        """
        repo_path: Path = self._resolve_repo_path()

        behind: int = self._branch_behind_count(repo_path)
        if behind > 0:
            main: str = self.ctx.maintainer.git_main_branch or 'master'
            branch: str = self.ctx.maintainer.git_branch or 'current branch'
            print(colorize(
                f"Warning: '{branch}' is {behind} commit(s) behind '{main}'.", 'yellow'))
            print(f"  Run {colorize('sbc sync', 'cyan')} to update before submitting.\n")
            if not confirm("Continue anyway?", after_newline=False):
                raise SystemExit(0)

        detected: list[tuple[str, str]] = self._detect_packages_from_git(repo_path)
        if not detected:
            print(colorize(f"{PROG}: No changes found in the repository.", 'red'))
            raise SystemExit(1)

        commit_cmd: str = self.ctx.maintainer.git_commit_cmd
        commit_base: list[str] = self._strip_a_flag(commit_cmd)
        if commit_base != commit_cmd.split():
            print(f"{colorize('Note:', 'yellow')} '-a' flag in GIT_COMMIT_CMD is ignored; "
                  "files are staged explicitly with 'git add'.")

        entries: list[tuple[str, str, str, str]] = []
        for name, category in detected:
            self.ctx.prg_name = name
            self.ctx.current_folder = repo_path / category / name
            self.files.read_info_file()
            version: str = self.ctx.info.version
            default_msg: str = f'{category}/{name}: Updated for version {version}.'
            if len(detected) == 1:
                raw: str = input(
                    f"\nCommit message [{colorize(default_msg, 'yellow')}]: ").strip()
                msg: str = raw or default_msg
            else:
                msg = default_msg
            entries.append((name, category, version, msg))

        status_color: dict[str, str] = {'M': 'yellow', 'A': 'green', 'D': 'red', '?': 'cyan'}

        print("\nCommands to run:\n")
        for name, category, _, msg in entries:
            changed: list[tuple[str, str]] = self._changed_files_in_pkg(repo_path, category, name)
            if changed:
                print(f"Files in {category}/{name}/:")
                for status, filename in changed:
                    color: str = status_color.get(status, 'yellow')
                    print(f"  {colorize(status, color)}  {filename}")
                print()
            print(f"  {colorize(f'git add {category}/{name}/', 'cyan')}")
            commit_display: str = ' '.join(commit_base) + ' "' + msg + '"'
            print(f"  {colorize(commit_display, 'cyan')}")

        push_parts: list[str] = []
        do_push: bool = confirm("Push after commit?", after_newline=False)
        if do_push:
            push_parts = self._resolve_push_cmd(repo_path)
            print(f"  {colorize(' '.join(push_parts), 'cyan')}")
        print()

        if not confirm("Proceed?", after_newline=True, before_newline=False):
            raise SystemExit(0)

        for name, category, _, msg in entries:
            add_result = subprocess.run(
                ['git', 'add', f'{category}/{name}/'], cwd=repo_path, check=False)
            if add_result.returncode != 0:
                print(colorize(f"{PROG}: git add failed for '{name}'.", 'red'))
                raise SystemExit(1)
            commit_result = subprocess.run(
                commit_base + [msg], cwd=repo_path, check=False)
            if commit_result.returncode != 0:
                print(colorize(f"{PROG}: git commit failed for '{name}'.", 'red'))
                raise SystemExit(1)

        if do_push:
            push_result = subprocess.run(push_parts, cwd=repo_path, check=False)
            if push_result.returncode != 0:
                print(colorize(f"{PROG}: git push failed.", 'red'))
                raise SystemExit(1)

        raise SystemExit(0)
