"""Thread-safe in-memory storage for GDPR 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.gdpr import (
    GdprFinding,
    GdprScanResult,
    GdprScanSummary,
    GdprSeverityCounts,
    StoredGdprScan,
)

logger = structlog.get_logger()


@dataclass
class GdprScanMeta:
    scan_root: str
    base_for_rel: str


@dataclass
class GdprScanIndexes:
    by_file: dict[str, list[str]] = field(default_factory=dict)
    by_severity: dict[str, list[str]] = field(default_factory=dict)
    by_category: dict[str, list[str]] = field(default_factory=dict)
    by_id: dict[str, GdprFinding] = 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 GdprScanStore:
    """Thread-safe storage for GDPR scan results."""

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

    def create_scan(
        self,
        result: GdprScanResult,
        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 = StoredGdprScan(
                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] = GdprScanMeta(scan_root=scan_root, base_for_rel=base_for_rel)
            self._indexes[result.scan_id] = GdprScanIndexes()
            return result.scan_id

    def get(self, scan_id: str) -> StoredGdprScan | 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[StoredGdprScan]:
        with self._lock:
            return list(self._scans.values())

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

    def add_findings(self, scan_id: str, findings: list[GdprFinding]) -> 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) -> StoredGdprScan | 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: GdprFinding) -> 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.category}"

    @staticmethod
    def _index_finding(indexes: GdprScanIndexes, finding: GdprFinding) -> None:
        indexes.by_id[finding.id] = finding
        indexes.by_file.setdefault(finding.file_path, []).append(finding.id)
        indexes.by_severity.setdefault(finding.severity.value, []).append(finding.id)
        indexes.by_category.setdefault(finding.category, []).append(finding.id)

    @staticmethod
    def _compute_summary(findings: list[GdprFinding], files_scanned: int) -> GdprScanSummary:
        severity_counts = {"low": 0, "medium": 0, "high": 0, "critical": 0}
        category_counts: dict[str, int] = {}
        for finding in findings:
            severity_counts[finding.severity.value] = severity_counts.get(finding.severity.value, 0) + 1
            category_counts[finding.category] = category_counts.get(finding.category, 0) + 1

        score = 100 - (severity_counts["critical"] * 15) - (severity_counts["high"] * 10) - (severity_counts["medium"] * 5) - (severity_counts["low"] * 2)
        score = max(0.0, min(100.0, score))

        return GdprScanSummary(
            total_findings=len(findings),
            by_severity=GdprSeverityCounts(**severity_counts),
            by_category=category_counts,
            files_scanned=files_scanned,
            compliance_score=score,
        )


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

def _get_gdpr_cache_path(scan_root: str) -> Path:
    """Return the path to the persistent GDPR 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 / "gdpr_scans" / root_hash
    cache_dir.mkdir(parents=True, exist_ok=True)
    return cache_dir / "findings.json"


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


def load_gdpr_from_disk(scan_root: str) -> StoredGdprScan | None:
    """Load a previously cached GDPR scan from disk."""
    cache_path = _get_gdpr_cache_path(scan_root)
    if not cache_path.exists():
        return None
    try:
        return StoredGdprScan.model_validate_json(cache_path.read_text())
    except Exception as e:
        logger.warning("gdpr_cache.load_failed", error=str(e))
        return None
