"""逐次書き出しレビュー画面（要件定義書4.5・4.7、CLAUDE.md「絶対に譲れない設計
原則」3の具体的なUI実装）。

書き出し対象ファイル（又は枝番ファイル結合グループ）を1件ずつ順に処理する。各回、
画面には「今どのファイルを処理しているか」（進捗表示）・対象ファイルの
プレビュー（証拠番号を既に記載済みでないかを目視確認できるように）・
適用する処理（ファイル名整形／メタデータ除去／証拠番号スタンプ）を選択する
チェックボックス（ファイルの状態に応じた既定値つき、手動で変更可）を表示する。

証拠番号スタンプのチェックボックスがON/OFFされた際は、プレビューを即座に
再生成し、ON時は実際にラベルを焼き込んだ見た目、OFF時は未適用の見た目に
切り替える（原本に証拠番号が既に記載されている場合に重複スタンプを避ける
判断を助けるため）。

証拠番号スタンプのチェックがONの間は、スタンプ位置調整（要件定義書4.7
「スタンプ位置調整」）の上下左右ボタンを表示する。既定位置（config.yamlの
margin_top_mm／margin_right_mm）からのオフセットをこの画面上でのみ調整
でき、調整結果はこのファイル（又は結合グループ）にのみ適用される
（config.yamlの既定値自体は変更しない）。
"""

from __future__ import annotations

import dataclasses
import tkinter as tk
from tkinter import ttk

from PIL import ImageTk

from mints_prep.config import EvidenceStampConfig, FilenameConfig
from mints_prep.models import FileRecord
from mints_prep.processors.output_writer import PlannedCopy
from mints_prep.ui.preview import render_preview, render_preview_with_stamp
from mints_prep.ui.review_logic import ReviewItem, apply_review_choices, build_review_options

# スタンプ位置調整ボタン1回分の移動量（mm）。一次資料に定めはなく、UI操作上の
# 実装値（CLAUDE.md「数値パラメータの設定ファイル化」原則が対象とする
# mints側の制約値〔200MB・100文字等〕とは性質が異なるため、設定ファイル化は
# せずここに定数として置く）。
_POSITION_STEP_MM = 2.0


def _describe_item(item: ReviewItem) -> str:
    prelim = item.preliminary
    if prelim.merge_group is not None:
        names = "、".join(m.record.path.name for m in prelim.merge_group.members)
        return f"{names}\n→ {prelim.output_filename}（枝番ファイル結合）"
    return prelim.record.path.name


def _preview_placeholder_text(record: FileRecord) -> str:
    if record.pdf_finding is not None and record.pdf_finding.has_password:
        return "プレビュー非対応\n（パスワード付きファイル）"
    if record.detected_format not in ("PDF", "JPEG", "PNG"):
        return "プレビュー非対応\n（音声・動画ファイル）"
    return "プレビューを生成できませんでした"


def _stamp_label_for_preview(item: ReviewItem) -> str | None:
    """プレビューに焼き込むべき証拠番号ラベルを決める。

    結合グループはプレビュー対象（preview_record）が代表メンバーの実ファイル
    であるため、そのメンバー自身の枝番ラベル（merge_branch_groupが実際に
    焼き込むものと同じ）を用いる。単独ファイルはstamp_labelをそのまま使う。
    """
    prelim = item.preliminary
    if prelim.merge_group is not None:
        return prelim.merge_group.representative.match.evidence_number_label
    return prelim.stamp_label


