#!/usr/bin/env python3
"""
Generate AtlasLootClassic_TBCA_BIS/enchants_gems.lua from a BiS enchant/gem CSV.

Usage:
  python AtlasLootClassic_TBCA_BIS/tools/csv_to_enchants_gems.py \
    "ADDONS-FOR-REFERENCE-ONLY/UPDATED Open AI BiS enchants gems.final.2026-03-05.csv" \
    "AtlasLootClassic_TBCA_BIS/enchants_gems.lua"
"""

from __future__ import annotations

import argparse
import csv
import re
from collections import defaultdict
from pathlib import Path


SPEC_MAP = {
    ("Druid", "Balance"): ["DruidBalance"],
    ("Druid", "Feral DPS"): ["DruidCat"],
    ("Druid", "Feral Tank"): ["DruidBear"],
    ("Druid", "Restoration"): ["DruidRestoration"],
    ("Hunter", "Beast Mastery"): ["HunterBM"],
    ("Mage", "Fire"): ["MageFire"],
    ("Paladin", "Holy"): ["PaladinHeal"],
    ("Paladin", "Protection"): ["PaladinTank"],
    ("Paladin", "Retribution"): ["PaladinRet"],
    ("Priest", "Holy"): ["PriestHoly"],
    ("Priest", "Shadow"): ["PriestShadow"],
    ("Rogue", "DPS"): ["RogueDPS"],
    ("Shaman", "Elemental"): ["ShamanElemental"],
    ("Shaman", "Enhancement"): ["ShamanEnhancement"],
    ("Shaman", "Restoration"): ["ShamanRestoration"],
    ("Warlock", "ALLSPECS"): ["WarlockAffliction", "WarlockDestruction"],
    ("Warrior", "Arms"): ["WarriorArms"],
    ("Warrior", "Fury"): ["WarriorFury"],
    ("Warrior", "Protection"): ["WarriorProt"],
}

CLASS_WIDE_SPECS = {
    "Hunter": ["HunterBM", "HunterSV"],
    "Mage": ["MageArcane", "MageFire"],
    "Warlock": ["WarlockAffliction", "WarlockDestruction"],
}

GEM_SECTION_ORDER = [
    ("Meta Gem", "META"),
    ("Red Gem", "RED"),
    ("Blue Gem", "BLUE"),
    ("Yellow Gem", "YELLOW"),
]

LEFT_COLUMN_START = 1
RIGHT_COLUMN_START = 16

TARGET_SORT = {
    "Head": 1,
    "Shoulder": 2,
    "Cloak": 3,
    "Chest": 4,
    "Bracers": 5,
    "Gloves": 6,
    "Legs": 7,
    "Boots": 8,
    "Rings": 9,
    "Shield": 10,
    "Weapon": 11,
}

RANK_SORT = {"BiS": 0, "Option": 1, "Alt": 1}

# Force-correct known TBC item IDs by exact item name. This protects the
# generated Lua from CSV ID/name mismatches.
CANONICAL_ITEM_IDS = {
    "bold living ruby": 24027,
    "delicate living ruby": 24028,
    "teardrop living ruby": 24029,
    "glowing nightseye": 24056,
    "royal nightseye": 24057,
    "shifting nightseye": 24055,
    "sovereign nightseye": 24054,
    "smooth dawnstone": 24048,
    "rigid dawnstone": 24051,
    "thick dawnstone": 24052,
    "inscribed noble topaz": 24058,
    "luminous noble topaz": 24060,
    "enduring talasite": 24062,
    "nethercobra leg armor": 29535,
    "nethercleft leg armor": 29536,
    "clefthide leg armor": 29534,
    "golden spellthread": 24276,
    "silver spellthread": 24275,
    "runic spellthread": 24274,
}

AMBIGUOUS_PREFERRED_IDS = {
    "greater inscription of the orb": 28909,
    "greater inscription of the blade": 28910,
    "greater inscription of the knight": 28911,
    "greater inscription of the oracle": 28912,
    "glyph of renewal": 29189,
}

CANONICAL_SPELL_IDS = {
    "enchant bracer - spellpower": 27917,
}


