from __future__ import annotations

import argparse
import logging
import platform
import subprocess
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import ClassVar

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
is_windows = platform.system() == "Windows"


def check_git_status() -> bool:
    """Check if there are uncommitted changes.

    Returns:
        bool: Whether there are uncommitted changes
    """
    try:
        result = subprocess.run(
            ["git", "status", "--porcelain"],
            capture_output=True,
            text=True,
            check=False,
        )
    except subprocess.CalledProcessError as e:
        logger.error(f"Git status failed: {e}")
        return False

    if result.stdout.strip():
        logger.error(
            f"Uncommitted changes exist, please commit changes first: {result.stdout}"
        )
        return False
    return True


@dataclass
class GitInitCommand:
    """Git init command data class."""

    SUBCOMMANDS: ClassVar[list[list[str]]] = [
        ["git", "init"],
        ["git", "add", "."],
        ["git", "commit", "-m", "initial commit"],
    ]

    def run(self) -> None:
        """Run git init command."""
        for subcommand in self.SUBCOMMANDS:
            try:
                subprocess.run(subcommand, check=True)
                logger.info(f"Executed: {' '.join(subcommand)}")
            except subprocess.CalledProcessError as e:
                logger.error(f"Git init failed: {e}")
                return
        logger.info("Git initialization completed successfully")


@dataclass(frozen=True)
class GitAddCommand:
    """Git add command data class."""

    def run(self) -> None:
        """Run git add command."""
        change_files_status = {
            "Added": self.added_filenames,
            "Modified": self.modified_filenames,
        }

        for change_type, files in change_files_status.items():
            if files:
                file_str = ", ".join(files)
                logger.info(f"{change_type} files: {file_str}")
                try:
                    subprocess.run(
                        ["git", "commit", "-m", f"{change_type} files: {file_str}"],
                        check=True,
                    )
                except subprocess.CalledProcessError as e:
                    logger.error(f"Git commit failed: {e}")
                    return
            else:
                logger.warning(f"No {change_type} files")

    @cached_property
    def modified_filenames(self) -> set[str]:
        """Get list of modified files from git status."""
        return {f.filepath.stem for f in self.added_filestatus if f.status == "M"}

    @cached_property
    def added_filenames(self) -> set[str]:
        """Get list of added files from git status."""
        return {f.filepath.stem for f in self.added_filestatus if f.status == "A"}

    @cached_property
    def added_filestatus(self) -> set[GitAddFileStatus]:
        """Get list of added files from git status."""
        before = self.changed_files_info

        try:
            subprocess.run(["git", "add", "."], check=True)
        except subprocess.CalledProcessError as e:
            logger.error(f"Git add failed: {e}")
            return set()

        after = self.changed_files_info
        return {f for f in after - before if f.status == "A"}

    @property
    def changed_files_info(self) -> set[GitAddFileStatus]:
        """Get list of changed files from git status."""
        result = subprocess.run(
            ["git", "status", "--porcelain"],
            capture_output=True,
            text=True,
            check=False,
        )
        files: set[GitAddFileStatus] = set()
        if result.returncode == 0:
            for line in result.stdout.splitlines():
                if line.strip():
                    status = line[:2].strip()
                    filename = line[3:].strip()
                    files.add(GitAddFileStatus(status, Path(filename)))
        return files


@dataclass
class GitAddFileStatus:
    """Git file status data class.

    Properties:
        status: File status, A: Added, M: Modified
        filepath: File path
    """

    status: str
    filepath: Path

    def __hash__(self) -> int:
        """Calculate hash value for unique identification in sets.

        Returns:
            int: Hash value
        """
        return hash((self.status, str(self.filepath)))


@dataclass
class GitCleanCommand:
    """Git clean command."""

    EXCLUDE_DIRS: ClassVar[list[str]] = [
        ".venv",
        "node_modules",
        ".git",
        ".idea",
        ".vscode",
    ]

    def run(self, force: bool = False) -> None:
        """Run git clean command.

        Args:
            force: Force clean without confirmation
        """
        try:
            cmd = ["git", "clean", "-xd"]
            for exclude_dir in self.EXCLUDE_DIRS:
                cmd.extend(["-e", exclude_dir])

            if force:
                cmd.append("-f")

            subprocess.run(cmd, check=True)
            subprocess.run(["git", "checkout", "."], check=True)
            logger.info("Clean completed successfully")
        except subprocess.CalledProcessError as e:
            logger.error(f"Git clean failed: {e}")
            return


@dataclass
class GitPushCommand:
    """Git push command."""

    def run(self, all_branches: bool = False) -> None:
        """Run git push command.

        Args:
            all_branches: Push all branches
        """
        try:
            if all_branches:
                subprocess.run(["git", "push", "--all"], check=True)
                logger.info("Push all branches completed successfully")
            else:
                subprocess.run(["git", "push"], check=True)
                logger.info("Push completed successfully")
        except subprocess.CalledProcessError as e:
            logger.error(f"Git push failed: {e}")
            return


