#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
目的:
VRChat表示名とTOML登録名を安全な順番で紐づける

特徴:
- 完全一致を先に確定して候補から除外
- 正規化一致を確定して候補から除外
- 残った候補だけを high confidence fuzzy 判定
- guest のみ low confidence fuzzy で再選別
- pending / ambiguous / unmatched 判定
- 記号除去
- 全角半角統一
- 小文字化
- 日本語 -> ローマ字変換
- rapidfuzz による類似度
- テストケース付き

前提:
pip install rapidfuzz pykakasi

期待結果:
- 100%一致した名前を fuzzy 候補に残さない
- 似た名前同士の誤リンクを減らす
"""

import re
import sys
import unicodedata
from dataclasses import dataclass, field
from typing import Dict, Iterable, List, Optional, Tuple

try:
    from rapidfuzz import fuzz
except Exception as e:
    if __name__ == "__main__":
        print(f"[FATAL] rapidfuzz import failed: {e}")
        input("Press Enter to exit...")
        sys.exit(1)
    raise

try:
    from pykakasi import kakasi
except Exception as e:
    if __name__ == "__main__":
        print(f"[FATAL] pykakasi import failed: {e}")
        input("Press Enter to exit...")
        sys.exit(1)
    raise


# =========================
# 設定
# =========================

AUTO_LINK_SCORE = 95
LOW_CONFIDENCE_SCORE = 80
AMBIGUOUS_DIFF = 10
LOW_CONFIDENCE_MAX_REMAINING = 3
WEAK_MATCH_SCORE = 50

STATUS_MATCHED = "matched"
STATUS_MATCHED_LOW_CONFIDENCE = "matched_low_confidence"
STATUS_PENDING = "pending"
STATUS_AMBIGUOUS = "ambiguous"
STATUS_UNMATCHED = "unmatched"
STATUS_UNKNOWN_MEMBER = "unknown_member"

ROLE_GUEST = "guest"
ROLE_SELF = "self"
ROLE_STAFF = "staff"

TEST_GUESTS = [
    "okada",
    "tanaka",
    "yamada",
    "messykenty",
]

TEST_CURRENT_USERS = [
    "岡田",
    "ikada",
    "okada_",
    "ＯＫＡＤＡ",
    "tanaka!!!",
    "田中",
    "やまだ",
    "messy_kenty",
    "messy.kenty",
    "MESSYKENTY",
    "山田",
]


@dataclass(frozen=True)
class TomlMember:
    name: str
    role: str = ROLE_GUEST


@dataclass
class MatchResult:
    toml_name: str
    role: str
    status: str
    vrchat_name: Optional[str] = None
    score: Optional[float] = None
    second_score: Optional[float] = None
    reason: str = ""
    candidates: List[Tuple[str, float]] = field(default_factory=list)


# =========================
# ローマ字変換
# =========================

kks = kakasi()


def jp_to_romaji(text: str) -> str:
    try:
        converted = kks.convert(text)

        result = []
        for item in converted:
            result.append(item["hepburn"])

        return "".join(result)

    except Exception as e:
        print(f"[ERROR] romaji convert failed: {text} : {e}")
        return text


# =========================
# 正規化
# =========================

def normalize_name(name: str) -> str:
    """
    正規化内容:
    - 全角半角統一
    - 小文字化
    - 日本語→ローマ字
    - 記号除去
    """

    # Unicode normalize
    name = unicodedata.normalize("NFKC", name)

    # 小文字化
    name = name.lower()

    # 日本語→ローマ字
    name = jp_to_romaji(name)

    # 記号除去
    name = re.sub(r"[^a-z0-9]", "", name)

    return name.strip()


# =========================
# 類似候補検索
# =========================

def find_candidates(
    target_guest: str,
    current_users: List[str],
) -> List[Tuple[str, float]]:

    normalized_target = normalize_name(target_guest)

    normalized_map = {}

    for user in current_users:
        normalized_map[user] = normalize_name(user)

    results = []

    for original_name, normalized_name in normalized_map.items():

        score = fuzz.ratio(
            normalized_target,
            normalized_name,
        )

        results.append((original_name, score))

    results.sort(key=lambda x: x[1], reverse=True)

    return results


def _to_toml_member(member) -> TomlMember:
    if isinstance(member, TomlMember):
        return member

    if isinstance(member, str):
        return TomlMember(name=member)

    if isinstance(member, dict):
        return TomlMember(
            name=str(member.get("name", "")),
            role=str(member.get("role", ROLE_GUEST)),
        )

    name = getattr(member, "name", "")
    role = getattr(member, "role", ROLE_GUEST)

    return TomlMember(
        name=str(name),
        role=str(role),
    )


def _make_candidates(
    toml_name: str,
    current_users: Iterable[str],
) -> List[Tuple[str, float]]:
    normalized_toml = normalize_name(toml_name)
    results = []

    for user in current_users:
        score = fuzz.ratio(
            normalized_toml,
            normalize_name(user),
        )
        results.append((user, score))

    results.sort(key=lambda x: x[1], reverse=True)

    return results


def _classify_remaining(
    member: TomlMember,
    current_users: Iterable[str],
) -> MatchResult:
    candidates = _make_candidates(
        member.name,
        current_users,
    )

    if not candidates:
        return MatchResult(
            toml_name=member.name,
            role=member.role,
            status=STATUS_UNMATCHED,
            reason="no_candidate",
        )

    top_name, top_score = candidates[0]
    second_score = candidates[1][1] if len(candidates) >= 2 else 0
    diff = top_score - second_score

    if top_score < WEAK_MATCH_SCORE:
        status = STATUS_UNMATCHED
        reason = "weak_candidate"
    elif diff < AMBIGUOUS_DIFF:
        status = STATUS_AMBIGUOUS
        reason = "close_candidates"
    else:
        status = STATUS_PENDING
        reason = "below_auto_link_threshold"

    return MatchResult(
        toml_name=member.name,
        role=member.role,
        status=status,
        vrchat_name=top_name,
        score=top_score,
        second_score=second_score,
        reason=reason,
        candidates=candidates,
    )


def _best_fuzzy_match(
    member: TomlMember,
    current_users: Iterable[str],
    min_score: float,
) -> Optional[MatchResult]:
    candidates = _make_candidates(
        member.name,
        current_users,
    )

    if not candidates:
        return None

    top_name, top_score = candidates[0]
    second_score = candidates[1][1] if len(candidates) >= 2 else 0

    if (
        top_score >= min_score
        and top_score - second_score >= AMBIGUOUS_DIFF
    ):
        return MatchResult(
            toml_name=member.name,
            role=member.role,
            status=STATUS_MATCHED,
            vrchat_name=top_name,
            score=top_score,
            second_score=second_score,
            reason="fuzzy",
            candidates=candidates,
        )

    return None


def _apply_fuzzy_round(
    remaining_members: Dict[str, TomlMember],
    remaining_users: set,
    min_score: float,
    status: str,
    reason: str,
    eligible_roles: Optional[set] = None,
) -> List[MatchResult]:
    results = []

    while True:
        possible_matches = []

        for key, member in remaining_members.items():
            if (
                eligible_roles is not None
                and member.role not in eligible_roles
            ):
                continue

            match = _best_fuzzy_match(
                member,
                remaining_users,
                min_score,
            )

            if match is None:
                continue

            possible_matches.append((key, match))

        if not possible_matches:
            break

        possible_matches.sort(
            key=lambda x: (
                x[1].score if x[1].score is not None else 0,
                (x[1].score or 0) - (x[1].second_score or 0),
            ),
            reverse=True,
        )

        key, match = possible_matches[0]
        match.status = status
        match.reason = reason

        results.append(match)
        remaining_members.pop(key)
        remaining_users.remove(match.vrchat_name)

    return results


def match_username_sets(
    toml_members,
    current_users: List[str],
) -> Tuple[List[MatchResult], List[str]]:
    """
    TOML登録名とVRChat現在ユーザーを段階的に1対1照合する。

    戻り値:
    - TOML側の照合結果
    - TOMLに紐づかなかったVRChat名
    """

    members = [_to_toml_member(member) for member in toml_members]
    remaining_members = {
        f"{idx}:{member.name}": member
        for idx, member in enumerate(members)
    }
    remaining_users = set(current_users)
    results = []

    # 1. 完全一致: fuzzy判定せず即確定して候補から除外
    for key, member in list(remaining_members.items()):
        if member.name in remaining_users:
            results.append(
                MatchResult(
                    toml_name=member.name,
                    role=member.role,
                    status=STATUS_MATCHED,
                    vrchat_name=member.name,
                    score=100,
                    second_score=None,
                    reason="exact",
                )
            )
            remaining_members.pop(key)
            remaining_users.remove(member.name)

    # 2. 正規化一致: 一意に対応できるものだけを確定して除外
    normalized_members: Dict[str, List[str]] = {}
    normalized_users: Dict[str, List[str]] = {}

    for key, member in remaining_members.items():
        normalized_members.setdefault(
            normalize_name(member.name),
            [],
        ).append(key)

    for user in remaining_users:
        normalized_users.setdefault(
            normalize_name(user),
            [],
        ).append(user)

    for normalized_name, member_keys in normalized_members.items():
        users = normalized_users.get(normalized_name, [])

        if len(member_keys) != 1 or len(users) != 1:
            continue

        key = member_keys[0]
        user = users[0]
        member = remaining_members[key]

        results.append(
            MatchResult(
                toml_name=member.name,
                role=member.role,
                status=STATUS_MATCHED,
                vrchat_name=user,
                score=100,
                second_score=None,
                reason="normalized",
            )
        )
        remaining_members.pop(key)
        remaining_users.remove(user)

    # 3. 高信頼fuzzy: 残った候補だけで、確定ごとに候補集合を縮める
    results.extend(
        _apply_fuzzy_round(
            remaining_members,
            remaining_users,
            AUTO_LINK_SCORE,
            STATUS_MATCHED,
            "high_confidence_fuzzy",
        )
    )

    # 4. 低信頼guest再選別: guestのみ、候補集合が少ない場合だけ
    if len(remaining_members) <= LOW_CONFIDENCE_MAX_REMAINING:
        results.extend(
            _apply_fuzzy_round(
                remaining_members,
                remaining_users,
                LOW_CONFIDENCE_SCORE,
                STATUS_MATCHED_LOW_CONFIDENCE,
                "guest_low_confidence_fuzzy",
                eligible_roles={ROLE_GUEST},
            )
        )

    # 5. 残りは無理に確定しない
    for member in remaining_members.values():
        results.append(
            _classify_remaining(
                member,
                remaining_users,
            )
        )

    unknown_users = sorted(remaining_users)

    return results, unknown_users


# =========================
# 判定
# =========================

def evaluate_guest_match(
    guest_name: str,
    current_users: List[str],
):

    print("")
    print("=" * 60)
    print(f"[CHECK] guest={guest_name}")

    normalized_guest = normalize_name(guest_name)

    print(f"[INFO] normalized_guest={normalized_guest}")

    candidates = find_candidates(
        guest_name,
        current_users,
    )

    if not candidates:
        print("[ERROR] no candidates")
        return

    print("")
    print("[CHECK] candidate list")

    for idx, (name, score) in enumerate(candidates[:5], start=1):
        print(f"  {idx}. {name:<20} score={score:.2f}")

    top_name, top_score = candidates[0]

    second_score = 0

    if len(candidates) >= 2:
        second_score = candidates[1][1]

    diff = top_score - second_score

    print("")
    print(f"[INFO] top_score={top_score:.2f}")
    print(f"[INFO] second_score={second_score:.2f}")
    print(f"[INFO] diff={diff:.2f}")

    # 自動確定
    if (
        top_score >= AUTO_LINK_SCORE
        and diff >= AMBIGUOUS_DIFF
    ):
        print("")
        print("[INFO] AUTO LINK SAFE")
        print(f"[INFO] linked => {top_name}")
        return

    # 曖昧
    print("")
    print("[WARN] ambiguous match")
    print("[WARN] auto link disabled")

    print("")
    print("[ACTION] manual review required")


# =========================
# main
# =========================

def main():

    print("[CHECK] normalize preview")

    for user in TEST_CURRENT_USERS:
        print(
            f"{user:<20}"
            f" => "
            f"{normalize_name(user)}"
        )

    print("")
    print("[CHECK] username sync ordered matching test")

    results, unknown_users = match_username_sets(
        TEST_GUESTS,
        TEST_CURRENT_USERS,
    )

    for result in results:
        if result.vrchat_name is None:
            print(
                f"{result.toml_name:<20}"
                f" => "
                f"{result.status:<22}"
                f" reason={result.reason}"
            )
            continue

        score_text = ""
        if result.score is not None:
            score_text = f" score={result.score:.2f}"

        second_text = ""
        if result.second_score is not None:
            second_text = f" second={result.second_score:.2f}"

        print(
            f"{result.toml_name:<20}"
            f" => "
            f"{result.vrchat_name:<20}"
            f" {result.status:<22}"
            f" reason={result.reason}"
            f"{score_text}"
            f"{second_text}"
        )

    for user in unknown_users:
        print(
            f"{user:<20}"
            f" => "
            f"{STATUS_UNKNOWN_MEMBER}"
        )

    print("")
    print("[INFO] completed")


if __name__ == "__main__":

    print("[CHECK] start")

    answer = input(
        "[ACTION] run fuzzy test? (yes/no): "
    ).strip().lower()

    if answer != "yes":
        print("[FATAL] cancelled")
        input("Press Enter to exit...")
        sys.exit(1)

    main()
