# BEGIN CHANGE: extract outfit PNGs via open-tibia-library (allowlist) + Playwright
"""
Extract outfit PNG packs (outfits_anim/) from Tibia.spr + Tibia.dat using Gesior OTL
(same engine as outfitImageGenerator.html / outfit-images.ots.me), automated with Playwright.

Setup once:
  git clone https://github.com/gesior/open-tibia-library tools/open-tibia-library
  cd tools/open-tibia-library && npm install
  cd .. && python apply_otl_outfit_patch.py --build
  pip install playwright && playwright install chromium

Styller port (new + fill, 526 IDs):
  python extract_outfit_pngs.py --styller-port-json P:/OT/OTSERVER/pedro/otx2_extended/LOOKTYPES_STYLLER_PORT.json

Custom ID list:
  python extract_outfit_pngs.py --ids-file my.json --ids 128,2500-2510

Output: tools/outfit-images-out/download/outfits.zip -> unzip to outfits_anim/
"""
from __future__ import annotations

import argparse
import json
import os
import shutil
import sys
import time
import zipfile
from pathlib import Path

REPO_HTML = Path(os.environ.get("WA_HTML_REPO", Path(__file__).resolve().parent.parent))
PEDRO = REPO_HTML.parent
OTL = REPO_HTML / "tools" / "open-tibia-library"
OUT = REPO_HTML / "tools" / "outfit-images-out"

SPR = PEDRO / "WhiteAntidoteCustomClient" / "Tibia.spr"
DAT = PEDRO / "WhiteAntidoteCustomClient" / "Tibia.dat"

if not SPR.is_file():
    SPR = REPO_HTML / "files-extended" / "Tibia.spr"
if not DAT.is_file():
    DAT = REPO_HTML / "files-extended" / "Tibia.dat"
if not SPR.is_file():
    SPR = Path(os.environ.get("WA_HTML_ROOT", "/home/ubuntu/portal/assets")) / "files-extended" / "Tibia.spr"
if not DAT.is_file():
    DAT = Path(os.environ.get("WA_HTML_ROOT", "/home/ubuntu/portal/assets")) / "files-extended" / "Tibia.dat"

DEFAULT_STYLLER_JSON = PEDRO / "otx2_extended" / "LOOKTYPES_STYLLER_PORT.json"


def parse_id_list(spec: str) -> list[int]:
    ids: set[int] = set()
    for part in spec.split(","):
        part = part.strip()
        if not part:
            continue
        if "-" in part:
            a_s, b_s = part.split("-", 1)
            a, b = int(a_s.strip()), int(b_s.strip())
            if b < a:
                a, b = b, a
            ids.update(range(a, b + 1))
        else:
            ids.add(int(part))
    return sorted(ids)


def load_ids_file(path: Path) -> list[int]:
    raw = path.read_text(encoding="utf-8")
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return parse_id_list(raw.replace("\n", ","))
    ids: set[int] = set()
    if isinstance(data, list):
        for x in data:
            ids.add(int(x))
        return sorted(ids)
    if isinstance(data, dict):
        for key in ("new", "fill"):
            if key in data and isinstance(data[key], list):
                for x in data[key]:
                    ids.add(int(x))
        if ids:
            return sorted(ids)
        for v in data.values():
            if isinstance(v, list):
                for x in v:
                    if isinstance(x, int) or (isinstance(x, str) and str(x).isdigit()):
                        ids.add(int(x))
    return sorted(ids)


def load_styller_port_json(path: Path, *, new_only: bool, fill_only: bool) -> list[int]:
    data = json.loads(path.read_text(encoding="utf-8"))
    ids: set[int] = set()
    if new_only:
        for x in data.get("new", []):
            ids.add(int(x))
    elif fill_only:
        for x in data.get("fill", []):
            ids.add(int(x))
    else:
        for x in data.get("new", []):
            ids.add(int(x))
        for x in data.get("fill", []):
            ids.add(int(x))
    return sorted(ids)


