"""Secure local document quarantine, extraction, candidate and review contracts."""
from __future__ import annotations

import hashlib
import json
import os
import re
import secrets
import shutil
import stat
import subprocess
import sys
import tempfile
import unicodedata
from dataclasses import asdict, dataclass
from datetime import date, datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
try:
    from .media_validation import (
        VIDEO_MAX_BYTES, create_safe_derivatives, detect_container, validate_media_file,
    )
except ImportError:  # direct importlib fixture execution
    sys.path.insert(0, str(Path(__file__).resolve().parent))
    from media_validation import (
        VIDEO_MAX_BYTES, create_safe_derivatives, detect_container, validate_media_file,
    )

MAX_DOCUMENT_BYTES = 20 * 1024 * 1024
MAX_DOCUMENT_UPLOAD_BYTES = VIDEO_MAX_BYTES
MAX_PDF_PAGES = 200
MAX_CONTEXT = 320
MAX_EXTRACTED_TEXT_BYTES = 8 * 1024 * 1024
MAX_PAGE_TEXT_BYTES = 512 * 1024
DOCUMENT_TYPES = frozenset({
    "doctor_report", "laboratory_report", "radiology", "medication_prescription",
    "vaccination", "insurance", "patient_information", "photo", "other",
})
MIME_MAGIC = (
    (b"%PDF-", "application/pdf", ".pdf"),
    (b"\xff\xd8\xff", "image/jpeg", ".jpg"),
    (b"\x89PNG\r\n\x1a\n", "image/png", ".png"),
)
HEIF_BRANDS = {b"heic", b"heix", b"hevc", b"hevx", b"mif1", b"msf1"}
TZ = ZoneInfo("Europe/Zurich")
TOKEN_RE = re.compile(r"docq_[a-f0-9]{32}")
ID_RE = re.compile(r"doc_[a-f0-9]{24}")
CANDIDATE_ID_RE = re.compile(r"cand_[a-f0-9]{24}")


@dataclass(frozen=True)
class ValidatedUpload:
    mime: str
    suffix: str
    sha256: str
    size: int
    page_count: int | None
    media_info: dict[str, Any] | None = None


def _safe_text(value: Any, maximum: int, *, required: bool = False) -> str:
    if not isinstance(value, str):
        raise ValueError("invalid metadata")
    clean = " ".join(value.split())
    if (required and not clean) or len(clean) > maximum or re.search(r"[\x00-\x1f\x7f]|https?://|\\\\|\.hermes", clean, re.I):
        raise ValueError("invalid metadata")
    return clean


def validate_metadata(raw: Any) -> dict[str, Any]:
    if not isinstance(raw, dict) or set(raw) != {
        "document_date", "document_type", "institution", "personal_title",
        "investigation_day", "note",
    }:
        raise ValueError("invalid metadata shape")
    document_type = str(raw["document_type"] or "")
    if document_type not in DOCUMENT_TYPES:
        raise ValueError("invalid document type")
    def optional_day(value: Any) -> str:
        if value in {None, ""}: return ""
        if not isinstance(value, str): raise ValueError("invalid date")
        try: parsed = date.fromisoformat(value)
        except ValueError as error: raise ValueError("invalid date") from error
        if parsed.year < 1900 or parsed > datetime.now(TZ).date(): raise ValueError("invalid date")
        return parsed.isoformat()
    return {
        "document_date": optional_day(raw["document_date"]),
        "document_type": document_type,
        "institution": _safe_text(raw["institution"], 120),
        "personal_title": _safe_text(raw["personal_title"], 120),
        "investigation_day": optional_day(raw["investigation_day"]),
        "note": _safe_text(raw["note"], 500),
    }


def detect_magic(data: bytes) -> tuple[str, str]:
    if data.startswith(b"%PDF-"):
        return "application/pdf", ".pdf"
    try:
        return detect_container(data[:256])
    except ValueError as exc:
        raise ValueError("unsupported_file_type") from exc


def _external_tool_env() -> dict[str, str]:
    """Keep Python package injection from corrupting system media/OCR tools."""
    environment = dict(os.environ)
    environment.pop("PYTHONPATH", None)
    environment.pop("PYTHONHOME", None)
    environment["LC_ALL"] = "C.UTF-8"
    return environment


def _pdf_pages(path: Path, descriptor: int | None = None) -> int:
    source = f"/proc/self/fd/{descriptor}" if descriptor is not None else str(path)
    completed = subprocess.run(
        ["pdfinfo", source], check=False, capture_output=True, text=True, timeout=20,
        env=_external_tool_env(),
        pass_fds=(descriptor,) if descriptor is not None else (),
    )
    if completed.returncode != 0:
        raise ValueError("invalid_pdf_structure")
    match = re.search(r"^Pages:\s+(\d+)\s*$", completed.stdout, re.M)
    if not match:
        raise ValueError("invalid_pdf_structure")
    pages = int(match.group(1))
    if not 1 <= pages <= MAX_PDF_PAGES:
        raise ValueError("pdf_page_limit")
    return pages


