# Copyright (C) 2026 NPO NT
# SPDX-License-Identifier: AGPL-3.0-only

"""Run a privacy-safe OCR smoke test over a neutral-name synthetic corpus."""

from __future__ import annotations

import argparse
import json
import re
import statistics
import time
from collections import Counter
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
from typing import Any

from mika_deid.analyzer import LOW_OCR_CONFIDENCE, analyze_document
from mika_deid.loader import load_document


NEUTRAL_PDF_NAME = re.compile(r"document-\d{4}\.pdf", re.IGNORECASE)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Analyze a neutral-name synthetic PDF corpus without exporting OCR text"
    )
    parser.add_argument("input_dir", type=Path)
    parser.add_argument("output_dir", type=Path)
    parser.add_argument("--workers", type=int, default=2)
    parser.add_argument(
        "--confirmed-synthetic",
        action="store_true",
        help="Required acknowledgement that the corpus contains no real patient data",
    )
    return parser.parse_args()


def _analyze_one(source: Path) -> dict[str, Any]:
    started = time.monotonic()
    document = load_document(source)
    result = analyze_document(document)
    public = result.public_dict()
    page_finding_counts = Counter(finding.page_index + 1 for finding in result.findings)
    ocr_confidences = [
        page.ocr_mean_confidence
        for page in document.pages
        if page.ocr_mean_confidence is not None
    ]
    finding_sources = Counter(finding.source for finding in result.findings)
    action_counts = Counter(finding.action.value for finding in result.findings)
    severity_counts = Counter(finding.severity.value for finding in result.findings)
    pages_with_findings = set(page_finding_counts)
    return {
        "schema": "mika-deid-corpus-document-analysis-v1",
        "document_id": source.stem,
        "status": "ok",
        "duration_seconds": round(time.monotonic() - started, 3),
        "analysis": public,
        "qa": {
            "pages_with_findings": len(pages_with_findings),
            "pages_without_findings": len(document.pages) - len(pages_with_findings),
            "finding_sources": dict(sorted(finding_sources.items())),
            "action_counts": dict(sorted(action_counts.items())),
            "severity_counts": dict(sorted(severity_counts.items())),
            "ocr_pages_with_confidence": len(ocr_confidences),
            "ocr_pages_without_confidence": len(document.pages) - len(ocr_confidences),
            "ocr_pages_below_threshold": sum(
                confidence < LOW_OCR_CONFIDENCE for confidence in ocr_confidences
            ),
            "ocr_mean_confidence_min": round(min(ocr_confidences), 2)
            if ocr_confidences
            else None,
            "ocr_mean_confidence_median": round(statistics.median(ocr_confidences), 2)
            if ocr_confidences
            else None,
            "ocr_mean_confidence_max": round(max(ocr_confidences), 2)
            if ocr_confidences
            else None,
        },
        "privacy": {
            "ocr_text_exported": False,
            "original_filename_exported": False,
            "source_sha256_exported": False,
        },
    }


def _safe_error(source: Path, exc: BaseException) -> dict[str, Any]:
    return {
        "schema": "mika-deid-corpus-document-analysis-v1",
        "document_id": source.stem,
        "status": "error",
        "error_type": type(exc).__name__,
        "privacy": {
            "error_message_exported": False,
            "ocr_text_exported": False,
            "original_filename_exported": False,
        },
    }


