from __future__ import annotations

import subprocess
from pathlib import Path
from typing import Any

from pydantic import BaseModel, Field

from .config import Settings
from .validation import validate_repository


DENIED_PARTS = {
    ".codex",
    ".git",
    ".pytest_cache",
    ".ruff_cache",
    ".venv",
    "__pycache__",
    "gnexus_book_server.egg-info",
    "node_modules",
}


class CommitRequest(BaseModel):
    summary: str = Field(min_length=1, max_length=200)
    details: str = Field(default="", max_length=4000)
    files: list[str] = Field(min_length=1)


class GitAdapterError(ValueError):
    pass


class GitAdapter:
    def __init__(self, settings: Settings) -> None:
        self.settings = settings
        self.repo_root = settings.repo_root.resolve()

    def status(self) -> dict[str, Any]:
        result = self._git("status", "--short")
        entries = []
        for line in result.stdout.splitlines():
            if not line:
                continue
            entries.append({"status": line[:2], "path": line[3:]})
        return {"entries": entries, "raw": result.stdout}

    def diff(self, files: list[str] | None = None) -> dict[str, str]:
        args = ["diff", "--"]
        if files:
            args.extend(self._validate_paths(files))
        result = self._git(*args)
        return {"diff": result.stdout}

    def commit(self, request: CommitRequest) -> dict[str, Any]:
        validation = validate_repository(self.settings)
        if validation["status"] != "ok":
            raise GitAdapterError("Repository validation failed; commit blocked")

        files = self._validate_paths(request.files)
        self._git("add", "--", *files)

        staged = self._git("diff", "--cached", "--name-only").stdout.splitlines()
        if not staged:
            raise GitAdapterError("No staged changes to commit")

        message = request.summary.strip()
        if request.details.strip():
            message += "\n\n" + request.details.strip()

        result = self._git("commit", "-m", message)
        return {
            "status": "committed",
            "files": staged,
            "stdout": result.stdout,
            "stderr": result.stderr,
        }

    def _validate_paths(self, paths: list[str]) -> list[str]:
        valid: list[str] = []
        for raw_path in paths:
            path = raw_path.strip()
            if not path:
                raise GitAdapterError("Empty file path is not allowed")
            if path.startswith("/") or "\\" in path:
                raise GitAdapterError(f"Invalid repository-relative path: {raw_path}")

            candidate = (self.repo_root / path).resolve()
            if self.repo_root not in candidate.parents and candidate != self.repo_root:
                raise GitAdapterError(f"Path escapes repository root: {raw_path}")

            rel = candidate.relative_to(self.repo_root).as_posix()
            if any(part in DENIED_PARTS for part in Path(rel).parts):
                raise GitAdapterError(f"Path is not allowed for commit: {raw_path}")
            valid.append(rel)
        return valid

    def _git(self, *args: str) -> subprocess.CompletedProcess[str]:
        result = subprocess.run(
            ["git", *args],
            cwd=self.repo_root,
            check=False,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        if result.returncode != 0:
            raise GitAdapterError(result.stderr.strip() or result.stdout.strip() or "Git command failed")
        return result
