#!/usr/bin/env python3
"""Extract color values from iOS Asset Catalogs into a machine-readable JSON file.

Scans two asset catalogs:
  - iosApp/iosApp/Assets.xcassets/Colors/**        (11 themes x 9 colors)
  - iosApp/HoneyDue/Assets.xcassets/**             (widget accent + bg)

Each color Contents.json defines up to two `colors` entries:
  - one universal (light-mode default)
  - one with appearances=[luminosity:dark]

Component values may be:
  - float strings "0.000" .. "1.000"  -> multiply by 255 and round
  - hex strings "0xHH"                -> parse as int

sRGB-only. If any display-p3 entry is encountered it is recorded
separately (at the top level) so Android implementers can decide how to
handle them; values are otherwise passed through as-is.

Output schema (see plan in rc-android-ios-parity.md):
{
  "displayP3_themes": [ ... optional ... ],
  "themes": { <ThemeName>: { <ColorName>: { "light": "#RRGGBB", "dark": "#RRGGBB" } } },
  "widget": { <ColorName>: { "light": "#RRGGBB", "dark": "#RRGGBB" } }
}
"""
from __future__ import annotations

import json
import os
import sys
from pathlib import Path
from typing import Any

REPO_ROOT = Path(__file__).resolve().parent.parent
COLOR_ROOT = REPO_ROOT / "iosApp" / "iosApp" / "Assets.xcassets" / "Colors"
WIDGET_ROOT = REPO_ROOT / "iosApp" / "HoneyDue" / "Assets.xcassets"
OUT_PATH = REPO_ROOT / "docs" / "ios-parity" / "colors.json"

EXPECTED_THEME_COUNT = 11
EXPECTED_COLORS_PER_THEME = 9
EXPECTED_COLOR_NAMES = {
    "Primary",
    "Secondary",
    "Accent",
    "Error",
    "BackgroundPrimary",
    "BackgroundSecondary",
    "TextPrimary",
    "TextSecondary",
    "TextOnPrimary",
}


def component_to_int(value: str | float | int) -> int:
    """Convert a color component (hex string, float-as-string, or numeric) to 0..255."""
    if isinstance(value, (int, float)):
        if 0.0 <= float(value) <= 1.0 and not (isinstance(value, int) and value > 1):
            return round(float(value) * 255)
        return int(value)
    s = str(value).strip()
    if s.lower().startswith("0x"):
        return int(s, 16)
    # float like "0.478" or "1.000"
    f = float(s)
    if 0.0 <= f <= 1.0:
        return round(f * 255)
    return int(f)


def hex_string(r: int, g: int, b: int, a: float) -> str:
    if abs(a - 1.0) < 1e-6:
        return f"#{r:02X}{g:02X}{b:02X}"
    a_int = round(a * 255) if 0.0 <= a <= 1.0 else int(a)
    return f"#{r:02X}{g:02X}{b:02X}{a_int:02X}"


def alpha_value(value: Any) -> float:
    if isinstance(value, (int, float)):
        return float(value)
    s = str(value).strip()
    if s.lower().startswith("0x"):
        return int(s, 16) / 255.0
    return float(s)


def parse_colorset(contents_path: Path) -> tuple[str | None, str | None, str | None]:
    """Return (light_hex, dark_hex, color_space) for a .colorset/Contents.json.

    Returns (None, None, None) if the colorset has no color data (e.g. Xcode
    placeholder `AccentColor` with only idiom but no components).
    """
    with contents_path.open("r", encoding="utf-8") as f:
        data = json.load(f)

    light = None
    dark = None
    color_space = None

    for entry in data.get("colors", []):
        color = entry.get("color")
        if not color:
            continue
        components = color.get("components") or {}
        if not components:
            continue
        color_space = color.get("color-space") or color_space
        r = component_to_int(components.get("red", 0))
        g = component_to_int(components.get("green", 0))
        b = component_to_int(components.get("blue", 0))
        a = alpha_value(components.get("alpha", 1.0))
        hex_str = hex_string(r, g, b, a)

        appearances = entry.get("appearances") or []
        is_dark = any(
            a.get("appearance") == "luminosity" and a.get("value") == "dark"
            for a in appearances
        )
        if is_dark:
            dark = hex_str
        else:
            light = hex_str

    if light is None and dark is None:
        return None, None, None
    # If one variant is missing, mirror it
    if light is None:
        light = dark
    if dark is None:
        dark = light
    return light, dark, color_space


