"""証拠番号パターンの判定（要件定義書 4.3）。

既存のファイル名が、証拠番号の形式（「甲」「乙」「丙」＋数字、枝番付きを含む）に
合致するかを判定する。このツールはファイルの中身をプレビューできずファイル名の
適否を判断できないため、ファイル名の自動生成・編集機能は持たない（利用者が
あらかじめ適切なファイル名を付けている前提）。判定結果は、4.7（証拠番号処理：
右上スタンプ・枝番ファイル結合）の適用候補になるかどうかの判断材料としてのみ用いる。

根拠：
- 符号・半角3桁数字・被告複数時の添字（乙A等）：準備の手引29頁
  「ファイル名：証拠番号（半角三桁）に加えて...標目のとおり記載（英数字は半角）
  例）「甲001 売買契約書原本」、「乙A001 領収書」」（一次資料の原文で明確に半角と指定）。
- 枝番の表記揺れ（ハイフン／アンダースコア、範囲の「~」）：
  準備の手引29頁の入力例「甲001-1~20」とFAQの「甲001_1～3」が併存
  （docs/02_constraints-report.md 補遺4(4)）。ツールは両表記を受理する。

添字は一次資料上は半角が正式だが、全角で入力されたファイルを誤って
「証拠番号処理の対象外」と判定してしまわないよう、判定自体は全角も
寛容に受理する（ただし本ツールが自動生成する際は半角を用いる）。
"""

from __future__ import annotations

import re
from collections.abc import Iterable
from pathlib import Path

from mints_prep.config import EvidenceNumberConfig
from mints_prep.models import EvidenceNumberMatch, EvidenceSequenceFinding

# 半角英字が正式（準備の手引29頁「英数字は半角」）。全角も判定上は寛容に受理する。
_SUBLETTER_CHARS = "A-Za-zＡ-Ｚａ-ｚ"
# 枝番範囲の区切りは半角チルダ・全角チルダ・波ダッシュのいずれも許容する。
_RANGE_SEP = "[~〜]"


def _build_pattern(config: EvidenceNumberConfig) -> re.Pattern[str]:
    symbols = "|".join(re.escape(s) for s in config.symbols)
    return re.compile(
        rf"^(?P<symbol>{symbols})"
        rf"(?P<subletter>[{_SUBLETTER_CHARS}]?)"
        rf"(?P<number>\d{{{config.digits}}})"
        rf"(?:[-_](?P<branch_from>\d+)(?:{_RANGE_SEP}(?P<branch_to>\d+))?)?"
    )


def match_evidence_number(
    filename: str, config: EvidenceNumberConfig
) -> EvidenceNumberMatch | None:
    """ファイル名（拡張子込み・拡張子なしいずれも可）の先頭が証拠番号パターンに
    合致するか判定する。合致しない場合は None を返す。"""
    stem = Path(filename).stem
    pattern = _build_pattern(config)
    m = pattern.match(stem)
    if not m:
        return None

    remainder = stem[m.end():].strip()
    return EvidenceNumberMatch(
        symbol=m.group("symbol"),
        subletter=m.group("subletter") or "",
        number=m.group("number"),
        branch_from=m.group("branch_from"),
        branch_to=m.group("branch_to"),
        title=remainder,
    )


def check_evidence_sequence(
    filenames: Iterable[str], config: EvidenceNumberConfig
) -> tuple[EvidenceSequenceFinding, ...]:
    """フォルダ内の証拠ファイル群の証拠番号を符号ごとに集計し、欠番・重複を
    検出する（要件定義書4.7「号証の連番・欠番チェック」）。

    証拠番号パターンに合致しないファイル名は無視する。符号（甲/乙/丙、乙A等）
    ごとに独立した連番系列として扱い、枝番（甲001-1〜3等）は親番号単位で1つと
    数える。欠番は観測された最大番号までの範囲（1〜最大）のうち出現しない番号、
    重複は同一の符号・番号・枝番の組み合わせが複数ファイルに存在する場合を指す
    （通し番号の異なる枝番同士は正当な結合対象であり重複とはしない）。
    警告のみで、自動採番の押し付けはしない。問題が見つかった符号系列のみを
    返す（欠番・重複のいずれも無い符号系列は含まない）。
    """
    groups: dict[str, list[EvidenceNumberMatch]] = {}
    for filename in filenames:
        match = match_evidence_number(filename, config)
        if match is None:
            continue
        symbol_key = f"{match.symbol}{match.subletter}"
        groups.setdefault(symbol_key, []).append(match)

    findings: list[EvidenceSequenceFinding] = []
    for symbol_key in sorted(groups):
        matches = groups[symbol_key]
        numbers = sorted({int(m.number) for m in matches})
        missing = (
            tuple(n for n in range(1, numbers[-1] + 1) if n not in numbers) if numbers else ()
        )

        occurrence_counts: dict[tuple[str, str | None], int] = {}
        for m in matches:
            key = (m.number, m.branch_from)
            occurrence_counts[key] = occurrence_counts.get(key, 0) + 1
        duplicate_numbers = tuple(
            sorted({int(number) for (number, _branch), count in occurrence_counts.items() if count >= 2})
        )

        if missing or duplicate_numbers:
            findings.append(
                EvidenceSequenceFinding(
                    symbol_key=symbol_key,
                    missing_numbers=missing,
                    duplicate_numbers=duplicate_numbers,
                )
            )
    return tuple(findings)
