"""
Per-repo scan cache. Stores file hashes and condensed scan notes to avoid
re-reading unchanged files across sessions.

Cache location: ~/.maeris/repos/<repo_hash>/scan_cache.json
"""
import hashlib
import json
import os
import time
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import Optional
import structlog

logger = structlog.get_logger()

_CACHE_VERSION = 1


@dataclass
class ScanCacheEntry:
    version: int = _CACHE_VERSION
    repo_hash: str = ""
    scan_root: str = ""
    last_scanned: float = 0.0  # unix timestamp
    file_hashes: dict = field(default_factory=dict)  # path -> sha256 hex
    app_domain: str = ""
    condensed_scan_notes: str = ""  # JSON-serialized condensed notes
    last_flow_description: str = ""
    # Per-file fingerprints to enable context-aware cache reuse across flows.
    # Key: absolute file path, Value: dict with tokens/selectors/labels/routes/imports
    file_fingerprints: dict = field(default_factory=dict)
    # Persisted page_elements extracted by the LLM during Phase 1.
    # Structure: {"page_elements": [...], "last_updated": float}
    # Injected into accumulated_notes at the start of each new session so
    # the LLM does not need to re-extract selectors for unchanged files.
    selector_cache: dict = field(default_factory=dict)


class RepoScanCache:
    def __init__(self, repo_root: str):
        self.repo_root = os.path.abspath(repo_root)
        self.repo_hash = hashlib.sha256(self.repo_root.encode()).hexdigest()[:16]
        self._cache_path = Path.home() / ".maeris" / "repos" / self.repo_hash / "scan_cache.json"
        self._entry: Optional[ScanCacheEntry] = None

    def load(self) -> ScanCacheEntry:
        if self._entry is not None:
            return self._entry
        try:
            if self._cache_path.exists():
                data = json.loads(self._cache_path.read_text(encoding="utf-8"))
                if data.get("version") == _CACHE_VERSION and data.get("scan_root") == self.repo_root:
                    data.setdefault("selector_cache", {})  # compat: field added after initial release
                    self._entry = ScanCacheEntry(**data)
                    logger.info("scan_cache.loaded", files=len(self._entry.file_hashes))
                    return self._entry
        except Exception as e:
            logger.warning("scan_cache.load_failed", error=str(e))
        self._entry = ScanCacheEntry(
            version=_CACHE_VERSION,
            repo_hash=self.repo_hash,
            scan_root=self.repo_root,
        )
        return self._entry

    def save(self) -> None:
        if self._entry is None:
            return
        try:
            self._cache_path.parent.mkdir(parents=True, exist_ok=True)
            self._entry.last_scanned = time.time()
            self._cache_path.write_text(
                json.dumps(asdict(self._entry), indent=2), encoding="utf-8"
            )
            logger.info("scan_cache.saved", files=len(self._entry.file_hashes))
        except Exception as e:
            logger.warning("scan_cache.save_failed", error=str(e))

    def hash_file(self, file_path: str) -> str:
        """Compute SHA-256 of file content."""
        try:
            content = Path(file_path).read_bytes()
            return hashlib.sha256(content).hexdigest()
        except Exception:
            return ""

    def get_changed_files(self, file_list: list[str]) -> tuple[list[str], list[str]]:
        """
        Returns (changed_files, unchanged_files).
        changed_files: files whose hash differs from cache or are new.
        unchanged_files: files present in cache with matching hash.
        """
        entry = self.load()
        changed, unchanged = [], []
        for path in file_list:
            current_hash = self.hash_file(path)
            cached_hash = entry.file_hashes.get(path, "")
            if current_hash and current_hash == cached_hash:
                unchanged.append(path)
            else:
                changed.append(path)
                if current_hash:
                    entry.file_hashes[path] = current_hash
        return changed, unchanged

    def update_file_hash(self, file_path: str) -> None:
        entry = self.load()
        h = self.hash_file(file_path)
        if h:
            entry.file_hashes[file_path] = h

    def get_condensed_notes(self) -> str:
        return self.load().condensed_scan_notes

    def set_condensed_notes(self, notes: str) -> None:
        self.load().condensed_scan_notes = notes
        self.save()

    def get_last_flow_description(self) -> str:
        return self.load().last_flow_description

    def set_last_flow_description(self, flow_description: str) -> None:
        self.load().last_flow_description = flow_description or ""
        self.save()

    def set_app_domain(self, domain: str) -> None:
        self.load().app_domain = domain
        self.save()

    def get_selector_cache(self) -> list[dict]:
        """Return cached page_elements from a previous session.

        Returns an empty list if no selector cache exists yet.
        """
        return self.load().selector_cache.get("page_elements", [])

    def set_selector_cache(self, page_elements: list[dict]) -> None:
        """Persist the LLM-extracted page_elements for this repo.

        Called at the end of a successful Phase 1 scan so future sessions
        can start with known selectors already in accumulated_notes.
        Never raises.
        """
        try:
            self.load().selector_cache = {
                "page_elements": page_elements,
                "last_updated": time.time(),
            }
            self.save()
        except Exception as e:
            logger.warning("scan_cache.set_selector_cache_failed", error=str(e))

    def update_selector(self, old_selector: str, new_selector: str) -> bool:
        """Replace a broken selector with a confirmed working one in the cache.

        Called after run_and_fix confirms a selector fix so future sessions
        start with the corrected selector. Returns True if any entry was updated.
        Never raises.
        """
        try:
            updated = False
            for page_entry in self.load().selector_cache.get("page_elements", []):
                for element in page_entry.get("elements", []):
                    if element.get("selector") == old_selector:
                        element["selector"] = new_selector
                        updated = True
            if updated:
                self.save()
            return updated
        except Exception as e:
            logger.warning("scan_cache.update_selector_failed", error=str(e))
            return False

    def get_file_fingerprints(self) -> dict:
        return self.load().file_fingerprints or {}

    def update_file_fingerprints(self, updates: dict) -> None:
        """Merge per-file fingerprints into cache. Never raises."""
        try:
            entry = self.load()
            entry.file_fingerprints.update(updates or {})
            self.save()
        except Exception as e:
            logger.warning("scan_cache.update_fingerprints_failed", error=str(e))
