"""Extract RPG Maker VX Ace .rvdata2 files (Weapons, Armors, States, ...) to CSV.

Usage:
    py tools/rvdata2_to_csv.py                                  # default game + files
    py tools/rvdata2_to_csv.py --game=csi-ii-forever Foo.rvdata2

Output: csv_data/<game>/<Type>.csv (UTF-8 BOM, ';' separator for Excel FR).
Auto-detects the type via ruby_class_name on the first non-nil element.
"""
import csv
import json
import re
import sys
from pathlib import Path
from rubymarshal.reader import loads

ROOT         = Path(r"c:\wamp64\www\csi-world")
CSV_ROOT     = ROOT / "csv_data"
DEFAULT_GAME = "csi-forever"

# RPG Maker VX Ace : params = [MHP, MMP, ATK, DEF, MAT, MDF, AGI, LUK]
PARAM_KEYS = ["hp", "mp", "atk", "def", "mat", "mdf", "agi", "luk"]

# Lignes à supprimer des notes : ce sont des références Yanfly aux templates de loot
# (CSI Forever). Pour les autres jeux, certains tags peuvent en revanche être chargés
# de sens : pour Narval Souls, `<attack skill: X>` est l'équivalent de `\asid[X]` côté CF,
# et doit être préservé pour résoudre la formule d'arme en lecture.
NOTE_STRIP_PATTERNS = [
    re.compile(r"^.*<\s*prefix\w*\b.*$",         re.IGNORECASE | re.MULTILINE),
    re.compile(r"^.*<\s*suffix\w*\b.*$",         re.IGNORECASE | re.MULTILINE),
    re.compile(r"^.*<\s*weapon[\s_]xstat\b.*$",  re.IGNORECASE | re.MULTILINE),
]


# ── Helpers ───────────────────────────────────────────────────────────────────

def decode(value):
    """Strings in rvdata2 are CP1252 (Windows FR) or UTF-8 — rubymarshal returns
    RubyString objects with a `.text` attribute already decoded."""
    if isinstance(value, bytes):
        for enc in ("utf-8", "cp1252", "latin-1"):
            try:
                return value.decode(enc)
            except UnicodeDecodeError:
                continue
        return value.decode("latin-1", errors="replace")
    if hasattr(value, "text"):
        return value.text
    return value


def clean_note(note: str) -> str:
    if not note:
        return ""
    for p in NOTE_STRIP_PATTERNS:
        note = p.sub("", note)
    return re.sub(r"\n{2,}", "\n", note).strip()


def feature_to_dict(f):
    a = f.attributes
    return {
        "code":    a.get("@code"),
        "data_id": a.get("@data_id"),
        "value":   a.get("@value"),
    }


def features_json(a: dict) -> str:
    return json.dumps(
        [feature_to_dict(f) for f in (a.get("@features") or [])],
        ensure_ascii=False,
    )


def effects_json(a: dict) -> str:
    """RPG::UsableItem::Effect : {code, data_id, value1, value2}."""
    out = []
    for e in (a.get("@effects") or []):
        ea = e.attributes
        out.append({
            "code":    ea.get("@code"),
            "data_id": ea.get("@data_id"),
            "value1":  ea.get("@value1"),
            "value2":  ea.get("@value2"),
        })
    return json.dumps(out, ensure_ascii=False)


def damage_json(a: dict) -> str:
    """RPG::UsableItem::Damage : {type, element_id, formula, variance, critical}."""
    d = a.get("@damage")
    if d is None:
        return ""
    da = d.attributes
    return json.dumps({
        "type":       da.get("@type"),
        "element_id": da.get("@element_id"),
        "formula":    decode(da.get("@formula", "")),
        "variance":   da.get("@variance"),
        "critical":   bool(da.get("@critical", False)),
    }, ensure_ascii=False)


# ── Extractors ────────────────────────────────────────────────────────────────

def extract_weapon(obj) -> dict:
    a      = obj.attributes
    params = a.get("@params") or [0] * 8
    row = {
        "id":            a.get("@id"),
        "name":          decode(a.get("@name", "")),
        "description":   decode(a.get("@description", "")),
        "price":         a.get("@price", 0),
        "icon_index":    a.get("@icon_index", 0),
        "wtype_id":      a.get("@wtype_id", 0),
        "etype_id":      a.get("@etype_id", 0),
        "animation_id":  a.get("@animation_id", 0),
        "note":          clean_note(decode(a.get("@note", ""))),
    }
    for i, key in enumerate(PARAM_KEYS):
        row[key] = params[i] if i < len(params) else 0
    row["features_json"] = features_json(a)
    return row


