"""Thread-safe in-memory storage for WCAG accessibility scan results."""

import hashlib
import os
import threading
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path

import structlog

from maeris_mcp.types.accessibility import (
    WcagFinding,
    WcagScanResult,
    WcagScanSummary,
    WcagSeverityCounts,
    StoredWcagScan,
)

logger = structlog.get_logger()


@dataclass
class WcagScanMeta:
    scan_root: str
    base_for_rel: str


@dataclass
class WcagScanIndexes:
    by_file: dict[str, list[str]] = field(default_factory=dict)
    by_impact: dict[str, list[str]] = field(default_factory=dict)
    by_id: dict[str, WcagFinding] = field(default_factory=dict)
    fingerprints: set[str] = field(default_factory=set)
    files_served: set[str] = field(default_factory=set)
    files_acknowledged: set[str] = field(default_factory=set)
    last_served_batch: list[str] = field(default_factory=list)


class AccessibilityScanStore:
    """Thread-safe storage for WCAG accessibility scan results."""

    def __init__(self, max_scans: int = 50) -> None:
        self._lock = threading.RLock()
        self._scans: dict[str, StoredWcagScan] = {}
        self._meta: dict[str, WcagScanMeta] = {}
        self._indexes: dict[str, WcagScanIndexes] = {}
        self._max_scans = max_scans

    def create_scan(
        self,
        result: WcagScanResult,
        file_list: list[str],
        scan_root: str,
        base_for_rel: str,
    ) -> str:
        with self._lock:
            self._enforce_max_size()
            now = datetime.now(timezone.utc).isoformat()
            stored = StoredWcagScan(
                id=result.scan_id,
                target_path=result.target_path,
                result=result,
                stored_at=now,
                last_accessed=now,
                status="pending",
                file_list=file_list,
                pushed_to_maeris=False,
                pushed_at=None,
            )
            self._scans[result.scan_id] = stored
            self._meta[result.scan_id] = WcagScanMeta(scan_root=scan_root, base_for_rel=base_for_rel)
            self._indexes[result.scan_id] = WcagScanIndexes()
            return result.scan_id

    def get(self, scan_id: str) -> StoredWcagScan | None:
        with self._lock:
            if scan := self._scans.get(scan_id):
                scan.last_accessed = datetime.now(timezone.utc).isoformat()
                return scan
            return None

    def get_all(self) -> list[StoredWcagScan]:
        with self._lock:
            return list(self._scans.values())

    def get_meta(self, scan_id: str) -> WcagScanMeta | None:
        with self._lock:
            return self._meta.get(scan_id)

    def add_findings(self, scan_id: str, findings: list[WcagFinding]) -> dict[str, int]:
        with self._lock:
            scan = self._scans.get(scan_id)
            indexes = self._indexes.get(scan_id)
            if not scan or not indexes:
                return {"added": 0, "deduped": 0}
            if scan.status == "completed":
                return {"added": 0, "deduped": 0}

            added = 0
            deduped = 0
            for finding in findings:
                if not finding.file_path or not finding.file_path.strip():
                    deduped += 1
                    continue
                fingerprint = self._fingerprint(finding)
                if fingerprint in indexes.fingerprints:
                    deduped += 1
                    continue
                indexes.fingerprints.add(fingerprint)
                scan.result.findings.append(finding)
                self._index_finding(indexes, finding)
                added += 1
            return {"added": added, "deduped": deduped}

    def record_files_served(self, scan_id: str, file_paths: list[str]) -> None:
        with self._lock:
            if idx := self._indexes.get(scan_id):
                idx.last_served_batch = list(file_paths)
                idx.files_served.update(file_paths)

    def acknowledge_last_batch(self, scan_id: str) -> None:
        with self._lock:
            if idx := self._indexes.get(scan_id):
                idx.files_acknowledged.update(idx.last_served_batch)

    def finalize_scan(self, scan_id: str) -> StoredWcagScan | None:
        with self._lock:
            scan = self._scans.get(scan_id)
            if not scan:
                return None
            files_scanned = len(scan.file_list)
            scan.result.summary = self._compute_summary(scan.result.findings, files_scanned)
            scan.status = "completed"
            return scan

    def mark_pushed(self, scan_id: str) -> None:
        with self._lock:
            scan = self._scans.get(scan_id)
            if scan:
                scan.pushed_to_maeris = True
                scan.pushed_at = datetime.now(timezone.utc).isoformat()

    def delete(self, scan_id: str) -> bool:
        with self._lock:
            if scan_id not in self._scans:
                return False
            del self._scans[scan_id]
            self._meta.pop(scan_id, None)
            self._indexes.pop(scan_id, None)
            return True

    def clear(self) -> int:
        with self._lock:
            count = len(self._scans)
            self._scans.clear()
            self._meta.clear()
            self._indexes.clear()
            return count

    def _enforce_max_size(self) -> None:
        if len(self._scans) < self._max_scans:
            return
        oldest_id = min(self._scans.keys(), key=lambda k: self._scans[k].stored_at)
        del self._scans[oldest_id]
        self._meta.pop(oldest_id, None)
        self._indexes.pop(oldest_id, None)

    @staticmethod
    def _fingerprint(finding: WcagFinding) -> str:
        if finding.code_snippet:
            normalized = finding.code_snippet.strip().lower()
            snippet_hash = hashlib.md5(normalized.encode()).hexdigest()[:8]
            return f"{finding.file_path}|{finding.line_start}|{snippet_hash}"
        return f"{finding.file_path}|{finding.line_start}|{finding.rule_id}"

    @staticmethod
    def _index_finding(indexes: WcagScanIndexes, finding: WcagFinding) -> None:
        indexes.by_id[finding.id] = finding
        indexes.by_file.setdefault(finding.file_path, []).append(finding.id)
        indexes.by_impact.setdefault(finding.impact.value, []).append(finding.id)

    @staticmethod
    def _compute_summary(findings: list[WcagFinding], files_scanned: int) -> WcagScanSummary:
        impact_counts = {"minor": 0, "moderate": 0, "serious": 0, "critical": 0}
        for finding in findings:
            if finding.result_type.value == "violation":
                impact_counts[finding.impact.value] = impact_counts.get(finding.impact.value, 0) + 1

        violations = sum(impact_counts.values())
        # Score: starts at 100, deducts by impact
        score = 100 - (impact_counts["critical"] * 15) - (impact_counts["serious"] * 10) - (impact_counts["moderate"] * 5) - (impact_counts["minor"] * 2)
        score = max(0.0, min(100.0, score))

        return WcagScanSummary(
            total_findings=violations,
            by_impact=WcagSeverityCounts(**impact_counts),
            files_scanned=files_scanned,
            compliance_score=score,
        )