def validate_upload_file(path: Path, *, max_bytes: int = MAX_DOCUMENT_UPLOAD_BYTES) -> ValidatedUpload:
    descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | getattr(os, "O_NONBLOCK", 0))
    try:
        meta = os.fstat(descriptor)
        if not stat.S_ISREG(meta.st_mode) or meta.st_uid != os.getuid() or meta.st_size < 4 or meta.st_size > max_bytes:
            raise ValueError("invalid_file_size")
        digest = hashlib.sha256(); first = b""; read = 0
        while chunk := os.read(descriptor, min(64 * 1024, max_bytes + 1 - read)):
            if not first: first = chunk[:256]
            read += len(chunk)
            if read > max_bytes: raise ValueError("invalid_file_size")
            digest.update(chunk)
        if read != meta.st_size: raise ValueError("file_changed")
        mime, suffix = detect_magic(first)
        checksum = digest.hexdigest()
        if mime == "application/pdf":
            if read > MAX_DOCUMENT_BYTES:
                raise ValueError("invalid_file_size")
            return ValidatedUpload(mime, suffix, checksum, read, _pdf_pages(path, descriptor), None)
        info = validate_media_file(path)
        if info.sha256 != checksum or info.byte_size != read or info.mime_type != mime:
            raise ValueError("file_changed")
        return ValidatedUpload(mime, suffix, checksum, read, None, asdict(info))
    finally:
        os.close(descriptor)


