"""Composite app icons onto the NexusOS nexus underlay tile."""
from pathlib import Path
import subprocess
import tempfile
import os
import re

from ..nexus_config import settings

_ROOT = settings.project_root
UNDERLAY_FILE = settings.assets_dir / "themes/NexusOS-icons-src/nexus-underlay.svg"
RING_FILE = settings.assets_dir / "themes/NexusOS-icons-src/nexus-underlay-ring.svg"
ICONS_OUT = Path.home() / ".icons" / "NexusOS"
SIZES = [16, 22, 24, 32, 48, 64, 128]

_ALLOWED_ROOTS = [
    "/usr/share/icons",
    "/usr/share/pixmaps",
    "/usr/local/share/icons",
    "/opt",
    str(Path.home() / ".local/share/icons"),
    str(Path.home() / ".icons"),
    str(settings.assets_dir),
]

_UNDERLAY_FALLBACK = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
  <defs>
    <radialGradient id="frame" cx="50%" cy="30%" r="70%">
      <stop offset="0%" stop-color="#b040c0"/>
      <stop offset="100%" stop-color="#4a0050"/>
    </radialGradient>
  </defs>
  <rect x="0"  y="0"  width="128" height="128" rx="30" fill="url(#frame)"/>
  <rect x="8"  y="8"  width="112" height="112" rx="26" fill="#120018"/>
  <rect x="16" y="16" width="96"  height="96"  rx="18" fill="#262626"/>
</svg>
"""

_RING_FALLBACK = """\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
  <defs>
    <radialGradient id="ring" cx="50%" cy="30%" r="70%">
      <stop offset="0%" stop-color="#8cc63f"/>
      <stop offset="100%" stop-color="#5a9020"/>
    </radialGradient>
    <radialGradient id="frame" cx="50%" cy="30%" r="70%">
      <stop offset="0%" stop-color="#b040c0"/>
      <stop offset="100%" stop-color="#4a0050"/>
    </radialGradient>
  </defs>
  <rect x="0"  y="0"  width="128" height="128" rx="30" fill="url(#ring)"/>
  <rect x="8"  y="8"  width="112" height="112" rx="26" fill="url(#frame)"/>
  <rect x="12" y="12" width="104" height="104" rx="22" fill="#120018"/>
  <rect x="18" y="18" width="92"  height="92"  rx="16" fill="#262626"/>
