"""Forensic evidence pipeline — verify hashes and check file integrity.

Provides three core operations:

1. **verify_hash**: Verify a hash against the CertiSigma registry and fetch
   the full evidence chain (T0/T1/T2).
2. **integrity_check**: Re-hash files in a manifest and compare against
   stored baselines to detect tampering.
3. **verify_manifest**: Full-chain verification — check every hash in a
   manifest against the CertiSigma attestation registry.

All operations respect Census's core principle: file content never leaves the
client.  Only SHA-256 hashes are sent to the API.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from certisigma import CertiSigmaClient, hash_file

from .manifest import Manifest
from .retry import retry_api_call

logger = logging.getLogger(__name__)

BATCH_SIZE = 100


@dataclass
class VerifyDetail:
    """Consolidated verification result for a single hash."""

    hash_hex: str
    exists: bool
    attestation_id: Optional[str] = None
    level: Optional[str] = None
    timestamp: Optional[str] = None
    source: Optional[str] = None
    t0: Optional[dict] = None
    t1: Optional[dict] = None
    t2: Optional[dict] = None
    blockchain_url: Optional[str] = None
    blockchain_tx_url: Optional[str] = None


@dataclass
class ModifiedFile:
    """A file whose current hash differs from the manifest baseline."""

    path: str
    expected_hash: str
    actual_hash: str
    expected_size: int
    actual_size: int


@dataclass
class IntegrityReport:
    """Result of an integrity check against a manifest."""

    root_dir: str = ""
    total_in_manifest: int = 0
    total_checked: int = 0
    unchanged: int = 0
    modified: list[ModifiedFile] = field(default_factory=list)
    missing: list[str] = field(default_factory=list)
    new_files: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)

    @property
    def has_discrepancies(self) -> bool:
        return bool(self.modified or self.missing or self.new_files)


@dataclass
class ManifestVerifyEntry:
    """Verification status for a single manifest entry."""

    hash_hex: str
    path: str
    verified: bool = False
    level: Optional[str] = None
    attestation_id: Optional[str] = None
    timestamp: Optional[str] = None
    source: Optional[str] = None
    error: Optional[str] = None


@dataclass
class ManifestVerifyReport:
    """Result of full-chain manifest verification against the registry."""

    manifest_path: str = ""
    root_dir: str = ""
    total: int = 0
    verified: int = 0
    not_found: int = 0
    errors: int = 0
    entries: list[ManifestVerifyEntry] = field(default_factory=list)

    @property
    def has_discrepancies(self) -> bool:
        return self.not_found > 0 or self.errors > 0


def verify_manifest(
    manifest: Manifest,
    client: CertiSigmaClient,
    *,
    manifest_path: str = "",
    show_progress: bool = False,
    detailed: bool = False,
) -> ManifestVerifyReport:
    """Verify all hashes in a manifest against the CertiSigma registry.

    Sends hashes in batches of 100 via batch_verify. Reports per-entry
    attestation status including certification level (T0/T1/T2).

    This completes the trust chain: file ↔ manifest ↔ registry.
    """
    import click

    report = ManifestVerifyReport(
        manifest_path=manifest_path,
        root_dir=manifest.root_dir,
    )

    all_entries = list(manifest.iter_entries())
    report.total = len(all_entries)

    if report.total == 0:
        return report

    hash_to_paths: dict[str, list[str]] = {}
    for entry in all_entries:
        hash_to_paths.setdefault(entry.hash_hex, []).append(entry.path)

    unique_hashes = list(hash_to_paths.keys())
    batches = [unique_hashes[i:i + BATCH_SIZE] for i in range(0, len(unique_hashes), BATCH_SIZE)]

    verified_set: dict[str, dict] = {}

    if show_progress and batches:
        bar_ctx = click.progressbar(batches, label="Verifying attestations", show_pos=True)
    else:
        from contextlib import nullcontext
        bar_ctx = nullcontext(batches)

    with bar_ctx as bar:
        for batch in bar:
            try:
                result = retry_api_call(
                    client.batch_verify, batch,
                    detailed=detailed,
                )
                for item in result.results:
                    h = item["hash_hex"]
                    if item.get("exists"):
                        verified_set[h] = {
                            "verified": True,
                            "level": item.get("level"),
                            "attestation_id": item.get("id"),
                            "timestamp": item.get("timestamp"),
                            "source": item.get("source"),
                        }
                    else:
                        verified_set[h] = {"verified": False}
            except Exception as exc:
                logger.error("Batch verify failed: %s", exc)
                for h in batch:
                    verified_set[h] = {"error": str(exc)}

    for entry in all_entries:
        info = verified_set.get(entry.hash_hex)
        if info is None:
            ve = ManifestVerifyEntry(
                hash_hex=entry.hash_hex,
                path=entry.path,
                error="Not in verify response",
            )
            report.errors += 1
        elif "error" in info:
            ve = ManifestVerifyEntry(
                hash_hex=entry.hash_hex,
                path=entry.path,
                error=info["error"],
            )
            report.errors += 1
        elif info.get("verified"):
            ve = ManifestVerifyEntry(
                hash_hex=entry.hash_hex,
                path=entry.path,
                verified=True,
                level=info.get("level"),
                attestation_id=info.get("attestation_id"),
                timestamp=info.get("timestamp"),
                source=info.get("source"),
            )
            report.verified += 1
        else:
            ve = ManifestVerifyEntry(
                hash_hex=entry.hash_hex,
                path=entry.path,
                verified=False,
            )
            report.not_found += 1

        report.entries.append(ve)

    return report


def verify_hash(
    client: CertiSigmaClient,
    hash_hex: str,
    *,
    fetch_evidence: bool = True,
) -> VerifyDetail:
    """Verify a hash and optionally fetch the full evidence chain.

    Steps:
    1. client.verify(hash) — check if attested
    2. If attested and fetch_evidence: client.get_evidence(id) — T0/T1/T2
    3. Extract blockchain URLs via SDK helpers

    All API calls use retry_api_call for resilience.
    """
    vr = retry_api_call(client.verify, hash_hex)

    detail = VerifyDetail(
        hash_hex=vr.hash_hex,
        exists=vr.exists,
        attestation_id=vr.id,
        level=vr.level,
        timestamp=vr.timestamp,
        source=vr.source,
    )

    if not vr.exists or not vr.id or not fetch_evidence:
        return detail

    try:
        from certisigma import get_blockchain_url

        ev = retry_api_call(client.get_evidence, vr.id)
        detail.t0 = ev.t0
        detail.t1 = ev.t1
        detail.t2 = ev.t2
        detail.level = ev.level
        detail.blockchain_url = get_blockchain_url(ev, type="block")
        detail.blockchain_tx_url = get_blockchain_url(ev, type="tx")
    except Exception as exc:
        logger.warning("Could not fetch evidence for %s: %s", vr.id, exc)

    return detail


def integrity_check(
    manifest: Manifest,
    *,
    show_progress: bool = False,
) -> IntegrityReport:
    """Re-hash all files in the manifest's root directory and compare.

    This is a 100% local operation — no API calls, no API key needed.
    Reuses walk_files() from scanner for consistency with scan filters.

    Returns an IntegrityReport with unchanged/modified/missing/new counts.
    """
    from .scanner import walk_files

    root = Path(manifest.root_dir).resolve()
    report = IntegrityReport(root_dir=str(root))

    manifest_paths: dict[str, tuple[str, int]] = {}
    for entry in manifest.iter_entries():
        manifest_paths[entry.path] = (entry.hash_hex, entry.size)
        report.total_in_manifest += 1

    disk_paths: set[str] = set()

    import click

    file_list = list(walk_files(root))

    if show_progress and file_list:
        ctx = click.progressbar(file_list, label="Checking integrity", show_pos=True)
    else:
        from contextlib import nullcontext
        ctx = nullcontext(file_list)

    with ctx as bar:
        for fpath in bar:
            try:
                rel = str(fpath.relative_to(root))
            except ValueError:
                continue

            disk_paths.add(rel)
            report.total_checked += 1

            baseline = manifest_paths.get(rel)
            if baseline is None:
                report.new_files.append(rel)
                continue

            expected_hash, expected_size = baseline

            try:
                actual_hash = hash_file(str(fpath))
                actual_size = fpath.stat().st_size
            except Exception as exc:
                report.errors.append(f"{rel}: {exc}")
                continue

            if actual_hash == expected_hash:
                report.unchanged += 1
            else:
                report.modified.append(ModifiedFile(
                    path=rel,
                    expected_hash=expected_hash,
                    actual_hash=actual_hash,
                    expected_size=expected_size,
                    actual_size=actual_size,
                ))

    for rel_path in manifest_paths:
        if rel_path not in disk_paths:
            report.missing.append(rel_path)

    return report