def extract_armor(obj) -> dict:
    a      = obj.attributes
    params = a.get("@params") or [0] * 8
    row = {
        "id":          a.get("@id"),
        "name":        decode(a.get("@name", "")),
        "description": decode(a.get("@description", "")),
        "price":       a.get("@price", 0),
        "icon_index":  a.get("@icon_index", 0),
        "atype_id":    a.get("@atype_id", 0),
        "etype_id":    a.get("@etype_id", 0),
        "note":        clean_note(decode(a.get("@note", ""))),
    }
    for i, key in enumerate(PARAM_KEYS):
        row[key] = params[i] if i < len(params) else 0
    row["features_json"] = features_json(a)
    return row


def extract_state(obj) -> dict:
    a = obj.attributes
    return {
        "id":                     a.get("@id"),
        "name":                   decode(a.get("@name", "")),
        "icon_index":             a.get("@icon_index", 0),
        "priority":               a.get("@priority", 0),
        # Restriction : 0=none, 1=attack enemy, 2=attack any, 3=attack ally, 4=can't act
        "restriction":            a.get("@restriction", 0),
        # Auto removal : 0=none, 1=action end, 2=turn end
        "auto_removal_timing":    a.get("@auto_removal_timing", 0),
        "min_turns":              a.get("@min_turns", 0),
        "max_turns":              a.get("@max_turns", 0),
        "chance_by_damage":       a.get("@chance_by_damage", 0),
        "steps_to_remove":        a.get("@steps_to_remove", 0),
        "remove_by_walking":      int(bool(a.get("@remove_by_walking",      False))),
        "remove_at_battle_end":   int(bool(a.get("@remove_at_battle_end",   False))),
        "remove_by_damage":       int(bool(a.get("@remove_by_damage",       False))),
        "remove_by_restriction":  int(bool(a.get("@remove_by_restriction",  False))),
        "release_by_damage":      int(bool(a.get("@release_by_damage",      False))),
        # Messages affichés en combat
        "message1":               decode(a.get("@message1", "")),
        "message2":               decode(a.get("@message2", "")),
        "message3":               decode(a.get("@message3", "")),
        "message4":               decode(a.get("@message4", "")),
        "note":                   clean_note(decode(a.get("@note", ""))),
        "features_json":          features_json(a),
    }


def extract_item(obj) -> dict:
    a = obj.attributes
    return {
        "id":            a.get("@id"),
        "name":          decode(a.get("@name", "")),
        "description":   decode(a.get("@description", "")),
        "icon_index":    a.get("@icon_index", 0),
        # itype_id : 1=normal, 2=key item
        "itype_id":      a.get("@itype_id", 0),
        "consumable":    int(bool(a.get("@consumable", False))),
        "price":         a.get("@price", 0),
        "scope":         a.get("@scope", 0),
        "occasion":      a.get("@occasion", 0),
        "hit_type":      a.get("@hit_type", 0),
        "success_rate":  a.get("@success_rate", 100),
        "speed":         a.get("@speed", 0),
        "animation_id":  a.get("@animation_id", 0),
        "repeats":       a.get("@repeats", 1),
        "tp_gain":       a.get("@tp_gain", 0),
        "note":          clean_note(decode(a.get("@note", ""))),
        "damage_json":   damage_json(a),
        "effects_json":  effects_json(a),
        "features_json": features_json(a),
    }


def drop_items_json(a: dict) -> str:
    """RPG::Enemy::DropItem : {kind (0=none, 1=item, 2=weapon, 3=armor), data_id, denominator}.
    Probabilité = 1/denominator. On garde uniquement les drops réels (kind > 0)."""
    out = []
    for di in (a.get("@drop_items") or []):
        da   = di.attributes
        kind = da.get("@kind")
        if not kind:
            continue
        out.append({
            "kind":        kind,
            "data_id":     da.get("@data_id"),
            "denominator": da.get("@denominator"),
        })
    return json.dumps(out, ensure_ascii=False)


