"""
CSV から各曲のクレジット情報を読み込み、内部データ構造に変換する。

入力 CSV カラム:
  album_slug, track_no, title, writers, producers,
  sampling_rights_holders, notes

writers / producers / sampling_rights_holders は **セミコロン区切り**。

人物同定辞書 (2026-09-10 追加, Codex監査 #2 対応):
  data/aliases.csv (canonical, alias) に登録された表記は、読み込み時点で
  canonical 名に正規化する。これにより「同一人物が writer 欄と producer 欄で
  別表記になっている」ことによる二重計上を防ぐ。
  未登録の表記揺れは自動統合しない（S軸の本人alias同様、確認済みのペアのみ登録する）。
"""
from __future__ import annotations

import csv
from pathlib import Path
from typing import TypedDict

DEFAULT_ALIASES_CSV = Path(__file__).resolve().parent.parent / "data" / "aliases.csv"
if not DEFAULT_ALIASES_CSV.exists():
    # 公開パッケージはsrc/とaliases.csvを同じ親ディレクトリに置く。
    DEFAULT_ALIASES_CSV = Path(__file__).resolve().parent.parent / "aliases.csv"


def load_person_aliases(aliases_csv: str | Path = DEFAULT_ALIASES_CSV) -> dict[str, str]:
    """
    data/aliases.csv (canonical, alias, source_note, source_url, verified_on) を読み込み、
    {alias: canonical} の正規化マップを返す。
    欠落・競合・連鎖する辞書を黙って使わない。
    """
    p = Path(aliases_csv)
    if not p.exists():
        raise FileNotFoundError(f"Person aliases CSV not found: {p}")
    mapping: dict[str, str] = {}
    with p.open("r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        if not {"canonical", "alias", "source_note", "source_url", "verified_on"} <= set(reader.fieldnames or []):
            raise ValueError(f"Incomplete alias dictionary columns: {p}")
        for row in reader:
            if None in row or not row.get("canonical", "").strip() or not row.get("alias", "").strip():
                raise ValueError(f"Malformed alias row: {p}")
            canonical = (row.get("canonical") or "").strip()
            alias = (row.get("alias") or "").strip()
            if canonical and alias:
                if not row.get("source_url", "").strip() or not row.get("verified_on", "").strip():
                    raise ValueError(f"Missing source_url or verified_on for alias: {alias}")
                if alias in mapping and mapping[alias] != canonical:
                    raise ValueError(f"Conflicting alias: {alias}")
                mapping[alias] = canonical
    for alias, canonical in mapping.items():
        if canonical in mapping and mapping[canonical] != canonical:
            raise ValueError(f"Alias chain must be flattened: {alias} -> {canonical}")
    return mapping


def _normalize_name(name: str, alias_map: dict[str, str]) -> str:
    if name.casefold() in {"kenny & billy", "cubeatz", "wallis lane", "fnz", "the loud pack"}:
        raise ValueError(f"Expand verified group credit into people in input CSV: {name}")
    if any(marker in name for marker in ("=", "?", "+")):
        raise ValueError(f"Unresolved identity annotation in credit field: {name}")
    return alias_map.get(name, name)


class Track(TypedDict):
    track_no: int
    title: str
    writers: list[str]
    producers: list[str]
    sampling_rights_holders: list[str]
    notes: str


class Album(TypedDict):
    album_slug: str
    tracks: list[Track]


def _split_semicolon(s: str) -> list[str]:
    if not s or not s.strip():
        return []
    return [x.strip() for x in s.split(";") if x.strip()]


def load_album_from_csv(
    csv_path: str | Path,
    expected_album_slug: str,
    aliases_csv: str | Path = DEFAULT_ALIASES_CSV,
) -> Album:
    """
    CSVから単一アルバムを読み込む。
    expected_album_slug と異なる album_slug の行はエラー。

    data/aliases.csv に登録された人物表記は canonical 名へ正規化してから
    Track に格納する (writers / producers / sampling_rights_holders 全対象)。
    """
    p = Path(csv_path)
    if not p.exists():
        raise FileNotFoundError(f"CSV not found: {p}")

    alias_map = load_person_aliases(aliases_csv)

    tracks: list[Track] = []
    with p.open("r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            if None in row:
                raise ValueError(f"Extra CSV cells (quote commas in notes): {p}")
            slug = row.get("album_slug", "").strip()
            if slug != expected_album_slug:
                raise ValueError(
                    f"album_slug mismatch in {p}: "
                    f"row={slug!r}, expected={expected_album_slug!r}"
                )
            writers = [_normalize_name(n, alias_map) for n in _split_semicolon(row.get("writers", ""))]
            producers = [_normalize_name(n, alias_map) for n in _split_semicolon(row.get("producers", ""))]
            sampling = [_normalize_name(n, alias_map) for n in _split_semicolon(row.get("sampling_rights_holders", ""))]
            tracks.append(Track(
                track_no=int(row["track_no"]),
                title=row["title"].strip(),
                writers=writers,
                producers=producers,
                sampling_rights_holders=sampling,
                notes=row.get("notes", "").strip(),
            ))

    tracks.sort(key=lambda t: t["track_no"])
    if len({t["track_no"] for t in tracks}) != len(tracks):
        raise ValueError(f"Duplicate track_no in {p}")
    return Album(album_slug=expected_album_slug, tracks=tracks)


if __name__ == "__main__":
    import sys
    if len(sys.argv) != 3:
        print("usage: python load_credits.py <csv_path> <album_slug>")
        sys.exit(2)
    album = load_album_from_csv(sys.argv[1], sys.argv[2])
    print(f"Loaded album: {album['album_slug']}")
    print(f"Total tracks: {len(album['tracks'])}")
    for t in album["tracks"][:3]:
        print(f"  {t['track_no']:>2}. {t['title']}")
        print(f"      writers   : {t['writers']}")
        print(f"      producers : {t['producers']}")
