"""墨消し完全性の検証（要件定義書 4.6「墨消し完全性の検証」）。

根拠：準備の手引39頁「文書等にマスキングをする際の留意点について」
「PDF編集ソフトでマーカー機能を用いるのみでは文字情報が完全に削除されない
ことから、墨消し機能を用いるなどして完全に文字情報を削除したうえで、
削除できていることを提出前に確認する必要がある」。

ページ内の被覆オブジェクト（塗りつぶし矩形＝黒塗り、ハイライト注釈＝マーカー）
の座標範囲と、下層のテキスト文字の座標とを照合し、被覆下に文字情報（コピー・
検索で読み取れる文字）が残存していないかを判定する。

検証（検出・警告）のみを行う。除去（選択的除去・ラスタライズ等の防止処理の
実行）はチェック専念方針により廃止した（要件定義書4.6・第8章参照）。本モジュール
はファイルの内容を一切変更しない読み取り専用処理であり、CLAUDE.md「絶対に
譲れない設計原則」3の確認UI原則の対象外である。
"""

from __future__ import annotations

from pathlib import Path

import pdfplumber

from mints_prep.models import RedactionFinding

_SNIPPET_MAX_LENGTH = 30


def _center_within(char: dict, x0: float, top: float, x1: float, bottom: float) -> bool:
    cx = (char["x0"] + char["x1"]) / 2
    cy = (char["top"] + char["bottom"]) / 2
    return x0 <= cx <= x1 and top <= cy <= bottom


def _covering_boxes(page: "pdfplumber.page.Page") -> list[tuple[float, float, float, float, str]]:
    boxes: list[tuple[float, float, float, float, str]] = []

    for rect in page.rects:
        if rect.get("fill"):
            boxes.append((rect["x0"], rect["top"], rect["x1"], rect["bottom"], "塗りつぶし矩形（黒塗り等）"))

    for annot in page.annots:
        subtype = annot.get("data", {}).get("Subtype")
        name = getattr(subtype, "name", None)
        if name == "Highlight":
            boxes.append((annot["x0"], annot["top"], annot["x1"], annot["bottom"], "マーカー（ハイライト注釈）"))

    return boxes


def detect_redaction_residue(path: Path) -> RedactionFinding | None:
    """墨消し・マーカーの下に文字情報が残存していないかを検証する。

    検証不可（暗号化・破損等で解析に失敗）の場合は None を返す。
    """
    try:
        pages_with_residual: list[int] = []
        details: list[str] = []

        with pdfplumber.open(path) as pdf:
            for page_index, page in enumerate(pdf.pages, start=1):
                boxes = _covering_boxes(page)
                if not boxes:
                    continue

                page_flagged = False
                for x0, top, x1, bottom, kind in boxes:
                    # 単語間のスペースもスニペットの可読性のため保持する
                    # （残存有無の判定自体は空白のみの場合を除外するためstrip後に判定）。
                    covered_text = "".join(
                        ch["text"] for ch in page.chars if _center_within(ch, x0, top, x1, bottom)
                    )
                    covered_text = covered_text.strip()
                    if covered_text:
                        page_flagged = True
                        snippet = covered_text
                        if len(snippet) > _SNIPPET_MAX_LENGTH:
                            snippet = snippet[:_SNIPPET_MAX_LENGTH] + "…"
                        details.append(
                            f"{page_index}ページ目：{kind}の下に文字情報が残存している可能性"
                            f"（例：「{snippet}」）"
                        )

                if page_flagged:
                    pages_with_residual.append(page_index)

        return RedactionFinding(
            has_residual_text=bool(pages_with_residual),
            pages_with_residual_text=tuple(pages_with_residual),
            details=tuple(details),
        )
    except Exception:
        # パスワード付き・破損PDF等、解析自体に失敗した場合は検証不可として扱う
        # （パスワード検出自体はpdf_detector側で別途警告するため、ここでは
        # 検出不可を示すにとどめる）。
        return None