def q(value: str) -> str:
    return value.replace("\\", "\\\\").replace('"', '\\"')


def parse_phase(raw: str) -> int:
    text = (raw or "").strip()
    if not text or text.lower() == "any":
        return 1
    try:
        phase = int(text)
    except ValueError:
        return 1
    if phase < 1:
        return 1
    if phase > 5:
        return 5
    return phase


def as_int(raw: str) -> int | None:
    text = (raw or "").strip()
    if not text:
        return None
    try:
        value = int(text)
    except ValueError:
        return None
    return value if value > 0 else None


def load_tbc_name_to_ids() -> dict[str, set[int]]:
    db_path = Path("Questie/Database/TBC/tbcItemDB.lua")
    if not db_path.exists():
        return {}

    text = db_path.read_text(encoding="utf-8", errors="ignore")
    rows = re.findall(r"\[(\d+)\]\s*=\s*\{'([^']+)'", text)
    out: dict[str, set[int]] = defaultdict(set)
    for item_id_raw, name in rows:
        out[name.strip().lower()].add(int(item_id_raw))
    return out


def correct_item_id(item_name: str, item_id: int | None, name_to_ids: dict[str, set[int]]) -> int | None:
    key = (item_name or "").strip().lower()
    if not key:
        return item_id

    if key in CANONICAL_ITEM_IDS:
        return CANONICAL_ITEM_IDS[key]

    ids = name_to_ids.get(key)
    if not ids:
        return item_id
    if len(ids) == 1:
        return next(iter(ids))
    if item_id in ids:
        return item_id
    if key in AMBIGUOUS_PREFERRED_IDS:
        return AMBIGUOUS_PREFERRED_IDS[key]
    return item_id


def correct_spell_id(item_name: str, spell_id: int | None) -> int | None:
    key = (item_name or "").strip().lower()
    if not key:
        return spell_id
    if key in CANONICAL_SPELL_IDS:
        return CANONICAL_SPELL_IDS[key]
    return spell_id


def make_token(item_id: int | None, spell_id: int | None, target: str = "", notes: str = "") -> str | None:
    # Shoulder enchants use a custom item token type so description text
    # stays as our explicit faction note instead of token-cost replacement.
    if target == "Shoulder" and item_id:
        note = (notes or "").strip()
        if note:
            return f"si{item_id}|{note}"
        return f"si{item_id}"
    # Head enchants display better in AtlasLoot using the item token.
    if target == "Head" and item_id:
        return f"i{item_id}"
    if spell_id:
        return f"prof{spell_id}"
    if item_id:
        return f"i{item_id}"
    return None


def rank_label(rank: str) -> str:
    r = (rank or "").strip()
    return "BiS" if r.lower() == "bis" else "Alt"


def row_sort_key(row: dict, include_target: bool = True) -> tuple:
    target = row.get("target", "")
    rank = row.get("rank", "")
    item_name = row.get("item_name", "")
    phase = row.get("min_phase", 1)
    target_idx = TARGET_SORT.get(target, 999)
    rank_idx = RANK_SORT.get(rank, 9)
    if include_target:
        return (phase, target_idx, rank_idx, item_name)
    return (phase, rank_idx, item_name)


def add_header(rows: list[dict], token: str, name: str, desc: str, min_phase: int = 1) -> None:
    rows.append(
        {
            "token": token,
            "name": name,
            "desc": desc,
            "min_phase": min_phase,
        }
    )


def add_spacer(rows: list[dict], min_phase: int = 1) -> None:
    rows.append(
        {
            "token": "hdr_spacer",
            "name": " ",
            "desc": " ",
            "min_phase": min_phase,
        }
    )


def to_row(token: str, name: str, desc: str, min_phase: int) -> dict:
    return {
        "token": token,
        "name": name,
        "desc": desc,
        "min_phase": min_phase,
    }


def place_column_rows(rows: list[dict], start_slot: int) -> list[dict]:
    out: list[dict] = []
    for idx, row in enumerate(rows):
        copy = row.copy()
        copy["slot"] = start_slot + idx
        out.append(copy)
    return out