def unzip_outfits_anim(zip_path: Path, dest: Path) -> int:
    dest.mkdir(parents=True, exist_ok=True)
    folders: set[str] = set()
    with zipfile.ZipFile(zip_path, "r") as zf:
        for name in zf.namelist():
            if not name.startswith("outfits_anim/"):
                continue
            parts = name.split("/")
            if len(parts) < 3 or not parts[1].isdigit():
                continue
            if name.endswith("/"):
                continue
            folders.add(parts[1])
            target = dest / parts[1] / "/".join(parts[2:])
            target.parent.mkdir(parents=True, exist_ok=True)
            with zf.open(name) as src, open(target, "wb") as out:
                shutil.copyfileobj(src, out)
    return len(folders)


def run_generator(
    only_ids: list[int] | None,
    *,
    spr_path: Path,
    dat_path: Path,
    client_version: str = "860",
    u32: bool = True,
    alpha: bool = False,
    enhanced: bool = False,
    idle_groups: bool = False,
    idle_animation: bool = True,
) -> Path:
    from playwright.sync_api import sync_playwright

    html = (OTL / "outfitImageGenerator.html").resolve()
    js = OTL / "js" / "outfitImageGenerator.js"
    if not js.is_file():
        raise SystemExit(
            "OTL not built. Run: python tools/apply_otl_outfit_patch.py --build"
        )
    for p, label in ((spr_path, "SPR"), (dat_path, "DAT")):
        if not p.is_file():
            raise SystemExit(f"Missing {label}: {p}")

    OUT.mkdir(parents=True, exist_ok=True)
    download_dir = OUT / "download"
    if download_dir.exists():
        shutil.rmtree(download_dir)
    download_dir.mkdir(parents=True)

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(accept_downloads=True)
        page = context.new_page()
        page.goto(html.as_uri())
        page.fill("#clientversion", client_version)
        page.set_input_files("#spr", str(spr_path.resolve()))
        page.set_input_files("#dat", str(dat_path.resolve()))

        def set_check(sel: str, on: bool) -> None:
            try:
                if on:
                    page.check(sel)
                else:
                    page.uncheck(sel)
            except Exception:
                pass

        set_check("#forceEnableExtendedSprites", u32)
        set_check("#enableTransparency", alpha)
        set_check("#enableEnhancedAnimations", enhanced)
        set_check("#enableIdleAnimations", idle_groups)
        set_check("#idleAnimation", idle_animation)

        print(
            f"Flags: u32={u32} alpha={alpha} enhanced={enhanced} "
            f"idle_groups={idle_groups} idle_anim={idle_animation} "
            f"ids={len(only_ids) if only_ids else 'all'}",
            flush=True,
        )
        print("Loading SPR/DAT (can take several minutes)...", flush=True)
        page.click("#loadFiles")
        page.wait_for_function(
            """() => {
              const t = (document.getElementById('progressBar')?.innerText || '').toLowerCase();
              return t.includes('data loaded') || t.includes('failed to load');
            }""",
            timeout=900_000,
        )
        time.sleep(1)
        progress = page.locator("#progressBar").inner_text()
        print("Progress after load:", progress, flush=True)
        if "failed to load" in progress.lower():
            raise RuntimeError(progress)

        if only_ids:
            page.evaluate(
                """(ids) => {
                  const gen = window.__outfitImageGenerator;
                  if (!gen || typeof gen.setAllowedOutfitIds !== 'function') {
                    throw new Error('__outfitImageGenerator allowlist missing; run apply_otl_outfit_patch.py --build');
                  }
                  gen.setAllowedOutfitIds(ids);
                }""",
                only_ids,
            )

        print("Generating outfit PNGs...", flush=True)
        with page.expect_download(timeout=7_200_000) as dl_info:
            page.click("#generateImages")
            last = ""
            for _ in range(7200):
                txt = page.locator("#progressBar").inner_text()
                if txt != last:
                    print(txt, flush=True)
                    last = txt
                low = txt.lower()
                if "error" in low and "failed" in low:
                    raise RuntimeError(txt)
                if "download now" in low or "zip generated" in low:
                    break
                time.sleep(2)

        download = dl_info.value
        zip_path = download_dir / "outfits.zip"
        download.save_as(str(zip_path))
        print("Saved", zip_path, "size", zip_path.stat().st_size, flush=True)
        browser.close()
        return zip_path


