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

from __future__ import annotations

from dataclasses import dataclass, field
from enum import StrEnum
from pathlib import Path
from typing import Any

from PIL import Image


class Action(StrEnum):
    REDACT = "redact"
    REVIEW = "review"


class Severity(StrEnum):
    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"


@dataclass(frozen=True, slots=True)
class Box:
    """A rectangular region in rendered page pixel coordinates."""

    x0: float
    y0: float
    x1: float
    y1: float

    def normalized(self) -> "Box":
        return Box(
            min(self.x0, self.x1),
            min(self.y0, self.y1),
            max(self.x0, self.x1),
            max(self.y0, self.y1),
        )

    @property
    def width(self) -> float:
        box = self.normalized()
        return max(0.0, box.x1 - box.x0)

    @property
    def height(self) -> float:
        box = self.normalized()
        return max(0.0, box.y1 - box.y0)

    @property
    def area(self) -> float:
        return self.width * self.height

    def expand(self, margin: float, width: int, height: int) -> "Box":
        box = self.normalized()
        return Box(
            max(0.0, box.x0 - margin),
            max(0.0, box.y0 - margin),
            min(float(width), box.x1 + margin),
            min(float(height), box.y1 + margin),
        )

    def union(self, other: "Box") -> "Box":
        a = self.normalized()
        b = other.normalized()
        return Box(min(a.x0, b.x0), min(a.y0, b.y0), max(a.x1, b.x1), max(a.y1, b.y1))

    def intersection_area(self, other: "Box") -> float:
        a = self.normalized()
        b = other.normalized()
        x0 = max(a.x0, b.x0)
        y0 = max(a.y0, b.y0)
        x1 = min(a.x1, b.x1)
        y1 = min(a.y1, b.y1)
        if x1 <= x0 or y1 <= y0:
            return 0.0
        return (x1 - x0) * (y1 - y0)

    def overlap_ratio(self, other: "Box") -> float:
        smaller = min(self.area, other.area)
        if smaller <= 0:
            return 0.0
        return self.intersection_area(other) / smaller

    def contains_point(self, x: float, y: float) -> bool:
        box = self.normalized()
        return box.x0 <= x <= box.x1 and box.y0 <= y <= box.y1

    def as_dict(self) -> dict[str, float]:
        box = self.normalized()
        return {
            "x0": round(box.x0, 2),
            "y0": round(box.y0, 2),
            "x1": round(box.x1, 2),
            "y1": round(box.y1, 2),
        }


@dataclass(frozen=True, slots=True)
class WordToken:
    text: str
    box: Box
    line_key: tuple[int, ...]
    confidence: float = 100.0
    source: str = "native"


@dataclass(slots=True)
class Finding:
    finding_id: str
    page_index: int
    category: str
    box: Box
    action: Action
    severity: Severity
    confidence: float
    source: str
    reason: str
    selected: bool = True
    manually_added: bool = False

    @property
    def requires_review(self) -> bool:
        return self.action == Action.REVIEW

    def public_dict(self) -> dict[str, Any]:
        """Return a log-safe representation that never includes detected source text."""

        return {
            "id": self.finding_id,
            "page": self.page_index + 1,
            "category": self.category,
            "box": self.box.as_dict(),
            "action": self.action.value,
            "severity": self.severity.value,
            "confidence": round(self.confidence, 1),
            "source": self.source,
            "reason": self.reason,
            "selected": self.selected,
            "manually_added": self.manually_added,
        }


@dataclass(slots=True)
class PageData:
    index: int
    image: Image.Image
    width_points: float
    height_points: float
    native_tokens: list[WordToken] = field(default_factory=list)
    ocr_tokens: list[WordToken] = field(default_factory=list)
    ocr_mean_confidence: float | None = None

    @property
    def width(self) -> int:
        return self.image.width

    @property
    def height(self) -> int:
        return self.image.height


@dataclass(slots=True)
class DocumentData:
    source_path: Path
    source_type: str
    source_size: int
    source_sha256: str
    pages: list[PageData]
    warnings: list[str] = field(default_factory=list)


@dataclass(slots=True)
class CapabilityStatus:
    ocr_available: bool
    ocr_languages: tuple[str, ...]
    barcode_available: bool
    heif_available: bool
    blocking_issues: list[str] = field(default_factory=list)

    @property
    def complete(self) -> bool:
        return not self.blocking_issues

    def as_dict(self) -> dict[str, Any]:
        return {
            "ocr_available": self.ocr_available,
            "ocr_languages": list(self.ocr_languages),
            "barcode_available": self.barcode_available,
            "heif_available": self.heif_available,
            "complete": self.complete,
            "blocking_issues": list(self.blocking_issues),
        }


@dataclass(slots=True)
class AnalysisResult:
    document: DocumentData
    findings: list[Finding]
    capabilities: CapabilityStatus
    warnings: list[str] = field(default_factory=list)
    pages_requiring_manual_review: set[int] = field(default_factory=set)

    def findings_for_page(self, page_index: int) -> list[Finding]:
        return [finding for finding in self.findings if finding.page_index == page_index]

    def public_dict(self) -> dict[str, Any]:
        category_counts: dict[str, int] = {}
        for finding in self.findings:
            category_counts[finding.category] = category_counts.get(finding.category, 0) + 1
        return {
            "schema": "mika-deid-analysis-v1",
            "source_type": self.document.source_type,
            "source_size": self.document.source_size,
            "page_count": len(self.document.pages),
            "finding_count": len(self.findings),
            "category_counts": category_counts,
            "findings": [finding.public_dict() for finding in self.findings],
            "capabilities": self.capabilities.as_dict(),
            "warnings": list(dict.fromkeys(self.document.warnings + self.warnings)),
            "pages_requiring_manual_review": [index + 1 for index in sorted(self.pages_requiring_manual_review)],
        }


@dataclass(frozen=True, slots=True)
class ResidualFinding:
    """Log-safe residual region reported from a rendered candidate PDF."""

    page_index: int
    category: str
    box: Box
    page_width: int
    page_height: int
    confidence: float

    def public_dict(self) -> dict[str, Any]:
        width = max(1, self.page_width)
        height = max(1, self.page_height)
        box = self.box.normalized()
        return {
            "page": self.page_index + 1,
            "category": self.category,
            "normalized_box": {
                "x0": round(box.x0 / width, 6),
                "y0": round(box.y0 / height, 6),
                "x1": round(box.x1 / width, 6),
                "y1": round(box.y1 / height, 6),
            },
            "confidence": round(self.confidence, 1),
        }


@dataclass(slots=True)
class VerificationResult:
    passed: bool
    issues: list[str]
    warnings: list[str]
    page_count: int
    annotation_count: int
    embedded_file_count: int
    extracted_text_characters: int
    residual_finding_count: int
    output_sha256: str
    residual_findings: list[ResidualFinding] = field(default_factory=list)

    def as_dict(self) -> dict[str, Any]:
        return {
            "schema": "mika-deid-verification-v1",
            "passed": self.passed,
            "issues": list(self.issues),
            "warnings": list(self.warnings),
            "page_count": self.page_count,
            "annotation_count": self.annotation_count,
            "embedded_file_count": self.embedded_file_count,
            "extracted_text_characters": self.extracted_text_characters,
            "residual_finding_count": self.residual_finding_count,
            "residual_regions": [finding.public_dict() for finding in self.residual_findings],
            "output_sha256": self.output_sha256,
        }