</svg>
"""


def _load_underlay(nexus_ring: bool = False) -> str:
    """Return SVG text for the underlay, falling back to inline if file missing."""
    target = RING_FILE if nexus_ring else UNDERLAY_FILE
    fallback = _RING_FALLBACK if nexus_ring else _UNDERLAY_FALLBACK
    if target.exists():
        return target.read_text()
    return fallback


def _inkscape_render(svg_path: str, out_path: str, size: int) -> None:
    subprocess.run(
        [
            "inkscape", svg_path,
            "--export-type=png",
            f"--export-filename={out_path}",
            f"--export-width={size}",
            f"--export-height={size}",
        ],
        check=True,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )


def _composite_png(underlay_png: str, app_png: str, out_path: str, frac: float, size: int) -> None:
    """Composite app icon centred on the underlay at given fractional size."""
    app_size = max(1, int(size * frac))
    offset = (size - app_size) // 2
    subprocess.run(
        [
            "convert",
            underlay_png,
            "(", app_png, "-resize", f"{app_size}x{app_size}", ")",
            "-gravity", "Center",
            "-geometry", f"+0+0",
            "-composite",
            out_path,
        ],
        check=True,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )


def _is_allowed_path(path: str) -> bool:
    p = Path(os.path.realpath(path))
    return p.is_file() and any(
        p == root or root in p.parents
        for root in (Path(r).resolve() for r in _ALLOWED_ROOTS)
    )


def _safe_component(value: str, label: str) -> str:
    if not value or not re.fullmatch(r"[A-Za-z0-9._-]+", value):
        raise ValueError(f"Invalid icon {label}")
    return value


def brand_icon(
    src_path: str,
    output_name: str,
    frac: float = 0.60,
    round_mask: bool = False,
    nexus_ring: bool = False,
    reload: bool = True,
    category: str = "apps",
) -> None:
    """Brand a single icon and write PNGs to the NexusOS icon theme at all sizes."""
    if not _is_allowed_path(src_path):
        raise ValueError(f"Icon source path not in allowed roots: {src_path}")
    output_name = _safe_component(output_name, "name")
    category = _safe_component(category, "category")

    underlay_svg_text = _load_underlay(nexus_ring)

    with tempfile.TemporaryDirectory() as tmp:
        tmp_path = Path(tmp)

        underlay_svg = tmp_path / "underlay.svg"
        underlay_svg.write_text(underlay_svg_text)

        for size in SIZES:
            out_dir = ICONS_OUT / f"{size}x{size}" / category
            out_dir.mkdir(parents=True, exist_ok=True)
            out_path = out_dir / f"{output_name}.png"

            # Break symlinks before writing; also remove any same-name .svg so
            # GTK doesn't prefer the old SVG over our new branded PNG.
            if out_path.is_symlink() or out_path.exists():
                out_path.unlink()
            svg_path = out_dir / f"{output_name}.svg"
            if svg_path.exists() or svg_path.is_symlink():
                svg_path.unlink()

            underlay_png = str(tmp_path / f"underlay_{size}.png")
            _inkscape_render(str(underlay_svg), underlay_png, size)

            # Resize app icon source to a temp PNG for compositing.
            # `-background none` is REQUIRED: ImageMagick rasterizes transparent
            # SVGs onto an opaque WHITE canvas by default, which shows up as a
            # white plate/border behind logos with transparent corners
            # (e.g. VSCode, Edge). It must precede the input to affect the SVG.
            app_png = str(tmp_path / f"app_{size}.png")
            app_size = max(1, int(size * frac))
            subprocess.run(
                ["convert", "-background", "none", src_path,
                 "-resize", f"{app_size}x{app_size}", app_png],
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )

            subprocess.run(
                [
                    "convert", underlay_png,
                    "(", app_png, ")",
                    "-gravity", "Center",
                    "-composite",
                    str(out_path),
                ],
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )

    if reload:
        apply_icon_cache()


def apply_icon_cache() -> None:
    """Rebuild the GTK icon cache and reload the panel."""
    import pwd
    uid = os.getuid()
    # systemd user session bus — needed for xfce4-panel -r to reach the running session
    dbus_addr = os.environ.get(
        "DBUS_SESSION_BUS_ADDRESS",
        f"unix:path=/run/user/{uid}/bus",
    )
    _env = {
        **os.environ,
        "DISPLAY": os.environ.get("DISPLAY", ":0"),
        "DBUS_SESSION_BUS_ADDRESS": dbus_addr,
        "HOME": pwd.getpwuid(uid).pw_dir,
    }

    subprocess.run(
        ["gtk-update-icon-cache", "-f", str(ICONS_OUT)],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )

    # Force every running GTK app (whisker menu, panel, Thunar) to drop its
    # in-memory icon cache. A panel reload alone does NOT do this — GTK only
    # re-resolves icons when the icon-theme NAME changes. Toggling to another
    # theme and back fires the "theme-changed" signal that triggers the reload.
    import time
    current = "NexusOS"
    try:
        out = subprocess.run(
            ["xfconf-query", "-c", "xsettings", "-p", "/Net/IconThemeName"],
            env=_env, capture_output=True, text=True,
        )
        if out.stdout.strip():
            current = out.stdout.strip()
    except Exception:
        pass
    alt = "Papirus-Dark" if current != "Papirus-Dark" else "Adwaita"
    subprocess.run(
        ["xfconf-query", "-c", "xsettings", "-p", "/Net/IconThemeName", "-s", alt],
        env=_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
    )
    time.sleep(0.5)
    subprocess.run(
        ["xfconf-query", "-c", "xsettings", "-p", "/Net/IconThemeName", "-s", current],
        env=_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
    )

    # Restart Plank so it re-resolves icons from the updated theme
    subprocess.run(["pkill", "plank"], env=_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    subprocess.Popen(
        ["plank"],
        env=_env,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