def _aggregate(records: list[dict[str, Any]], elapsed: float) -> dict[str, Any]:
    category_counts: Counter[str] = Counter()
    source_counts: Counter[str] = Counter()
    action_counts: Counter[str] = Counter()
    severity_counts: Counter[str] = Counter()
    warning_counts: Counter[str] = Counter()
    durations: list[float] = []
    ocr_confidences: list[float] = []
    total_pages = 0
    total_findings = 0
    manual_pages = 0
    pages_without_findings = 0
    low_confidence_pages = 0
    pages_without_ocr_confidence = 0

    successful = [record for record in records if record["status"] == "ok"]
    for record in successful:
        analysis = record["analysis"]
        qa = record["qa"]
        total_pages += analysis["page_count"]
        total_findings += analysis["finding_count"]
        manual_pages += len(analysis["pages_requiring_manual_review"])
        pages_without_findings += qa["pages_without_findings"]
        low_confidence_pages += qa["ocr_pages_below_threshold"]
        pages_without_ocr_confidence += qa["ocr_pages_without_confidence"]
        category_counts.update(analysis["category_counts"])
        source_counts.update(qa["finding_sources"])
        action_counts.update(qa["action_counts"])
        severity_counts.update(qa["severity_counts"])
        durations.append(record["duration_seconds"])
        for warning in analysis["warnings"]:
            normalized = re.sub(r"Страница \d+", "Страница N", warning)
            warning_counts[normalized] += 1
        for key in (
            "ocr_mean_confidence_min",
            "ocr_mean_confidence_median",
            "ocr_mean_confidence_max",
        ):
            value = qa[key]
            if value is not None:
                ocr_confidences.append(float(value))

    return {
        "schema": "mika-deid-corpus-analysis-v1",
        "corpus_classification": "CONFIRMED_FULLY_SYNTHETIC",
        "document_count": len(records),
        "successful_documents": len(successful),
        "failed_documents": len(records) - len(successful),
        "total_pages": total_pages,
        "total_findings": total_findings,
        "category_counts": dict(sorted(category_counts.items())),
        "finding_source_counts": dict(sorted(source_counts.items())),
        "action_counts": dict(sorted(action_counts.items())),
        "severity_counts": dict(sorted(severity_counts.items())),
        "pages_requiring_manual_review": manual_pages,
        "pages_without_findings": pages_without_findings,
        "ocr_pages_below_confidence_threshold": low_confidence_pages,
        "ocr_pages_without_confidence": pages_without_ocr_confidence,
        "warning_type_counts": dict(sorted(warning_counts.items())),
        "elapsed_seconds": round(elapsed, 3),
        "throughput_pages_per_minute": round(total_pages / elapsed * 60, 2)
        if elapsed and total_pages
        else 0,
        "document_duration_seconds": {
            "min": round(min(durations), 3) if durations else None,
            "median": round(statistics.median(durations), 3) if durations else None,
            "max": round(max(durations), 3) if durations else None,
        },
        "ocr_confidence_summary_from_document_extrema_and_medians": {
            "min": round(min(ocr_confidences), 2) if ocr_confidences else None,
            "median": round(statistics.median(ocr_confidences), 2)
            if ocr_confidences
            else None,
            "max": round(max(ocr_confidences), 2) if ocr_confidences else None,
        },
        "privacy": {
            "ocr_text_exported": False,
            "detected_source_text_exported": False,
            "original_filenames_exported": False,
            "source_hashes_exported": False,
        },
        "metric_boundary": {
            "recall_computed": False,
            "precision_computed": False,
            "reason": "The corpus has no coordinate-level ground-truth annotations.",
        },
    }


def main() -> int:
    args = parse_args()
    if not args.confirmed_synthetic:
        raise SystemExit("Refusing corpus OCR without --confirmed-synthetic")
    if args.workers < 1 or args.workers > 4:
        raise SystemExit("--workers must be between 1 and 4")

    input_dir = args.input_dir.expanduser().resolve()
    output_dir = args.output_dir.expanduser().resolve()
    sources = sorted(input_dir.glob("*.pdf"))
    if not sources:
        raise SystemExit("No PDF files found")
    invalid_names = [source.name for source in sources if not NEUTRAL_PDF_NAME.fullmatch(source.name)]
    if invalid_names:
        raise SystemExit("Refusing files whose names are not neutral document-NNNN.pdf identifiers")
    if output_dir.exists() and any(output_dir.iterdir()):
        raise SystemExit("Output directory must be absent or empty")
    output_dir.mkdir(mode=0o700, parents=True, exist_ok=True)

    started = time.monotonic()
    records: list[dict[str, Any]] = []
    with ProcessPoolExecutor(max_workers=args.workers) as executor:
        future_sources = {executor.submit(_analyze_one, source): source for source in sources}
        completed_count = 0
        for future in as_completed(future_sources):
            source = future_sources[future]
            try:
                record = future.result()
            except BaseException as exc:
                record = _safe_error(source, exc)
            records.append(record)
            report_path = output_dir / f"{source.stem}.analysis.json"
            report_path.write_text(
                json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
            )
            report_path.chmod(0o600)
            completed_count += 1
            print(
                json.dumps(
                    {
                        "completed": completed_count,
                        "total": len(sources),
                        "document_id": source.stem,
                        "status": record["status"],
                        "pages": record.get("analysis", {}).get("page_count"),
                        "findings": record.get("analysis", {}).get("finding_count"),
                        "duration_seconds": record.get("duration_seconds"),
                    },
                    ensure_ascii=True,
                ),
                flush=True,
            )

    records.sort(key=lambda record: record["document_id"])
    summary = _aggregate(records, time.monotonic() - started)
    summary_path = output_dir / "corpus-summary.json"
    summary_path.write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
    )
    summary_path.chmod(0o600)
    print(json.dumps({"summary": str(summary_path), **summary}, ensure_ascii=True), flush=True)
    return 0 if summary["failed_documents"] == 0 else 2


if __name__ == "__main__":
    raise SystemExit(main())