def main() -> int:
    try:
        sys.stdout.reconfigure(line_buffering=True)  # type: ignore[attr-defined]
    except Exception:
        pass

    ap = argparse.ArgumentParser(description="Extract outfits_anim PNG pack via OTL + Playwright.")
    ap.add_argument("--styller-port-json", type=Path, default=None, help="LOOKTYPES_STYLLER_PORT.json (new+fill)")
    ap.add_argument("--new-only", action="store_true", help="With --styller-port-json: only JSON new[]")
    ap.add_argument("--fill-only", action="store_true", help="With --styller-port-json: only JSON fill[]")
    ap.add_argument("--ids-file", type=Path, default=None, help="JSON with new/fill arrays or id list")
    ap.add_argument("--ids", type=str, default="", help="Comma IDs/ranges e.g. 128,2500-2510")
    ap.add_argument("--from", dest="id_from", type=int, default=None)
    ap.add_argument("--to", dest="id_to", type=int, default=None)
    ap.add_argument("--unzip-to", type=Path, default=None, help="Extract outfits_anim/ from ZIP to this dir")
    ap.add_argument("--spr", type=Path, default=None, help="Tibia.spr path (default: files-extended)")
    ap.add_argument("--dat", type=Path, default=None, help="Tibia.dat path (default: files-extended)")
    ap.add_argument("--client-version", type=str, default="860", help="OTL client version field (860 or 1099)")
    ap.add_argument("--skip-generate", action="store_true")
    ap.add_argument("--no-u32", action="store_true")
    ap.add_argument("--alpha", action="store_true", help="Enable transparency (usually OFF for WA extended)")
    ap.add_argument("--enhanced", action="store_true", help="GameEnhancedAnimations (breaks WA extended DAT load)")
    ap.add_argument("--idle-groups", action="store_true", dest="idle_groups", help="GameIdleAnimations (breaks WA extended DAT load)")
    ap.add_argument("--no-idle-animation", action="store_true")
    args = ap.parse_args()

    only_ids: list[int] | None = None

    if args.styller_port_json is not None:
        path = args.styller_port_json
        if not path.is_file():
            print("styller-port-json not found:", path)
            return 1
        only_ids = load_styller_port_json(path, new_only=args.new_only, fill_only=args.fill_only)
    else:
        id_parts: list[str] = []
        file_ids: list[int] = []
        if args.ids_file is not None:
            if not args.ids_file.is_file():
                print("ids-file not found:", args.ids_file)
                return 1
            file_ids = load_ids_file(args.ids_file)
        if args.ids.strip():
            id_parts.append(args.ids.strip())
        if args.id_from is not None or args.id_to is not None:
            if args.id_from is None or args.id_to is None:
                print("Use both --from and --to")
                return 1
            id_parts.append(f"{args.id_from}-{args.id_to}")
        if id_parts:
            only_ids = parse_id_list(",".join(id_parts))
        if file_ids:
            only_ids = sorted(set((only_ids or []) + file_ids))

    if only_ids is not None:
        print(f"Target looktypes: {len(only_ids)} ({only_ids[0]} .. {only_ids[-1]})", flush=True)
    elif DEFAULT_STYLLER_JSON.is_file():
        print("Hint: pass --styller-port-json for Styller subset (526 IDs)", flush=True)

    zip_path: Path | None = None
    spr_path = args.spr.resolve() if args.spr else SPR.resolve()
    dat_path = args.dat.resolve() if args.dat else DAT.resolve()
    if not args.skip_generate:
        zip_path = run_generator(
            only_ids,
            spr_path=spr_path,
            dat_path=dat_path,
            client_version=args.client_version,
            u32=not args.no_u32,
            alpha=bool(args.alpha),
            enhanced=bool(args.enhanced),
            idle_groups=bool(args.idle_groups),
            idle_animation=not args.no_idle_animation,
        )
    else:
        zip_path = OUT / "download" / "outfits.zip"
        if not zip_path.is_file():
            print("No ZIP to unzip")
            return 1

    dest = args.unzip_to or (OUT / "outfits_anim")
    count = unzip_outfits_anim(zip_path, dest)
    print(f"Extracted {count} looktype folders -> {dest}")
    print("Next: merge to portal/assets + www/html animatedOutfits1092/outfits_anim; cacheGenerator.php both")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
# END CHANGE