class WriteReviewDialog(tk.Toplevel):
    def __init__(
        self,
        parent: tk.Misc,
        item: ReviewItem,
        index: int,
        total: int,
        filename_config: FilenameConfig,
        evidence_stamp_config: EvidenceStampConfig,
    ) -> None:
        super().__init__(parent)
        self.title("書き出し内容の確認")
        self.geometry("950x650")
        self.minsize(800, 500)
        self.transient(parent)
        self.grab_set()

        self.result: PlannedCopy | None = None
        self._item = item
        self._filename_config = filename_config
        self._evidence_stamp_config = evidence_stamp_config
        self._stamp_label = _stamp_label_for_preview(item)
        self._choice_vars: dict[str, tk.BooleanVar] = {}
        self._preview_label: ttk.Label | None = None
        self._preview_photo: ImageTk.PhotoImage | None = None  # GC対策で参照を保持
        # 要件定義書4.7「スタンプ位置調整」：このファイル（又は結合グループ）
        # のみに適用する余白。config.yamlの既定値から開始し、上下左右ボタンで
        # このダイアログ限りで調整できる（config.yamlの既定値自体は変更しない）。
        self._stamp_margin_top_mm = evidence_stamp_config.margin_top_mm
        self._stamp_margin_right_mm = evidence_stamp_config.margin_right_mm
        self._position_label: ttk.Label | None = None

        ttk.Label(
            self,
            text=f"処理中：{index} / {total} 件目",
            padding=(12, 12, 12, 0),
            font=("", 11, "bold"),
        ).pack(side=tk.TOP, fill=tk.X)
        ttk.Label(
            self,
            text=_describe_item(item),
            padding=(12, 2, 12, 10),
            wraplength=920,
            justify="left",
        ).pack(side=tk.TOP, fill=tk.X)

        body = ttk.Frame(self)
        body.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=12)

        left = ttk.Frame(body)
        left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        right = ttk.Frame(body, width=440)
        right.pack(side=tk.RIGHT, fill=tk.Y, padx=(12, 0))
        right.pack_propagate(False)

        self._build_preview(right)
        self._build_options(left)
        self._refresh_preview()

        button_frame = ttk.Frame(self, padding=12)
        button_frame.pack(side=tk.BOTTOM, fill=tk.X)
        ttk.Button(
            button_frame, text="書き出しを中止（残りは処理しません）", command=self._on_abort
        ).pack(side=tk.RIGHT, padx=(6, 0))
        ttk.Button(button_frame, text="この内容で書き出す", command=self._on_confirm).pack(
            side=tk.RIGHT
        )

        self.protocol("WM_DELETE_WINDOW", self._on_abort)

    def _build_options(self, parent: tk.Misc) -> None:
        options = build_review_options(self._item, self._filename_config)
        if not options:
            ttk.Label(
                parent,
                text="検出された問題はありません。このまま書き出します。",
                wraplength=460,
                justify="left",
            ).pack(side=tk.TOP, anchor="w", pady=8)
            return
        for opt in options:
            var = tk.BooleanVar(value=opt.default_checked)
            self._choice_vars[opt.key] = var
            if opt.key == "stamp":
                # 証拠番号スタンプの選択はプレビューの見た目に直結するため、
                # チェックボックスのON/OFFに連動してプレビューを再生成する
                # （原本に証拠番号が既に記載されていないかを判断する材料）。
                var.trace_add("write", lambda *_args: self._refresh_preview())
            # ttk.Checkbuttonはwraplengthオプションを持たないため、長い説明文を
            # 折り返し表示できる標準tk.Checkbuttonを使う。
            tk.Checkbutton(
                parent,
                variable=var,
                text=opt.label,
                wraplength=440,
                justify="left",
                anchor="w",
            ).pack(side=tk.TOP, anchor="w", pady=6, fill=tk.X)

        if "stamp" in self._choice_vars:
            self._build_position_controls(parent)

    def _build_position_controls(self, parent: tk.Misc) -> None:
        """スタンプ位置調整UI（要件定義書4.7）。上下左右ボタンでこのファイル
        （又は結合グループ）限りの余白を調整し、プレビューへ即座に反映する。
        """
        frame = ttk.LabelFrame(parent, text="スタンプ位置調整（右上からの余白）")
        frame.pack(side=tk.TOP, anchor="w", pady=(10, 6), fill=tk.X)

        self._position_label = ttk.Label(frame, text=self._position_label_text())
        self._position_label.pack(side=tk.TOP, anchor="w", padx=8, pady=(6, 4))

        grid = ttk.Frame(frame)
        grid.pack(side=tk.TOP, padx=8, pady=(0, 8))
        ttk.Button(
            grid, text="↑", width=3, command=lambda: self._adjust_stamp_position(dy=-1)
        ).grid(row=0, column=1)
        ttk.Button(
            grid, text="←", width=3, command=lambda: self._adjust_stamp_position(dx=-1)
        ).grid(row=1, column=0)
        ttk.Button(grid, text="リセット", command=self._reset_stamp_position).grid(
            row=1, column=1, padx=4
        )
        ttk.Button(
            grid, text="→", width=3, command=lambda: self._adjust_stamp_position(dx=1)
        ).grid(row=1, column=2)
        ttk.Button(
            grid, text="↓", width=3, command=lambda: self._adjust_stamp_position(dy=1)
        ).grid(row=2, column=1)

    def _position_label_text(self) -> str:
        return (
            f"現在位置：上端から{self._stamp_margin_top_mm:.0f}mm、"
            f"右端から{self._stamp_margin_right_mm:.0f}mm"
        )

    def _adjust_stamp_position(self, dx: int = 0, dy: int = 0) -> None:
        # dx=+1は右方向へ移動（margin_right_mmを減らす）、dy=+1は下方向へ移動
        # （margin_top_mmを増やす）。ページ外（負の余白）にはみ出さないよう
        # 0mm未満にはならないようクランプする。
        self._stamp_margin_right_mm = max(0.0, self._stamp_margin_right_mm - dx * _POSITION_STEP_MM)
        self._stamp_margin_top_mm = max(0.0, self._stamp_margin_top_mm + dy * _POSITION_STEP_MM)
        if self._position_label is not None:
            self._position_label.configure(text=self._position_label_text())
        self._refresh_preview()

    def _reset_stamp_position(self) -> None:
        self._stamp_margin_top_mm = self._evidence_stamp_config.margin_top_mm
        self._stamp_margin_right_mm = self._evidence_stamp_config.margin_right_mm
        if self._position_label is not None:
            self._position_label.configure(text=self._position_label_text())
        self._refresh_preview()

    def _effective_stamp_config(self) -> EvidenceStampConfig:
        return dataclasses.replace(
            self._evidence_stamp_config,
            margin_top_mm=self._stamp_margin_top_mm,
            margin_right_mm=self._stamp_margin_right_mm,
        )

    def _build_preview(self, parent: tk.Misc) -> None:
        self._preview_label = ttk.Label(parent, anchor="center", justify="center")
        self._preview_label.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

    def _refresh_preview(self) -> None:
        stamp_checked = self._choice_vars["stamp"].get() if "stamp" in self._choice_vars else False

        if stamp_checked and self._stamp_label is not None:
            image = render_preview_with_stamp(
                self._item.preview_record, self._stamp_label, self._effective_stamp_config()
            )
        else:
            image = render_preview(self._item.preview_record)

        if image is not None:
            self._preview_photo = ImageTk.PhotoImage(image)
            self._preview_label.configure(image=self._preview_photo, text="")
        else:
            self._preview_photo = None
            self._preview_label.configure(
                image="", text=_preview_placeholder_text(self._item.preview_record)
            )

    def _on_confirm(self) -> None:
        choices = {key: var.get() for key, var in self._choice_vars.items()}
        stamp_offset_mm = (self._stamp_margin_top_mm, self._stamp_margin_right_mm)
        self.result = apply_review_choices(
            self._item, choices, self._filename_config, stamp_offset_mm=stamp_offset_mm
        )
        self.destroy()

    def _on_abort(self) -> None:
        self.result = None
        self.destroy()


def review_and_confirm(
    parent: tk.Misc,
    item: ReviewItem,
    index: int,
    total: int,
    filename_config: FilenameConfig,
    evidence_stamp_config: EvidenceStampConfig,
) -> PlannedCopy | None:
    """逐次レビュー画面を表示し、確定したPlannedCopyを返す。

    「書き出しを中止」を選んだ場合（ウィンドウを閉じる操作も同様）はNoneを
    返す。呼び出し側はこれを「以降の処理を打ち切る」合図として扱うこと。
    """
    dialog = WriteReviewDialog(parent, item, index, total, filename_config, evidence_stamp_config)
    parent.wait_window(dialog)
    return dialog.result
