from __future__ import annotations

import os
import subprocess
from pathlib import Path
from typing import Any, Callable, Iterable


DEFAULT_EXCLUDED_DIRS = [
    ".git",
    "node_modules",
    "dist",
    "build",
    ".venv",
    "__pycache__",
    "docs",
    "gar.egg-info",
]
DEFAULT_EXCLUDED_SUFFIXES = [
    ".lock",
    ".min.js",
    ".md",
]

DEFAULT_EXTENSIONS = [
    ".py",
    ".js",
    ".ts",
    ".vue",
    ".java",
    ".go",
    ".cs",
]


def _run_git(args: list[str], cwd: str | None = None) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["git", *args],
        cwd=cwd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )


def _get_repo_root() -> str:
    result = _run_git(["rev-parse", "--show-toplevel"])
    if result.returncode != 0:
        return str(Path.cwd().resolve())
    return result.stdout.decode("utf-8", errors="replace").strip() or str(Path.cwd().resolve())


def _get_staged_paths(repo_root: str) -> list[str]:
    result = _run_git(["diff", "--cached", "--name-only", "--diff-filter=ACMR"], cwd=repo_root)
    if result.returncode != 0:
        return []
    output = result.stdout.decode("utf-8", errors="replace")
    return [line.strip() for line in output.splitlines() if line.strip()]


def _get_staged_blob(repo_root: str, path: str) -> bytes | None:
    result = _run_git(["show", f":{path}"], cwd=repo_root)
    if result.returncode != 0:
        return None
    return result.stdout


def _get_staged_diff(repo_root: str) -> str | None:
    result = _run_git(["diff", "--cached"], cwd=repo_root)
    if result.returncode != 0:
        return None
    return result.stdout.decode("utf-8", errors="replace")


def _is_excluded_path(path: str, excluded_dirs: Iterable[str], excluded_suffixes: Iterable[str]) -> bool:
    parts = path.split("/")
    if any(part in excluded_dirs for part in parts):
        return True
    return any(path.endswith(suffix) for suffix in excluded_suffixes)


def _iter_repo_files(
    repo_root: str,
    *,
    excluded_dirs: Iterable[str],
    excluded_suffixes: Iterable[str],
    extensions: Iterable[str],
) -> list[str]:
    result: list[str] = []
    for root, dirs, files in os.walk(repo_root):
        dirs[:] = [d for d in dirs if d not in excluded_dirs]
        for filename in files:
            rel_path = os.path.relpath(os.path.join(root, filename), repo_root)
            if _is_excluded_path(rel_path, excluded_dirs, excluded_suffixes):
                continue
            if extensions and not any(filename.endswith(ext) for ext in extensions):
                continue
            result.append(rel_path)
    return result


def _select_full_paths(
    repo_root: str,
    staged_paths: list[str],
    *,
    extensions: Iterable[str],
    excluded_dirs: Iterable[str],
    excluded_suffixes: Iterable[str],
    top_n: int,
) -> tuple[list[str], bool]:
    candidates = _iter_repo_files(
        repo_root,
        excluded_dirs=excluded_dirs,
        excluded_suffixes=excluded_suffixes,
        extensions=extensions,
    )
    if not candidates:
        return []

    staged_set = {p for p in staged_paths if p in candidates}
    interface_set = {p for p in candidates if "interface" in p.lower()}
    src_set = {p for p in candidates if "/src/" in f"/{p}"}

    ordered: list[str] = []
    for bucket in (staged_set, interface_set, src_set):
        for p in candidates:
            if p in bucket and p not in ordered:
                ordered.append(p)
    for p in candidates:
        if p not in ordered:
            ordered.append(p)

    if top_n > 0 and len(ordered) > top_n:
        return ordered[:top_n], True
    return ordered, False


def _read_worktree_file(repo_root: str, path: str) -> bytes | None:
    abs_path = Path(repo_root) / path
    if not abs_path.exists() or not abs_path.is_file():
        return None
    try:
        return abs_path.read_bytes()
    except Exception:
        return None


def _decode_text(blob: bytes) -> tuple[str, bool]:
    if b"\x00" in blob:
        return "", True
    return blob.decode("utf-8", errors="replace"), False


def build_input_context(
    mode: str,
    scope: str,
    *,
    max_chars: int = 80000,
    top_n: int = 200,
    exclude_rules: dict[str, list[str]] | None = None,
    status_hook: Callable[[str], None] | None = None,
    progress_handle=None,
) -> dict[str, Any]:
    if status_hook:
        if scope == "staged":
            status_hook("준비: git staged 파일 수집 중")
        else:
            status_hook("준비: 전체 코드 파일 수집 중")
    excluded_dirs = exclude_rules.get("excluded_dirs", []) if exclude_rules else DEFAULT_EXCLUDED_DIRS
    excluded_suffixes = exclude_rules.get("excluded_suffixes", []) if exclude_rules else DEFAULT_EXCLUDED_SUFFIXES
    extensions = exclude_rules.get("extensions", []) if exclude_rules else DEFAULT_EXTENSIONS
    if mode == "interface":
        extensions = [".py"]

    repo_root = _get_repo_root()
    staged_paths = _get_staged_paths(repo_root)

    limited_by_count = False
    if scope == "staged":
        selected_paths = [p for p in staged_paths if not _is_excluded_path(p, excluded_dirs, excluded_suffixes)]
    else:
        selected_paths, limited_by_count = _select_full_paths(
            repo_root,
            staged_paths,
            extensions=extensions,
            excluded_dirs=excluded_dirs,
            excluded_suffixes=excluded_suffixes,
            top_n=top_n,
        )

    if status_hook:
        status_hook("전처리: 대상 파일 선별/읽기")
    
    # progress handle이 있으면 total 설정
    from common.ui import update_file_read_progress
    if progress_handle:
        update_file_read_progress(progress_handle, total=len(selected_paths))
    
    files: list[dict[str, Any]] = []
    for path in selected_paths:
        if _is_excluded_path(path, excluded_dirs, excluded_suffixes):
            continue
        if scope == "staged":
            blob = _get_staged_blob(repo_root, path)
            source = "index"
        else:
            blob = _read_worktree_file(repo_root, path)
            source = "worktree"

        if blob is None:
            continue
        text, is_binary = _decode_text(blob)
        if is_binary:
            continue
        truncated = False
        if max_chars and len(text) > max_chars:
            text = text[:max_chars]
            truncated = True

        files.append(
            {
                "path": path,
                "content": text,
                "is_binary": False,
                "truncated": truncated,
                "source": source,
            }
        )
        
        # 파일 읽기 진행률 업데이트
        if progress_handle:
            update_file_read_progress(progress_handle)

    diff = _get_staged_diff(repo_root) if scope == "staged" else None

    if status_hook:
        status_hook("전처리: 대상 파일 선별/읽기 완료")

    return {
        "repo_root": repo_root,
        "mode": mode,
        "scope": scope,
        "staged_paths": staged_paths,
        "selected_paths": selected_paths,
        "files": files,
        "diff": diff,
        "meta": {
            "max_chars": max_chars,
            "excluded_dirs": list(excluded_dirs),
            "excluded_suffixes": list(excluded_suffixes),
            "limited_by_count": limited_by_count,
        },
    }