def build_rows_for_spec(spec_rows: list[dict]) -> list[dict]:
    gems = [r for r in spec_rows if r["category"] == "Gem"]
    ench = [r for r in spec_rows if r["category"] == "Enchant"]

    # Left column: gems, spacing, pants enchant, shoulders.
    left_rows: list[dict] = []
    add_header(left_rows, "hdr_gems", "GEMS", "Meta / Red / Blue / Yellow", min_phase=1)
    for target, prefix in GEM_SECTION_ORDER:
        section = [r for r in gems if r["target"] == target]
        if not section:
            continue

        # Keep the gem area compact and stable across all specs: one best row per gem color.
        section_sorted = sorted(section, key=lambda x: row_sort_key(x, include_target=False))
        bis_rows = [r for r in section_sorted if (r.get("rank") or "").strip().lower() == "bis"]
        chosen = bis_rows[0] if bis_rows else section_sorted[0]

        left_rows.append(
            to_row(
                chosen["token"],
                f'{prefix} - {chosen["item_name"]} [{rank_label(chosen["rank"])}]',
                chosen["notes"] or " ",
                chosen["min_phase"],
            )
        )

    # Two visual line breaks between gems and pants section.
    add_spacer(left_rows)
    add_spacer(left_rows)

    legs = [r for r in ench if r["target"] == "Legs" and r["item_id"] is not None]
    shoulder = [r for r in ench if r["target"] == "Shoulder"]

    # Pants: prefer BiS only (single practical choice).
    legs_sorted = sorted(legs, key=row_sort_key)
    legs_bis = [r for r in legs_sorted if (r.get("rank") or "").strip().lower() == "bis"]
    legs_final = legs_bis[:1] if legs_bis else (legs_sorted[:1] if legs_sorted else [])
    if legs_final:
        add_header(left_rows, "hdr_legs", "LEGS ENCHANTS", "Usually one BiS choice", min_phase=1)
        for r in legs_final:
            left_rows.append(
                to_row(
                    r["token"],
                    f'LEGS - {r["item_name"]} [{rank_label(r["rank"])}]',
                    r["notes"] or " ",
                    r["min_phase"],
                )
            )

    # One visual line break between legs and shoulders.
    add_spacer(left_rows)

    # Shoulders: show both reputations and clarify mutual exclusivity.
    shoulder_sorted = sorted(shoulder, key=row_sort_key)
    if shoulder_sorted:
        add_header(left_rows, "hdr_shoulders", "SHOULDER ENCHANTS", "Choose one: Aldor or Scryers", min_phase=1)
        for idx, r in enumerate(shoulder_sorted[:2], start=1):
            if idx == 1:
                desc = "Requires: The Aldor"
            elif idx == 2:
                desc = "Requires: The Scryers"
            else:
                desc = r["notes"] or " "
            left_rows.append(
                to_row(
                    r["token"],
                    f'SHOULDER - {r["item_name"]} [{rank_label(r["rank"])}]',
                    desc,
                    r["min_phase"],
                )
            )

    # Right column: head first, then a spacer, then the remaining enchants.
    remainder = [r for r in ench if r not in legs and r not in shoulder]
    head_rows = [r for r in remainder if r["target"] == "Head"]
    other_rows = [r for r in remainder if r["target"] != "Head"]

    right_rows: list[dict] = []
    add_header(right_rows, "hdr_otherench", "OTHER ENCHANTS", "All remaining enchants", min_phase=1)

    for r in sorted(head_rows, key=lambda x: row_sort_key(x, include_target=False)):
        right_rows.append(
            to_row(
                r["token"],
                f'HEAD - {r["item_name"]} [{rank_label(r["rank"])}]',
                r["notes"] or " ",
                r["min_phase"],
            )
        )

    if head_rows:
        add_spacer(right_rows)

    for r in sorted(other_rows, key=row_sort_key):
        right_rows.append(
            to_row(
                r["token"],
                f'{r["target"]} - {r["item_name"]} [{rank_label(r["rank"])}]',
                r["notes"] or " ",
                r["min_phase"],
            )
        )

    out = place_column_rows(left_rows, LEFT_COLUMN_START) + place_column_rows(right_rows, RIGHT_COLUMN_START)
    out.sort(key=lambda x: x["slot"])

    return out


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("csv_path", type=Path)
    parser.add_argument("lua_path", type=Path)
    args = parser.parse_args()

    if not args.csv_path.exists():
        raise SystemExit(f"CSV not found: {args.csv_path}")

    name_to_ids = load_tbc_name_to_ids()
    per_spec_rows: dict[str, list[dict]] = defaultdict(list)
    with args.csv_path.open(newline="", encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)
        for row in reader:
            cls = (row.get("class") or "").strip()
            talent = (row.get("talent") or "").strip()
            category = (row.get("category") or "").strip()
            target = (row.get("target") or "").strip()
            rank = (row.get("rank") or "").strip()
            item_name = (row.get("item_name") or "").strip()
            notes = (row.get("notes") or "").strip()
            phase = parse_phase(row.get("phase") or "")
            item_id = as_int(row.get("item_id") or "")
            spell_id = as_int(row.get("spell_id") or "")
            item_id = correct_item_id(item_name, item_id, name_to_ids)
            spell_id = correct_spell_id(item_name, spell_id)
            token = make_token(item_id, spell_id, target, notes)

            if category not in {"Gem", "Enchant"} or not item_name or not token:
                continue

            keys = CLASS_WIDE_SPECS.get(cls) or SPEC_MAP.get((cls, talent))
            if not keys:
                continue

            normalized = {
                "category": category,
                "target": target,
                "rank": rank,
                "item_name": item_name,
                "notes": notes,
                "item_id": item_id,
                "token": token,
                "min_phase": phase,
            }

            for key in keys:
                per_spec_rows[key].append(normalized.copy())

    final_rows_by_spec: dict[str, dict[int, list[dict]]] = {}
    for spec_key, rows in per_spec_rows.items():
        ordered = build_rows_for_spec(rows)
        phase_map: dict[int, list[dict]] = {}
        for phase in range(1, 6):
            phase_rows = [r for r in ordered if r["min_phase"] <= phase]
            phase_map[phase] = phase_rows
        final_rows_by_spec[spec_key] = phase_map

    args.lua_path.parent.mkdir(parents=True, exist_ok=True)
    source_label = args.csv_path.as_posix()

    with args.lua_path.open("w", encoding="utf-8", newline="\n") as out:
        out.write("-----------------------------------------------------------------------\n")
        out.write(f"-- Auto-generated from {source_label}.\n")
        out.write("-- Adds one unified page (Gems / Enchants / Attachments) per spec.\n")
        out.write("-----------------------------------------------------------------------\n\n")
        out.write("local addonName = ...\n")
        out.write("local AtlasLoot = _G.AtlasLoot\n")
        out.write("if not AtlasLoot or not AtlasLoot.ItemDB or not AtlasLoot.Button then\n")
        out.write("\treturn\n")
        out.write("end\n\n")
        out.write("local data = AtlasLoot.ItemDB:Get(addonName)\n")
        out.write("if not data then\n")
        out.write("\treturn\n")
        out.write("end\n\n")
        out.write("local p1 = data:GetDifficultyByName(\"p1\") or 1\n")
        out.write("local p2 = data:GetDifficultyByName(\"p2\") or 2\n")
        out.write("local p3 = data:GetDifficultyByName(\"p3\") or 3\n")
        out.write("local p4 = data:GetDifficultyByName(\"p4\") or 4\n")
        out.write("local p5 = data:GetDifficultyByName(\"p5\") or 5\n")
        out.write("local phases = { p1, p2, p3, p4, p5 }\n\n")
        out.write("local HeaderType = AtlasLoot.Button:AddType(\"TBCAEGHeader\", \"hdreg\")\n")
        out.write("AtlasLoot.Button:DisableDescriptionReplaceForce(\"TBCAEGHeader\", true)\n\n")
        out.write("function HeaderType.GetStringContent(str)\n")
        out.write("\treturn str\n")
        out.write("end\n\n")
        out.write("function HeaderType.OnSet(button, second)\n")
        out.write("\tlocal target = second and button.secButton or button\n")
        out.write("\tHeaderType.Refresh(target)\n")
        out.write("end\n\n")
        out.write("function HeaderType.Refresh(button)\n")
        out.write("\tif button.type == \"secButton\" then\n")
        out.write("\t\tbutton:SetNormalTexture(nil)\n")
        out.write("\t\tif button.overlay then button.overlay:Hide() end\n")
        out.write("\t\treturn\n")
        out.write("\tend\n\n")
        out.write("\tlocal info = button.__atlaslootinfo or {}\n")
        out.write("\tlocal name = info.Name or \" \"\n")
        out.write("\tlocal desc = info.Description or \" \"\n")
        out.write("\tif name == \"\" then name = \" \" end\n")
        out.write("\tif desc == \"\" then desc = \" \" end\n\n")
        out.write("\tbutton.icon:SetTexture(nil)\n")
        out.write("\tbutton.icon:SetAlpha(0)\n")
        out.write("\tif button.overlay then button.overlay:Hide() end\n")
        out.write("\tif button.count then button.count:Hide() end\n")
        out.write("\tif button.phaseIndicator then button.phaseIndicator:Hide() end\n")
        out.write("\tif button.favourite then button.favourite:Hide() end\n")
        out.write("\tbutton.name:SetText(\"|cffffcc00\" .. name .. \"|r\")\n")
        out.write("\tbutton.extra:SetText(\"|cffbfbfbf\" .. desc .. \"|r\")\n")
        out.write("end\n\n")
        out.write("function HeaderType.OnClear(button)\n")
        out.write("\tif button.type == \"secButton\" then\n")
        out.write("\t\tbutton:SetNormalTexture(nil)\n")
        out.write("\t\tif button.overlay then button.overlay:Hide() end\n")
        out.write("\t\treturn\n")
        out.write("\tend\n")
        out.write("\tif button.icon then button.icon:SetAlpha(1) end\n")
        out.write("end\n\n")
        out.write("local MIX_ITTYPE = data:AddItemTableType(\"TBCAEGHeader\", \"Name\", \"Description\")\n\n")
        out.write("local ShoulderItemType = AtlasLoot.Button:AddIdentifier(\"Item\", \"si\")\n")
        out.write("local BaseItemType = AtlasLoot.Button:GetType(\"Item\")\n\n")
        out.write("local SHOULDER_REQ_DESC = {\n")
        out.write('\t[28886] = "Requires: The Aldor",\n')
        out.write('\t[28887] = "Requires: The Aldor",\n')
        out.write('\t[28888] = "Requires: The Aldor",\n')
        out.write('\t[28889] = "Requires: The Aldor",\n')
        out.write('\t[28909] = "Requires: The Scryers",\n')
        out.write('\t[28910] = "Requires: The Scryers",\n')
        out.write('\t[28911] = "Requires: The Scryers",\n')
        out.write('\t[28912] = "Requires: The Scryers",\n')
        out.write('\t[35728] = "Requires: The Scryers",\n')
        out.write('\t[35729] = "Requires: The Scryers",\n')
        out.write('\t[35730] = "Requires: The Scryers",\n')
        out.write('\t[35731] = "Requires: The Scryers",\n')
        out.write("}\n\n")
        out.write("do\n")
        out.write("\tlocal OriginalGetDescription = BaseItemType.GetDescription\n")
        out.write("\tif OriginalGetDescription and not BaseItemType.__tbcaBisShoulderDescPatched then\n")
        out.write("\t\tBaseItemType.__tbcaBisShoulderDescPatched = true\n")
        out.write("\t\tfunction BaseItemType.GetDescription(itemID, itemEquipLoc, itemType, itemSubType)\n")
        out.write("\t\t\tlocal forced = itemID and SHOULDER_REQ_DESC[itemID] or nil\n")
        out.write("\t\t\tif forced then\n")
        out.write("\t\t\t\treturn forced\n")
        out.write("\t\t\tend\n")
        out.write("\t\t\treturn OriginalGetDescription(itemID, itemEquipLoc, itemType, itemSubType)\n")
        out.write("\t\tend\n")
        out.write("\tend\n")
        out.write("end\n\n")
        out.write("function ShoulderItemType.GetStringContent(str)\n")
        out.write("\tlocal itemId, forced = str:match(\"^(%d+)|(.+)$\")\n")
        out.write("\tif itemId then\n")
        out.write("\t\treturn { itemID = tonumber(itemId), forcedDesc = forced }\n")
        out.write("\tend\n")
        out.write("\treturn tonumber(str)\n")
        out.write("end\n\n")
        out.write("local function ApplyShoulderDesc(button, second)\n")
        out.write("\tif not button then return end\n")
        out.write("\tlocal target = second and button.secButton or button\n")
        out.write("\tif not target or target.type == \"secButton\" then return end\n")
        out.write("\tlocal info = button.__atlaslootinfo\n")
        out.write("\tlocal typ = second and info and info.secType or info and info.type\n")
        out.write("\tlocal data = typ and typ[2] or nil\n")
        out.write("\tlocal desc = type(data) == \"table\" and data.forcedDesc or nil\n")
        out.write("\tif (not desc or desc == \"\") and target.ItemID then\n")
        out.write("\t\tdesc = SHOULDER_REQ_DESC[target.ItemID]\n")
        out.write("\tend\n")
        out.write("\tif desc and desc ~= \"\" then\n")
        out.write("\t\ttarget.extra:SetText(desc)\n")
        out.write("\tend\n")
        out.write("end\n\n")

        out.write("function ShoulderItemType.OnSet(button, second)\n")
        out.write("\tBaseItemType.OnSet(button, second)\n")
        out.write("\tApplyShoulderDesc(button, second)\n")
        out.write("end\n\n")

        out.write("function ShoulderItemType.Refresh(button)\n")
        out.write("\tBaseItemType.Refresh(button)\n")
        out.write("\tApplyShoulderDesc(button, false)\n")
        out.write("end\n\n")

        out.write("local SPEC_ROWS = {\n")
        for spec_key in sorted(final_rows_by_spec.keys()):
            out.write(f"\t{spec_key} = {{\n")
            for phase in range(1, 6):
                phase_rows = final_rows_by_spec[spec_key][phase]
                out.write(f"\t\t[{phase}] = {{\n")
                for r in phase_rows:
                    out.write(
                        f'\t\t\t{{ {r["slot"]}, "{q(r["token"])}", "{q(r["name"])}", "{q(r["desc"])}" }},\n'
                    )
                out.write("\t\t},\n")
            out.write("\t},\n")
        out.write("}\n\n")

        out.write("local function buildRows(sourceRows)\n")
        out.write("\tlocal out = {}\n")
        out.write("\tfor i = 1, #sourceRows do\n")
        out.write("\t\tlocal r = sourceRows[i]\n")
        out.write("\t\tout[i] = { r[1], r[2], r[3], r[4] }\n")
        out.write("\tend\n")
        out.write("\treturn out\n")
        out.write("end\n\n")

        out.write("local function addUnifiedPage(specKey)\n")
        out.write("\tlocal spec = data[specKey]\n")
        out.write("\tif not spec or not spec.items or spec.__tbcaBisUnifiedPage then\n")
        out.write("\t\treturn\n")
        out.write("\tend\n")
        out.write("\tlocal phaseRows = SPEC_ROWS[specKey]\n")
        out.write("\tif not phaseRows then\n")
        out.write("\t\treturn\n")
        out.write("\tend\n")
        out.write("\tspec.__tbcaBisUnifiedPage = true\n\n")
        out.write("\tlocal entry = {\n")
        out.write('\t\tname = "Gems / Enchants / Attachments",\n')
        out.write("\t\tTableType = MIX_ITTYPE,\n")
        out.write("\t}\n\n")
        out.write("\tfor phase = 1, 5 do\n")
        out.write("\t\tlocal rows = phaseRows[phase]\n")
        out.write("\t\tif rows and #rows > 0 then\n")
        out.write("\t\t\tentry[phases[phase]] = buildRows(rows)\n")
        out.write("\t\tend\n")
        out.write("\tend\n\n")
        out.write("\ttable.insert(spec.items, 1, entry)\n")
        out.write("end\n\n")

        out.write("for specKey in pairs(SPEC_ROWS) do\n")
        out.write("\taddUnifiedPage(specKey)\n")
        out.write("end\n")


if __name__ == "__main__":
    main()
