from __future__ import annotations

import hashlib
import logging
from pathlib import Path, PurePosixPath

LOGGER = logging.getLogger(__name__)


class FilesystemSnapshotResolver:
    _SKIP_DIRS = {".git", ".venv", "venv", "__pycache__", ".pytest_cache", "node_modules", "dist", "build", "target"}
    _TEXT_EXTENSIONS = {
        ".py",
        ".md",
        ".txt",
        ".rst",
        ".json",
        ".yaml",
        ".yml",
        ".toml",
        ".ini",
        ".cfg",
        ".env",
        ".js",
        ".ts",
        ".tsx",
        ".jsx",
        ".sql",
        ".sh",
    }

    def augment(self, *, project_id: str, files: list[dict]) -> list[dict]:
        root = self._resolve_root(project_id)
        request_files, excluded_request = self._normalize_request_files(files)
        if root is None:
            if excluded_request:
                LOGGER.warning(
                    "snapshot resolver excluded request files: count=%s reasons=%s",
                    len(excluded_request),
                    excluded_request[:20],
                )
            return request_files
        disk_files = self._collect(root)
        merged = self._merge(disk_files=disk_files, request_files=request_files)
        LOGGER.warning(
            "snapshot resolver summary: project_id=%s request_total=%s request_kept=%s disk_docs=%s merged=%s",
            project_id,
            len(files),
            len(request_files),
            len(disk_files),
            len(merged),
        )
        if excluded_request:
            LOGGER.warning(
                "snapshot resolver excluded request files: count=%s reasons=%s",
                len(excluded_request),
                excluded_request[:20],
            )
        return merged

    def _resolve_root(self, project_id: str) -> Path | None:
        candidate = Path(str(project_id or "").strip()).expanduser()
        if not candidate.is_absolute():
            LOGGER.warning("snapshot resolver skip fs augment: project_id is not absolute (%s)", project_id)
            return None
        if not candidate.exists() or not candidate.is_dir():
            LOGGER.warning("snapshot resolver skip fs augment: project_id is not existing dir (%s)", project_id)
            return None
        return candidate

    def _collect(self, root: Path) -> list[dict]:
        docs_root = root / "docs"
        if not docs_root.exists() or not docs_root.is_dir():
            LOGGER.warning("snapshot resolver: docs root not found (%s)", docs_root)
            return []
        out: list[dict] = []
        for path in sorted(docs_root.rglob("*")):
            if not path.is_file():
                continue
            rel = path.relative_to(root).as_posix()
            if self._should_skip(rel):
                continue
            item = self._read_file(path, rel)
            if item is not None:
                out.append(item)
        return out

    def _should_skip(self, rel_path: str) -> bool:
        parts = PurePosixPath(rel_path).parts
        if not parts:
            return True
        if parts[0] != "docs":
            return True
        if any(part in self._SKIP_DIRS for part in parts):
            return True
        if any(part.startswith(".") for part in parts):
            return True
        return Path(rel_path).suffix.lower() not in self._TEXT_EXTENSIONS

    def _read_file(self, path: Path, rel_path: str) -> dict | None:
        raw = path.read_bytes()
        if b"\x00" in raw:
            return None
        content = raw.decode("utf-8", errors="ignore")
        return {
            "path": rel_path,
            "content": content,
            "content_hash": hashlib.sha256(content.encode("utf-8")).hexdigest(),
        }

    def _merge(self, *, disk_files: list[dict], request_files: list[dict]) -> list[dict]:
        merged: dict[str, dict] = {}
        for item in disk_files:
            path = str(item.get("path") or "").strip()
            if path:
                merged[path] = item
        for item in request_files:
            path = str(item.get("path") or "").strip()
            if path:
                # Request payload has priority for same path
                merged[path] = item
        return list(merged.values())

    def _normalize_request_files(self, files: list[dict]) -> tuple[list[dict], list[str]]:
        kept: list[dict] = []
        excluded: list[str] = []
        for item in files:
            path = str(item.get("path") or "").replace("\\", "/").strip()
            if not path:
                excluded.append("empty_path")
                continue
            rel_path = self._normalize_rel_path(path)
            if rel_path is None:
                excluded.append(f"{path}:outside_docs")
                continue
            normalized = dict(item)
            normalized["path"] = rel_path
            kept.append(normalized)
        return kept, excluded

    def _normalize_rel_path(self, path: str) -> str | None:
        marker = "/docs/"
        lowered = path.lower()
        if lowered.startswith("docs/"):
            rel = path
        elif marker in lowered:
            idx = lowered.index(marker) + 1
            rel = path[idx:]
        else:
            return None
        rel = rel.lstrip("/").replace("\\", "/")
        return rel if rel.startswith("docs/") else None