# ---------------------------------------------------------------------------
# Persistent disk cache — module-level helpers
# ---------------------------------------------------------------------------

def _get_wcag_cache_path(scan_root: str) -> Path:
    """Return the path to the persistent WCAG findings JSON for this scan root."""
    root_hash = hashlib.sha256(scan_root.encode()).hexdigest()[:16]
    base = Path(os.getenv("MAERIS_CONFIG_DIR") or Path.home() / ".maeris")
    cache_dir = base / "wcag_scans" / root_hash
    cache_dir.mkdir(parents=True, exist_ok=True)
    return cache_dir / "findings.json"


def save_wcag_to_disk(scan_root: str, stored: StoredWcagScan) -> None:
    """Persist a finalized WCAG scan to disk for later push retry."""
    try:
        cache_path = _get_wcag_cache_path(scan_root)
        cache_path.write_text(stored.model_dump_json(by_alias=True, indent=2))
        logger.info("wcag_cache.saved", path=str(cache_path), findings=len(stored.result.findings))
    except Exception as e:
        logger.warning("wcag_cache.save_failed", error=str(e))


def load_wcag_from_disk(scan_root: str) -> StoredWcagScan | None:
    """Load a previously cached WCAG scan from disk."""
    cache_path = _get_wcag_cache_path(scan_root)
    if not cache_path.exists():
        return None
    try:
        return StoredWcagScan.model_validate_json(cache_path.read_text())
    except Exception as e:
        logger.warning("wcag_cache.load_failed", error=str(e))
        return None
