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

from __future__ import annotations

import json
import hashlib
import os
import tempfile
from collections import Counter
from pathlib import Path

from . import __version__
from .models import Action, AnalysisResult, Box, Finding, Severity, VerificationResult
from .ocr import TesseractOcr
from .sanitizer import SanitizationError, build_raster_pdf
from .verifier import verify_raster_pdf


RESIDUAL_REVIEW_MARGIN = 12


class ReleaseBlockedError(RuntimeError):
    pass


class ResidualFindingsForReview(ReleaseBlockedError):
    def __init__(self, findings: list[Finding], verification: VerificationResult) -> None:
        self.findings = findings
        self.verification = verification
        pages = sorted({finding.page_index + 1 for finding in findings})
        categories = sorted({finding.category for finding in findings})
        super().__init__(
            "Финальная проверка обнаружила дополнительные области: "
            f"{len(findings)}; страницы: {', '.join(map(str, pages))}; "
            f"категории: {', '.join(categories)}"
        )


def _map_residual_findings(
    analysis: AnalysisResult,
    verification: VerificationResult,
) -> list[Finding]:
    mapped: list[Finding] = []
    for residual in verification.residual_findings:
        if not (0 <= residual.page_index < len(analysis.document.pages)):
            continue
        if residual.page_width <= 0 or residual.page_height <= 0:
            continue
        page = analysis.document.pages[residual.page_index]
        scale_x = page.width / residual.page_width
        scale_y = page.height / residual.page_height
        candidate_box = residual.box.normalized()
        box = Box(
            candidate_box.x0 * scale_x,
            candidate_box.y0 * scale_y,
            candidate_box.x1 * scale_x,
            candidate_box.y1 * scale_y,
        ).expand(RESIDUAL_REVIEW_MARGIN, page.width, page.height).normalized()
        material = (
            f"verification:{residual.page_index}:{residual.category}:"
            f"{box.x0:.2f}:{box.y0:.2f}:{box.x1:.2f}:{box.y1:.2f}"
        )
        mapped.append(
            Finding(
                finding_id=hashlib.sha256(material.encode("utf-8")).hexdigest()[:16],
                page_index=residual.page_index,
                category=residual.category,
                box=box,
                action=Action.REDACT,
                severity=Severity.CRITICAL,
                confidence=residual.confidence,
                source="verification",
                reason="Остаточная область найдена при проверке создаваемой копии",
                selected=True,
            )
        )
    return mapped


def create_verified_copy(
    analysis: AnalysisResult,
    output_path: str | Path,
    *,
    confirmed_pages: set[int],
    ocr: TesseractOcr | None = None,
) -> tuple[Path, Path, VerificationResult]:
    output = Path(output_path).expanduser().resolve()
    expected_pages = set(range(len(analysis.document.pages)))
    if confirmed_pages != expected_pages:
        missing = sorted(expected_pages - confirmed_pages)
        display = ", ".join(str(index + 1) for index in missing)
        raise ReleaseBlockedError(f"Не подтверждены страницы: {display}")
    if not analysis.capabilities.complete:
        raise ReleaseBlockedError("Нельзя выпускать копию: " + "; ".join(analysis.capabilities.blocking_issues))
    if output.exists():
        raise ReleaseBlockedError("Выходной файл уже существует; выберите новое имя")
    if output.suffix.lower() != ".pdf":
        raise ReleaseBlockedError("Очищенная копия должна иметь расширение .pdf")
    selected = [finding for finding in analysis.findings if finding.selected]
    with tempfile.TemporaryDirectory(prefix="mika-deid-release-", dir=output.parent) as temp_directory:
        temp_path = Path(temp_directory) / "candidate.pdf"
        try:
            build_raster_pdf(analysis.document, selected, temp_path)
        except SanitizationError as exc:
            raise ReleaseBlockedError(str(exc)) from exc
        verification = verify_raster_pdf(temp_path, expected_page_count=len(analysis.document.pages), ocr=ocr)
        if not verification.passed:
            residual_issue_prefix = "После очистки повторно обнаружены прямые идентификаторы:"
            non_residual_issues = [
                issue for issue in verification.issues if not issue.startswith(residual_issue_prefix)
            ]
            if verification.residual_findings and not non_residual_issues:
                mapped = _map_residual_findings(analysis, verification)
                if mapped:
                    raise ResidualFindingsForReview(mapped, verification)
            raise ReleaseBlockedError("Итоговая проверка не пройдена: " + "; ".join(verification.issues))
        os.replace(temp_path, output)

    if analysis.document.source_sha256 != _source_hash(analysis):
        output.unlink(missing_ok=True)
        raise ReleaseBlockedError("Исходный файл изменился во время обработки; результат удалён")

    report_path = output.with_suffix(".verification.json")
    if report_path.exists():
        output.unlink(missing_ok=True)
        raise ReleaseBlockedError("Файл отчёта уже существует; выберите другое имя результата")
    categories = Counter(finding.category for finding in selected)
    verification_added = [finding for finding in selected if finding.source == "verification"]
    verification_categories = Counter(finding.category for finding in verification_added)
    report = {
        "schema": "mika-deid-release-v1",
        "application": "MIKA De-ID Local",
        "version": __version__,
        "status": "LOCAL_SYNTHETIC_ONLY",
        "mode": "raster_only",
        "page_count": len(analysis.document.pages),
        "redaction_count": len(selected),
        "category_counts": dict(sorted(categories.items())),
        "verification_added_redaction_count": len(verification_added),
        "verification_added_category_counts": dict(sorted(verification_categories.items())),
        "original_preserved": True,
        "human_pages_confirmed": len(confirmed_pages),
        "verification": verification.as_dict(),
        "limitations": [
            "Автоматическое обнаружение не гарантирует нахождение всех идентификаторов.",
            "Копия предназначена для навигации и не заменяет неизменённый оригинал.",
            "Медицинское содержание остаётся чувствительной информацией.",
        ],
    }
    try:
        report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    except Exception:
        output.unlink(missing_ok=True)
        report_path.unlink(missing_ok=True)
        raise
    return output, report_path, verification


def _source_hash(analysis: AnalysisResult) -> str:
    digest = hashlib.sha256()
    with analysis.document.source_path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()
