"""Shared utility module for pre-commit hooks analyzing staged files."""

import subprocess
from dataclasses import dataclass, field
from pathlib import PurePosixPath


@dataclass
class StagedFileInfo:
    path: str
    added: int
    removed: int
    is_new: bool
    category: str


@dataclass
class CommitAnalysis:
    source_files: list[StagedFileInfo] = field(default_factory=list)
    test_files: list[StagedFileInfo] = field(default_factory=list)
    doc_files: list[StagedFileInfo] = field(default_factory=list)

    @property
    def new_source_files(self):
        return [f for f in self.source_files if f.is_new]

    @property
    def total_source_added(self):
        return sum(f.added for f in self.source_files)

    @property
    def has_tests(self):
        return len(self.test_files) > 0

    @property
    def has_docs(self):
        return len(self.doc_files) > 0


# Directories/patterns excluded from "source" classification
_EXCLUDED_DIRS = {
    "migrations",
    "alembic",
    "config",
    "settings",
    "scripts",
    ".pre-commit-hooks",
    "examples",
    "docs",
}

_EXCLUDED_BASENAMES = {"__init__.py", "conftest.py"}


def _run_git(args):
    """Run a git command and return stdout lines.

    Returns empty list if git command fails (e.g., not in a git repo).
    """
    result = subprocess.run(
        ["git"] + args,
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        return []
    return result.stdout.strip().splitlines() if result.stdout.strip() else []


def _parse_numstat(lines):
    """Parse git diff --numstat output into {filepath: (added, removed)}."""
    stats = {}
    for line in lines:
        parts = line.split("\t")
        if len(parts) != 3:
            continue
        added_str, removed_str, filepath = parts
        # Binary files show '-' for counts
        added = int(added_str) if added_str != "-" else 0
        removed = int(removed_str) if removed_str != "-" else 0
        stats[filepath] = (added, removed)
    return stats


def classify_file(path):
    """Classify a file path into a category string.

    Returns one of: source, test, doc, init, conftest, migration, config,
    hook, script, other
    """
    p = PurePosixPath(path)
    basename = p.name
    parts_set = set(p.parts)

    # Doc files
    if p.suffix == ".md":
        return "doc"

    # JavaScript files
    if p.suffix == ".js":
        # Vitest test files: tests/js/**/*.test.js
        if "tests" in parts_set and basename.endswith(".test.js"):
            return "test"
        # Source: under src/.../static/js/
        if "static" in parts_set and "js" in parts_set and parts_set & {"src"}:
            # Skip vendored / third-party
            if "vendor" in parts_set or "lib" in parts_set:
                return "other"
            return "source"
        return "other"

    # Non-Python files are "other"
    if p.suffix != ".py":
        return "other"

    # Specific basename exclusions
    if basename == "__init__.py":
        return "init"
    if basename == "conftest.py":
        return "conftest"

    # Test files: under tests/ or matching test_*.py / *_test.py
    if (
        "tests" in parts_set
        or basename.startswith("test_")
        or basename.endswith("_test.py")
    ):
        return "test"

    # Check excluded directory patterns
    for part in p.parts:
        if part.lower() in _EXCLUDED_DIRS:
            return _dir_to_category(part.lower())

    # Source files: under src/
    if len(p.parts) > 0 and p.parts[0] == "src":
        return "source"

    return "other"


def _dir_to_category(dirname):
    """Map excluded directory names to categories."""
    mapping = {
        "migrations": "migration",
        "alembic": "migration",
        "config": "config",
        "settings": "config",
        "scripts": "script",
        ".pre-commit-hooks": "hook",
        "examples": "other",
        "docs": "doc",
    }
    return mapping.get(dirname, "other")


def suggest_test_path(source_path):
    """Suggest a test file path for a given source file.

    Python: src/local_deep_research/web/api.py -> tests/web/test_api.py
    JS:     src/local_deep_research/web/static/js/components/foo.js
            -> tests/js/components/foo.test.js
    """
    p = PurePosixPath(source_path)

    # JavaScript: mirror tests/js/<subpath under static/js/>
    if p.suffix == ".js":
        parts = list(p.parts)
        if "static" in parts and "js" in parts:
            js_idx = parts.index("js", parts.index("static"))
            sub_parts = parts[js_idx + 1 :]
            if sub_parts:
                stem = PurePosixPath(sub_parts[-1]).stem
                # Convert snake_case → kebab-case to match existing test naming
                stem = stem.replace("_", "-")
                test_name = f"{stem}.test.js"
                test_parts = ["tests", "js"] + sub_parts[:-1] + [test_name]
                return str(PurePosixPath(*test_parts))
        return f"tests/js/{p.stem.replace('_', '-')}.test.js"

    parts = list(p.parts)

    # Strip leading src/ and package name (e.g., src/local_deep_research/)
    if parts and parts[0] == "src":
        parts = parts[1:]
    if parts and not parts[0].startswith("test"):
        parts = parts[1:]  # Remove package name like local_deep_research

    # Build test path
    if parts:
        test_name = f"test_{parts[-1]}"
        test_parts = ["tests"] + parts[:-1] + [test_name]
    else:
        test_parts = ["tests", f"test_{p.name}"]

    return str(PurePosixPath(*test_parts))


def analyze_commit():
    """Analyze staged files and return a CommitAnalysis."""
    # Get list of staged files with their status
    status_lines = _run_git(["diff", "--cached", "--name-status"])
    new_files = set()
    for line in status_lines:
        parts = line.split("\t")
        if len(parts) >= 2 and parts[0].startswith("A"):
            new_files.add(parts[-1])

    # Get line counts
    numstat_lines = _run_git(["diff", "--cached", "--numstat"])
    stats = _parse_numstat(numstat_lines)

    analysis = CommitAnalysis()

    for filepath, (added, removed) in stats.items():
        category = classify_file(filepath)
        info = StagedFileInfo(
            path=filepath,
            added=added,
            removed=removed,
            is_new=filepath in new_files,
            category=category,
        )

        if category == "source":
            analysis.source_files.append(info)
        elif category == "test":
            analysis.test_files.append(info)
        elif category == "doc":
            analysis.doc_files.append(info)

    return analysis
