"""Breach comparison — match suspect files against the attestation registry.

Computes hashes of suspect files and verifies them in batch via the
CertiSigma API. Matches indicate the file was previously inventoried,
proving it originated from the organisation's assets.
"""

from __future__ import annotations

import logging
from concurrent.futures import ProcessPoolExecutor, as_completed
from contextlib import nullcontext
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, Sequence

import click
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 MatchResult:
    hash_hex: str
    suspect_path: str
    attestation_id: Optional[str] = None
    timestamp: Optional[str] = None
    level: Optional[str] = None
    source: Optional[str] = None
    inventory_path: Optional[str] = None


@dataclass
class CompareReport:
    total_suspect: int = 0
    matched: int = 0
    not_matched: int = 0
    errors: int = 0
    matches: list[MatchResult] = field(default_factory=list)


def compare_directory(
    suspect_dir: Path,
    client: CertiSigmaClient,
    *,
    local_manifest: Optional[Manifest] = None,
    show_progress: bool = False,
    include_globs: Sequence[str] = (),
    exclude_globs: Sequence[str] = (),
    min_file_size: int = 0,
    max_file_size: int = 0,
    detailed: bool = False,
    workers: int = 1,
) -> CompareReport:
    """Compare suspect files against the CertiSigma registry.

    If *local_manifest* is provided, matched hashes are cross-referenced
    with the original file path from the inventory.

    When *detailed* is True, ``batch_verify`` is called with
    ``detailed=True``, returning enriched per-hash data: source label,
    certification level (T0/T1/T2), and tier timestamps.  Requires an
    authenticated client (API key).
    """
    from .scanner import DEFAULT_MAX_FILE_SIZE, MAX_WORKERS, walk_files

    workers = max(1, min(workers, MAX_WORKERS))

    if max_file_size <= 0:
        max_file_size = DEFAULT_MAX_FILE_SIZE

    suspect_dir = suspect_dir.resolve()
    report = CompareReport()

    file_list = list(walk_files(
        suspect_dir,
        include_globs=include_globs,
        exclude_globs=exclude_globs,
        min_file_size=min_file_size,
        max_file_size=max_file_size,
    ))

    hash_to_paths: dict[str, list[str]] = {}

    if workers > 1 and len(file_list) > 1:
        bar_ctx = (
            click.progressbar(length=len(file_list), label="Hashing suspects", show_pos=True)
            if show_progress
            else None
        )
        with ProcessPoolExecutor(max_workers=workers) as pool:
            futures = {
                pool.submit(hash_file, str(fp)): fp for fp in file_list
            }
            try:
                if bar_ctx:
                    bar_ctx = bar_ctx.__enter__()
                for future in as_completed(futures):
                    fpath = futures[future]
                    try:
                        h = future.result()
                        hash_to_paths.setdefault(h, []).append(
                            str(fpath.relative_to(suspect_dir)))
                        report.total_suspect += 1
                    except Exception as exc:
                        logger.warning("Cannot hash %s: %s", fpath, exc)
                        report.errors += 1
                    if bar_ctx:
                        bar_ctx.update(1)
            finally:
                if bar_ctx:
                    bar_ctx.__exit__(None, None, None)
    else:
        hash_ctx = click.progressbar(file_list, label="Hashing suspects", show_pos=True) if show_progress and file_list else nullcontext(file_list)
        with hash_ctx as bar:
            for fpath in bar:
                try:
                    h = hash_file(str(fpath))
                    hash_to_paths.setdefault(h, []).append(
                        str(fpath.relative_to(suspect_dir)))
                    report.total_suspect += 1
                except Exception as exc:
                    logger.warning("Cannot hash %s: %s", fpath, exc)
                    report.errors += 1

    hashes = list(hash_to_paths.keys())
    logger.info("Hashed %d suspect files, verifying in batches of %d", len(hashes), BATCH_SIZE)

    batches = [hashes[i : i + BATCH_SIZE] for i in range(0, len(hashes), BATCH_SIZE)]
    verify_ctx = click.progressbar(batches, label="Verifying", show_pos=True) if show_progress and batches else nullcontext(batches)
    with verify_ctx as bar:
        for batch in bar:
            try:
                result = retry_api_call(
                    client.batch_verify, batch,
                    detailed=detailed,
                )
                for item in result.results:
                    if item.get("exists"):
                        h = item["hash_hex"]
                        inv_path = None
                        if local_manifest:
                            entry = local_manifest.get(h)
                            if entry:
                                inv_path = entry.path
                        for sp in hash_to_paths.get(h, []):
                            match = MatchResult(
                                hash_hex=h,
                                suspect_path=sp,
                                attestation_id=item.get("id"),
                                timestamp=item.get("timestamp"),
                                level=item.get("level"),
                                source=item.get("source"),
                                inventory_path=inv_path,
                            )
                            report.matches.append(match)
                        report.matched += 1
                    else:
                        report.not_matched += 1
            except Exception as exc:
                logger.error("Batch verify failed: %s", exc)
                report.errors += len(batch)

    return report