def actions_json(a: dict) -> str:
    """RPG::Enemy::Action : {skill_id, rating (1-9), condition_type, condition_param1, condition_param2}.
    condition_type : 0=always, 1=turn, 2=hp, 3=mp, 4=state, 5=party level, 6=switch."""
    out = []
    for act in (a.get("@actions") or []):
        ac = act.attributes
        out.append({
            "skill_id":         ac.get("@skill_id"),
            "rating":           ac.get("@rating"),
            "condition_type":   ac.get("@condition_type"),
            "condition_param1": ac.get("@condition_param1"),
            "condition_param2": ac.get("@condition_param2"),
        })
    return json.dumps(out, ensure_ascii=False)


def extract_enemy(obj) -> dict:
    a      = obj.attributes
    params = a.get("@params") or [0] * 8
    row = {
        "id":            a.get("@id"),
        "name":          decode(a.get("@name", "")),
        "battler_name":  decode(a.get("@battler_name", "")),
        "battler_hue":   a.get("@battler_hue", 0),
        "exp":           a.get("@exp", 0),
        "gold":          a.get("@gold", 0),
        "hit":           a.get("@hit", 95),
        "eva":           a.get("@eva", 5),
        "note":          clean_note(decode(a.get("@note", ""))),
    }
    for i, key in enumerate(PARAM_KEYS):
        row[key] = params[i] if i < len(params) else 0
    row["drop_items_json"] = drop_items_json(a)
    row["actions_json"]    = actions_json(a)
    row["features_json"]   = features_json(a)
    return row


def extract_actor(obj) -> dict:
    """RPG::Actor — un acteur (personnage jouable).
    On garde `class_id` pour résoudre la classe qui porte les learnings de sorts.
    Les équipements initiaux et les paramètres sont ignorés — hors périmètre ici."""
    a = obj.attributes
    return {
        "id":              a.get("@id"),
        "name":            decode(a.get("@name", "")),
        "nickname":        decode(a.get("@nickname", "")),
        "description":     decode(a.get("@description", "")),
        "class_id":        a.get("@class_id", 0),
        "initial_level":   a.get("@initial_level", 1),
        "max_level":       a.get("@max_level", 99),
        "note":            clean_note(decode(a.get("@note", ""))),
        "features_json":   features_json(a),
    }


def learnings_json(a: dict) -> str:
    """RPG::Class::Learning : {level, skill_id, note}.
    On sérialise pour permettre au consommateur PHP de résoudre skill_id → DB id."""
    out = []
    for lr in (a.get("@learnings") or []):
        la = lr.attributes
        out.append({
            "level":    la.get("@level"),
            "skill_id": la.get("@skill_id"),
            "note":     decode(la.get("@note", "")),
        })
    return json.dumps(out, ensure_ascii=False)


def extract_class(obj) -> dict:
    """RPG::Class — porte les `@learnings` (sorts appris par niveau).
    La note peut aussi contenir un bloc <learn skills>…</learn skills> pour les
    sorts appris via le menu (traités côté PHP, la note est conservée telle quelle)."""
    a = obj.attributes
    return {
        "id":              a.get("@id"),
        "name":            decode(a.get("@name", "")),
        # `clean_note` retire les patterns Yanfly de loot — on garde `<learn skills>`
        # qui n'est pas dans NOTE_STRIP_PATTERNS.
        "note":            clean_note(decode(a.get("@note", ""))),
        "learnings_json":  learnings_json(a),
        "features_json":   features_json(a),
    }


def extract_skill(obj) -> dict:
    a = obj.attributes
    return {
        "id":                  a.get("@id"),
        "name":                decode(a.get("@name", "")),
        "description":         decode(a.get("@description", "")),
        "icon_index":          a.get("@icon_index", 0),
        "stype_id":            a.get("@stype_id", 0),
        # Scope : 0=none, 1=1 ennemi, 2=tous ennemis, 3..6=N ennemis aléatoires,
        # 7=1 allié, 8=tous alliés, 9=1 allié KO, 10=tous alliés KO, 11=soi-même
        "scope":               a.get("@scope", 0),
        # Occasion : 0=toujours, 1=combat, 2=menu, 3=jamais
        "occasion":            a.get("@occasion", 0),
        # Hit type : 0=certain, 1=physique, 2=magique
        "hit_type":            a.get("@hit_type", 0),
        "success_rate":        a.get("@success_rate", 100),
        "speed":               a.get("@speed", 0),
        "mp_cost":             a.get("@mp_cost", 0),
        "tp_cost":             a.get("@tp_cost", 0),
        "tp_gain":             a.get("@tp_gain", 0),
        "animation_id":        a.get("@animation_id", 0),
        "repeats":             a.get("@repeats", 1),
        "required_wtype_id1":  a.get("@required_wtype_id1", 0),
        "required_wtype_id2":  a.get("@required_wtype_id2", 0),
        "message1":            decode(a.get("@message1", "")),
        "message2":            decode(a.get("@message2", "")),
        "note":                clean_note(decode(a.get("@note", ""))),
        "damage_json":         damage_json(a),
        "effects_json":        effects_json(a),
        "features_json":       features_json(a),
    }