def extract_theme_colors() -> tuple[dict[str, dict[str, dict[str, str]]], set[str]]:
    themes: dict[str, dict[str, dict[str, str]]] = {}
    display_p3_themes: set[str] = set()

    if not COLOR_ROOT.is_dir():
        raise SystemExit(f"color root not found: {COLOR_ROOT}")

    for theme_dir in sorted(COLOR_ROOT.iterdir()):
        if not theme_dir.is_dir():
            continue
        theme_name = theme_dir.name
        theme_colors: dict[str, dict[str, str]] = {}

        for colorset_dir in sorted(theme_dir.iterdir()):
            if not colorset_dir.is_dir() or not colorset_dir.name.endswith(".colorset"):
                continue
            color_name = colorset_dir.name[: -len(".colorset")]
            contents_path = colorset_dir / "Contents.json"
            if not contents_path.is_file():
                continue
            light, dark, cs = parse_colorset(contents_path)
            if light is None:
                continue
            theme_colors[color_name] = {"light": light, "dark": dark}
            if cs and "display-p3" in cs.lower():
                display_p3_themes.add(theme_name)

        if theme_colors:
            themes[theme_name] = theme_colors

    return themes, display_p3_themes


def extract_widget_colors() -> dict[str, dict[str, str]]:
    widget: dict[str, dict[str, str]] = {}
    if not WIDGET_ROOT.is_dir():
        print(f"[warn] widget asset root missing: {WIDGET_ROOT}", file=sys.stderr)
        return widget
    for entry in sorted(WIDGET_ROOT.iterdir()):
        if not entry.is_dir() or not entry.name.endswith(".colorset"):
            continue
        color_name = entry.name[: -len(".colorset")]
        contents_path = entry / "Contents.json"
        if not contents_path.is_file():
            continue
        light, dark, _ = parse_colorset(contents_path)
        if light is None:
            # Asset catalog placeholder with no concrete color; skip.
            continue
        widget[color_name] = {"light": light, "dark": dark}
    return widget


def main() -> int:
    themes, display_p3 = extract_theme_colors()
    widget = extract_widget_colors()

    # Validation
    errors: list[str] = []
    if len(themes) != EXPECTED_THEME_COUNT:
        errors.append(
            f"Expected {EXPECTED_THEME_COUNT} themes, got {len(themes)}: "
            f"{sorted(themes.keys())}"
        )
    for name, colors in sorted(themes.items()):
        if len(colors) != EXPECTED_COLORS_PER_THEME:
            errors.append(
                f"Theme {name!r} has {len(colors)} colors, expected "
                f"{EXPECTED_COLORS_PER_THEME}: {sorted(colors.keys())}"
            )
        missing = EXPECTED_COLOR_NAMES - set(colors.keys())
        extra = set(colors.keys()) - EXPECTED_COLOR_NAMES
        if missing:
            errors.append(f"Theme {name!r} missing colors: {sorted(missing)}")
        if extra:
            errors.append(f"Theme {name!r} has unexpected colors: {sorted(extra)}")

    if errors:
        print("[extract_ios_colors] VALIDATION FAILED:", file=sys.stderr)
        for e in errors:
            print(f"  - {e}", file=sys.stderr)
        return 1

    output: dict[str, Any] = {}
    if display_p3:
        output["displayP3_themes"] = sorted(display_p3)
    # Preserve key order: Primary, Secondary, Accent, Error, Backgrounds, Text
    color_order = [
        "Primary",
        "Secondary",
        "Accent",
        "Error",
        "BackgroundPrimary",
        "BackgroundSecondary",
        "TextPrimary",
        "TextSecondary",
        "TextOnPrimary",
    ]
    ordered_themes: dict[str, dict[str, dict[str, str]]] = {}
    # Put Default first if present
    theme_order = sorted(themes.keys(), key=lambda n: (n != "Default", n))
    for theme_name in theme_order:
        ordered_themes[theme_name] = {
            cname: themes[theme_name][cname]
            for cname in color_order
            if cname in themes[theme_name]
        }
    output["themes"] = ordered_themes
    output["widget"] = widget

    OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
    with OUT_PATH.open("w", encoding="utf-8") as f:
        json.dump(output, f, indent=2)
        f.write("\n")

    print(f"[extract_ios_colors] wrote {OUT_PATH}")
    print(
        f"  themes={len(ordered_themes)}  "
        f"colors/theme={EXPECTED_COLORS_PER_THEME}  "
        f"widget_entries={len(widget)}  "
        f"displayP3_themes={sorted(display_p3) if display_p3 else 'none'}"
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