def quarantine_upload(data: bytes, metadata: Any, quarantine: Path) -> dict[str, Any]:
    clean_metadata = validate_metadata(metadata)
    if not isinstance(data, bytes) or not 4 <= len(data) <= MAX_DOCUMENT_UPLOAD_BYTES:
        raise ValueError("invalid_file_size")
    if quarantine.exists() and quarantine.is_symlink():
        raise OSError("quarantine_must_not_be_symlink")
    quarantine.mkdir(parents=True, mode=0o700, exist_ok=True)
    os.chmod(quarantine, 0o700)
    directory_meta = quarantine.lstat()
    if not stat.S_ISDIR(directory_meta.st_mode) or directory_meta.st_uid != os.getuid():
        raise OSError("invalid_quarantine")
    token = "docq_" + secrets.token_hex(16)
    temporary = quarantine / f".{token}.tmp"
    raw_path = quarantine / f"{token}.bin"
    descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
    try:
        with os.fdopen(descriptor, "wb") as handle:
            handle.write(data); handle.flush(); os.fsync(handle.fileno())
        validated = validate_upload_file(temporary)
        os.replace(temporary, raw_path)
        record = {"version":1,"token":token,"mime":validated.mime,"suffix":validated.suffix,
                  "sha256":validated.sha256,"size":validated.size,"page_count":validated.page_count,
                  "media_info":validated.media_info,
                  "metadata":clean_metadata,"created_at":datetime.now(TZ).isoformat(timespec="seconds")}
        meta_path = quarantine / f"{token}.json"
        packed = json.dumps(record, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode()
        meta_tmp = quarantine / f".{token}.json.tmp"
        fd = os.open(meta_tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
        with os.fdopen(fd, "wb") as handle:
            handle.write(packed); handle.flush(); os.fsync(handle.fileno())
        os.replace(meta_tmp, meta_path)
        dirfd = os.open(quarantine, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
        try: os.fsync(dirfd)
        finally: os.close(dirfd)
        return {"token":token,"sha256":validated.sha256,"mime":validated.mime,"size":validated.size}
    except Exception:
        temporary.unlink(missing_ok=True); raw_path.unlink(missing_ok=True)
        (quarantine / f"{token}.json").unlink(missing_ok=True)
        (quarantine / f".{token}.json.tmp").unlink(missing_ok=True)
        raise


def load_quarantine(token: str, quarantine: Path) -> tuple[dict[str, Any], Path]:
    if not TOKEN_RE.fullmatch(token): raise ValueError("invalid_quarantine_token")
    meta_path, raw_path = quarantine / f"{token}.json", quarantine / f"{token}.bin"
    fd = os.open(meta_path, os.O_RDONLY | os.O_NOFOLLOW)
    try:
        st = os.fstat(fd)
        if not stat.S_ISREG(st.st_mode) or st.st_uid != os.getuid() or stat.S_IMODE(st.st_mode) != 0o600 or st.st_size > 16384:
            raise ValueError("invalid_quarantine_metadata")
        raw = os.read(fd, 16385)
    finally: os.close(fd)
    record = json.loads(raw, object_pairs_hook=_reject_duplicates)
    validated = validate_upload_file(raw_path)
    if record.get("token") != token or record.get("sha256") != validated.sha256 or record.get("mime") != validated.mime or record.get("size") != validated.size or record.get("media_info") != validated.media_info:
        raise ValueError("quarantine_metadata_mismatch")
    record["metadata"] = validate_metadata(record.get("metadata"))
    return record, raw_path


def open_quarantine_source(record: dict[str, Any], raw_path: Path) -> int:
    descriptor = os.open(raw_path, os.O_RDONLY | os.O_NOFOLLOW)
    try:
        meta = os.fstat(descriptor)
        if not stat.S_ISREG(meta.st_mode) or meta.st_uid != os.getuid() or stat.S_IMODE(meta.st_mode) != 0o600 or meta.st_size != int(record["size"]):
            raise ValueError("quarantine_source_changed")
        digest = hashlib.sha256(); first = b""; read = 0
        while chunk := os.read(descriptor, 64 * 1024):
            if not first: first = chunk[:16]
            read += len(chunk); digest.update(chunk)
            if read > MAX_DOCUMENT_UPLOAD_BYTES: raise ValueError("invalid_file_size")
        mime, _ = detect_magic(first)
        if digest.hexdigest() != record["sha256"] or mime != record["mime"] or read != int(record["size"]):
            raise ValueError("quarantine_source_changed")
        os.lseek(descriptor, 0, os.SEEK_SET)
        return descriptor
    except Exception:
        os.close(descriptor)
        raise


def discard_quarantine(token: str, quarantine: Path) -> None:
    if not TOKEN_RE.fullmatch(token):
        return
    for suffix in (".bin", ".json"):
        try:
            (quarantine / f"{token}{suffix}").unlink(missing_ok=True)
        except OSError:
            pass


def _reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
    out: dict[str, Any] = {}
    for key, value in pairs:
        if key in out: raise ValueError("duplicate_json_key")
        out[key] = value
    return out


def normalize_search_text(text: str) -> str:
    clean = unicodedata.normalize("NFKC", text).replace("\x00", " ")
    return "\n".join(" ".join(line.split()) for line in clean.splitlines()).strip()


def _tool_version(command: str) -> str:
    try:
        version_flag = "-v" if command == "pdftotext" else "--version"
        result = subprocess.run(
            [command, version_flag], capture_output=True, text=True, timeout=10,
            env=_external_tool_env(),
        )
        return (result.stdout or result.stderr).splitlines()[0][:80]
    except Exception:
        return "unknown"


def _pdftotext_page(path: Path, page: int, workspace: Path, prefix: str) -> str:
    output = workspace / f"{prefix}-{page}.txt"
    result = subprocess.run(
        ["pdftotext", "-f", str(page), "-l", str(page), "-layout", str(path), str(output)],
        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=30,
        env=_external_tool_env(),
    )
    if result.returncode != 0 or not output.is_file():
        return ""
    if output.stat().st_size > MAX_PAGE_TEXT_BYTES:
        raise RuntimeError("extracted_text_limit")
    return output.read_text(errors="replace")


def _bounded_pages(pages: list[str]) -> list[str]:
    if sum(len(page.encode("utf-8", "replace")) for page in pages) > MAX_EXTRACTED_TEXT_BYTES:
        raise RuntimeError("extracted_text_limit")
    return pages


def extract_pages(path: Path, mime: str, workspace: Path) -> tuple[str, str, list[str], list[float | None]]:
    if mime == "application/pdf":
        pages = _pdf_pages(path)
        text_pages: list[str] = []
        for page in range(1, pages + 1):
            text_pages.append(_pdftotext_page(path, page, workspace, "layer"))
        _bounded_pages(text_pages)
        useful = sum(len(normalize_search_text(value)) for value in text_pages)
        if useful >= max(20, pages * 12):
            return "text_layer", _tool_version("pdftotext"), text_pages, [None] * pages
        ocr_output = workspace / "ocr.pdf"
        result = subprocess.run(["ocrmypdf", "--skip-text", "--deskew", "--rotate-pages", "--jobs", "2", "--output-type", "pdf", str(path), str(ocr_output)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600, env=_external_tool_env())
        if result.returncode != 0 or not ocr_output.is_file():
            raise RuntimeError("ocr_failed")
        if ocr_output.stat().st_size > 128 * 1024 * 1024:
            raise RuntimeError("ocr_output_limit")
        ocr_pages = []
        for page in range(1, pages + 1):
            ocr_pages.append(_pdftotext_page(ocr_output, page, workspace, "ocr"))
        return "ocr", _tool_version("ocrmypdf"), _bounded_pages(ocr_pages), [None] * pages
    if mime.startswith("video/"):
        return "media_preview", _tool_version("ffmpeg"), [""], [None]
    info = validate_media_file(path)
    derivatives = create_safe_derivatives(path, workspace, "document_ocr_source", info)
    normalized = workspace / derivatives["preview_name"]
    output = workspace / "image-ocr"
    result = subprocess.run(["tesseract", str(normalized), str(output), "-l", "deu+eng", "--psm", "6"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=180, env=_external_tool_env())
    if result.returncode != 0 or not output.with_suffix(".txt").is_file():
        raise RuntimeError("ocr_failed")
    if output.with_suffix(".txt").stat().st_size > MAX_PAGE_TEXT_BYTES:
        raise RuntimeError("extracted_text_limit")
    version = f"{_tool_version('tesseract')} | {info.decoder_name} {info.decoder_version}"
    return "ocr", version[:240], _bounded_pages([output.with_suffix(".txt").read_text(errors="replace")]), [None]


def candidate_rows(pages: list[str], engine: str, metadata: dict[str, Any], *, page_numbers: list[int] | None = None, identity_scope: str = "unscoped", text_version: int = 1) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    def add(kind: str, value: str, unit: str | None, page: int, section: int, context: str, confidence: float) -> None:
        clean = " ".join(context.split())[:MAX_CONTEXT]
        fingerprint=hashlib.sha256(f"{kind}|{value}|{unit}|{page}|{section}|{clean}".encode()).hexdigest()
        identity = hashlib.sha256(f"{identity_scope}|{text_version}|{fingerprint}".encode()).hexdigest()[:24]
        rows.append({"id":"cand_"+identity,"candidate_fingerprint":fingerprint,"source_text_version":text_version,"candidate_type":kind,"value_text":value[:240],"unit":unit[:40] if unit else None,
                     "page_number":page,"section_number":section,"context_text":clean,"engine":engine,"confidence":confidence})
    if metadata.get("document_date"):
        add("document_date", metadata["document_date"], None, 1, 1, metadata["document_date"], 1.0)
    if metadata.get("institution"):
        add("institution", metadata["institution"], None, 1, 1, metadata["institution"], 1.0)
    lab = re.compile(r"(?P<name>[A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß0-9 .()/%+-]{1,60}?)\s+(?P<value>[<>≤≥]?\s*\d+(?:[.,]\d+)?)\s*(?P<unit>mg/l|mg/dl|g/l|mmol/l|µmol/l|U/l|G/l|T/l|%|ng/ml|pg/ml)\b", re.I)
    ref = re.compile(r"(?:Referenz|Norm(?:bereich)?)\s*[: ]\s*([<>≤≥]?\s*\d+(?:[.,]\d+)?\s*(?:[-–]\s*\d+(?:[.,]\d+)?)?)\s*([A-Za-zµ/%]+)?", re.I)
    medication = re.compile(r"(?:Medikament|Präparat|Rx)\s*[:\-]\s*([^\n]{2,100})", re.I)
    headings = (("diagnosis", re.compile(r"(?:Diagnose|Vorerkrankung)\s*[:\-]\s*([^\n]{2,180})",re.I)),
                ("symptom", re.compile(r"(?:Symptom|Beschwerde|Anamnese)\s*[:\-]\s*([^\n]{2,180})",re.I)),
                ("appointment", re.compile(r"(?:Termin|Untersuchung)\s*[:\-]\s*([^\n]{2,180})",re.I)),
                ("vaccination", re.compile(r"(?:Impfung|Impfstoff)\s*[:\-]\s*([^\n]{2,180})",re.I)),
                ("important_event", re.compile(r"(?:Ereignis|Verlauf)\s*[:\-]\s*([^\n]{2,180})",re.I)))
    numbers = page_numbers if page_numbers is not None else list(range(1, len(pages) + 1))
    if len(numbers) != len(pages):
        raise ValueError("invalid_candidate_page_map")
    for page_no, text in zip(numbers, pages):
        for section, line in enumerate(text.splitlines(), 1):
            for match in lab.finditer(line): add("laboratory_value", f"{match.group('name').strip()}: {match.group('value').replace(' ','')}", match.group("unit"), page_no, section, line, .78)
            for match in ref.finditer(line): add("reference_range", match.group(1), match.group(2), page_no, section, line, .72)
            for match in medication.finditer(line): add("medication", match.group(1).strip(), None, page_no, section, line, .65)
            for kind, pattern in headings:
                for match in pattern.finditer(line): add(kind, match.group(1).strip(), None, page_no, section, line, .6)
    unique: dict[str, dict[str, Any]] = {row["id"]: row for row in rows}
    return list(unique.values())[:500]


def section_similarity(left: str, right: str) -> float:
    a, b = set(normalize_search_text(left).casefold().split()), set(normalize_search_text(right).casefold().split())
    return len(a & b) / len(a | b) if a and b else 0.0