EXTRACTORS = {
    "RPG::Weapon": extract_weapon,
    "RPG::Armor":  extract_armor,
    "RPG::State":  extract_state,
    "RPG::Skill":  extract_skill,
    "RPG::Item":   extract_item,
    "RPG::Enemy":  extract_enemy,
    "RPG::Actor":  extract_actor,
    "RPG::Class":  extract_class,
}


# ── Main ──────────────────────────────────────────────────────────────────────

def process(src: Path, out_dir: Path) -> None:
    data = loads(src.read_bytes())
    if not isinstance(data, list):
        print(f"[SKIP] {src.name}: pas une liste (probablement System.rvdata2 — utilisé pour mappings).", file=sys.stderr)
        return
    rows = []
    cls  = None

    for obj in data:
        if obj is None:
            continue
        if cls is None:
            cls = obj.ruby_class_name
            extractor = EXTRACTORS.get(cls)
            if extractor is None:
                print(f"[WARN] {src.name}: type '{cls}' non supporté, ignoré.", file=sys.stderr)
                return
        rows.append(extractor(obj))

    if not rows:
        print(f"[WARN] {src.name}: aucun objet exploitable.", file=sys.stderr)
        return

    out_dir.mkdir(parents=True, exist_ok=True)

    # CSV pour inspection Excel (quoting=ALL pour éviter les corruptions de parsing
    # quand un champ contient des guillemets ou un mix newline + double-quote)
    dst = out_dir / (src.stem + ".csv")
    with dst.open("w", encoding="utf-8-sig", newline="") as fh:
        writer = csv.DictWriter(
            fh, fieldnames=list(rows[0].keys()),
            delimiter=";", quoting=csv.QUOTE_ALL,
        )
        writer.writeheader()
        writer.writerows(rows)

    # JSON sidecar : format canonique pour les importeurs PHP (lecture sans ambiguïté).
    # On dé-stringifie les champs *_json pour que les consommateurs n'aient pas à re-parser.
    json_string_fields = (
        "damage_json", "effects_json", "features_json",
        "drop_items_json", "actions_json", "learnings_json",
    )
    rows_for_json = []
    for row in rows:
        copy = dict(row)
        for k in json_string_fields:
            if k not in copy:
                continue
            if isinstance(copy[k], str) and copy[k]:
                copy[k] = json.loads(copy[k])
            elif not copy[k]:
                copy[k] = None
        rows_for_json.append(copy)
    dst_json = out_dir / (src.stem + ".json")
    with dst_json.open("w", encoding="utf-8") as fh:
        json.dump(rows_for_json, fh, ensure_ascii=False, indent=None)

    print(f"OK [{cls}] {len(rows)} ligne(s) -> {dst.relative_to(ROOT)} + {dst_json.name}")


def main() -> None:
    args  = sys.argv[1:]
    game  = DEFAULT_GAME
    files = []

    for arg in args:
        if arg.startswith("--game="):
            game = arg.split("=", 1)[1].strip() or DEFAULT_GAME
        else:
            files.append(Path(arg))

    out_dir = CSV_ROOT / game

    if not files:
        # Auto-discover : tous les .rvdata2 du dossier du jeu
        files = sorted(out_dir.glob("*.rvdata2"))
        if not files:
            print(f"[ERROR] aucun .rvdata2 trouvé dans {out_dir}", file=sys.stderr)
            sys.exit(1)

    for src in files:
        if not src.exists():
            print(f"[SKIP] introuvable : {src}", file=sys.stderr)
            continue
        process(src, out_dir)


if __name__ == "__main__":
    main()
