from __future__ import annotations

import os
from pathlib import Path
from typing import Iterable

from pathspec import PathSpec

COMMON_IGNORE_PATTERNS = [
    "**/.git/",
    "**/.hg/",
    "**/.svn/",
    "**/.DS_Store",
    "**/__pycache__/",
    "**/*.pyc",
    "**/.venv/",
    "**/venv/",
    "**/node_modules/",
    "**/dist/",
    "**/build/",
    "**/.idea/",
    "**/.vscode/",
    "**/.mypy_cache/",
    "**/.pytest_cache/",
    "**/coverage/",
    "**/.coverage",
    "**/*.log",
]

COMMON_IGNORE_DIRS = {
    ".git",
    ".hg",
    ".svn",
    "node_modules",
    "dist",
    "build",
    ".venv",
    "venv",
    "__pycache__",
    ".idea",
    ".vscode",
    ".mypy_cache",
    ".pytest_cache",
}


def _convert_gitignore_pattern(
    pattern: str,
    base_dir: Path,
    repo_root: Path,
) -> str | None:
    stripped = pattern.strip()
    if not stripped or stripped.startswith("#"):
        return None

    negated = stripped.startswith("!")
    if negated:
        stripped = stripped[1:]

    if not stripped:
        return None

    rel_base = base_dir.relative_to(repo_root).as_posix()
    prefix = "" if rel_base == "." else f"{rel_base}/"

    anchored = stripped.startswith("/")
    if anchored:
        body = stripped.lstrip("/")
        converted = f"{prefix}{body}"
    else:
        if "/" in stripped:
            converted = f"{prefix}{stripped}"
        else:
            converted = f"{prefix}**/{stripped}"

    return f"!{converted}" if negated else converted


def _load_gitignore_files(repo_root: Path) -> list[Path]:
    gitignores: list[Path] = []
    for root, dirs, files in os.walk(repo_root):
        dirs[:] = [d for d in dirs if d not in COMMON_IGNORE_DIRS]
        if ".gitignore" in files:
            gitignores.append(Path(root) / ".gitignore")
    return sorted(gitignores, key=lambda p: len(p.relative_to(repo_root).parts))


def _load_patterns(repo_root: Path) -> list[str]:
    patterns: list[str] = list(COMMON_IGNORE_PATTERNS)
    for gitignore in _load_gitignore_files(repo_root):
        try:
            content = gitignore.read_text(encoding="utf-8")
        except OSError:
            continue
        for line in content.splitlines():
            converted = _convert_gitignore_pattern(line, gitignore.parent, repo_root)
            if converted:
                patterns.append(converted)
    return patterns


def build_ignore_spec(repo_root: Path) -> PathSpec:
    patterns = _load_patterns(repo_root)
    return PathSpec.from_lines("gitwildmatch", patterns)


class IgnoreMatcher:
    def __init__(self, repo_root: Path) -> None:
        self.repo_root = repo_root
        self.spec = build_ignore_spec(repo_root)

    def is_ignored(self, path: Path) -> bool:
        try:
            relpath = path.relative_to(self.repo_root).as_posix()
        except ValueError:
            relpath = path.as_posix()
        if path.is_dir() and not relpath.endswith("/"):
            relpath = f"{relpath}/"
        if self.spec.match_file(relpath):
            return True
        if not path.is_dir():
            parent = path.parent
            while parent != self.repo_root and parent != parent.parent:
                parent_rel = parent.relative_to(self.repo_root).as_posix()
                if not parent_rel.endswith("/"):
                    parent_rel = f"{parent_rel}/"
                if self.spec.match_file(parent_rel):
                    return True
                parent = parent.parent
        return False

    def is_ignored_rel(self, relpath: str) -> bool:
        return self.spec.match_file(relpath)


def iter_ignored(paths: Iterable[Path], matcher: IgnoreMatcher) -> Iterable[Path]:
    for path in paths:
        if matcher.is_ignored(path):
            yield path