@dataclass
class GitPullCommand:
    """Git pull command."""

    def run(self, rebase: bool = False) -> None:
        """Run git pull command.

        Args:
            rebase: Use rebase when pulling
        """
        try:
            if rebase:
                subprocess.run(["git", "pull", "--rebase"], check=True)
                logger.info("Pull with rebase completed successfully")
            else:
                subprocess.run(["git", "pull"], check=True)
                logger.info("Pull completed successfully")
        except subprocess.CalledProcessError as e:
            logger.error(f"Git pull failed: {e}")
            return


@dataclass
class GitCheckoutCommand:
    """Git checkout command."""

    def run(self, branch: str, create: bool = False) -> None:
        """Run git checkout command.

        Args:
            branch: Branch name to checkout
            create: Create new branch if True
        """
        try:
            if create:
                subprocess.run(["git", "checkout", "-b", branch], check=True)
                logger.info(f"Created and checked out branch: {branch}")
            else:
                subprocess.run(["git", "checkout", branch], check=True)
                logger.info(f"Checked out branch: {branch}")
        except subprocess.CalledProcessError as e:
            logger.error(f"Git checkout failed: {e}")
            return


@dataclass
class GitBranchCommand:
    """Git branch command."""

    def run(self, list_all: bool = False, delete: str | None = None) -> None:
        """Run git branch command.

        Args:
            list_all: List all branches (remote and local)
            delete: Branch name to delete
        """
        try:
            if delete:
                subprocess.run(["git", "branch", "-d", delete], check=True)
                logger.info(f"Deleted branch: {delete}")
            elif list_all:
                result = subprocess.run(
                    ["git", "branch", "-a"],
                    capture_output=True,
                    text=True,
                    check=True,
                )
                logger.info("All branches:")
                logger.info(result.stdout)
            else:
                result = subprocess.run(
                    ["git", "branch"],
                    capture_output=True,
                    text=True,
                    check=True,
                )
                logger.info("Local branches:")
                logger.info(result.stdout)
        except subprocess.CalledProcessError as e:
            logger.error(f"Git branch command failed: {e}")
            return


@dataclass
class GitRestartTGitCacheCommand:
    """Git restart command."""

    def run(self) -> None:
        """Run git restart command."""
        logger.info("Restarting tgitcache...")
        cmds = (
            ["taskkill", "/f", "/t", "/im", "tgitcache.exe"]
            if is_windows
            else ["killall", "tgitcache"]
        )

        try:
            subprocess.run(cmds, check=True)
            logger.info("Restart completed successfully")
        except subprocess.CalledProcessError as e:
            logger.error(f"Git restart failed: {e}")
            return


_COMMAND_MAP = {
    "i": GitInitCommand,
    "a": GitAddCommand,
    "c": GitCleanCommand,
    "p": GitPushCommand,
    "pl": GitPullCommand,
    "co": GitCheckoutCommand,
    "b": GitBranchCommand,
}


def parse_args() -> argparse.Namespace:
    """Parse command line arguments.

    Returns:
        argparse.Namespace: Parsed arguments
    """
    parser = argparse.ArgumentParser(description="Gittool - Git command wrapper")
    subparsers = parser.add_subparsers(
        dest="command", help="Available commands", required=True
    )

    # Init subcommand
    subparsers.add_parser("i", help="Initialize git repository")

    # Add subcommand
    subparsers.add_parser("a", help="Stage and commit changes")

    # Clean subcommand
    parser_c = subparsers.add_parser("c", help="Clean untracked files")
    parser_c.add_argument(
        "-f",
        "--force",
        required=False,
        action="store_true",
        help="Force clean",
    )

    # Push subcommand
    parser_p = subparsers.add_parser("p", help="Push to remote")
    parser_p.add_argument("--all", "-a", action="store_true", help="Push all branches")

    # Pull subcommand
    parser_pl = subparsers.add_parser("pl", help="Pull from remote")
    parser_pl.add_argument(
        "--rebase", "-r", action="store_true", help="Pull with rebase"
    )

    # Checkout subcommand
    parser_co = subparsers.add_parser("co", help="Checkout branch")
    parser_co.add_argument("branch", help="Branch name")
    parser_co.add_argument(
        "-b", "--create", action="store_true", help="Create new branch"
    )

    # Branch subcommand
    parser_b = subparsers.add_parser("b", help="List or manage branches")
    parser_b.add_argument("-a", "--all", action="store_true", help="List all branches")
    parser_b.add_argument("-d", "--delete", metavar="BRANCH", help="Delete branch")

    return parser.parse_args()


def main() -> None:
    """Main entry point for gittool CLI."""
    args = parse_args()
    command = _COMMAND_MAP.get(args.command)

    if not command:
        raise ValueError(f"Invalid command: {args.command}")

    cmd_instance = command()
    if args.command == "i" or args.command == "a":
        cmd_instance.run()
    elif args.command == "c":
        cmd_instance.run(force=args.force)
    elif args.command == "p":
        cmd_instance.run(all_branches=args.all)
    elif args.command == "pl":
        cmd_instance.run(rebase=args.rebase)
    elif args.command == "co":
        cmd_instance.run(branch=args.branch, create=args.create)
    elif args.command == "b":
        cmd_instance.run(list_all=args.all, delete=args.delete)


if __name__ == "__main__":
    main()
