from __future__ import annotations

import json
import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent
DESKTOP_TEMPLATE = PROJECT_ROOT / "packaging" / "codex-account-switcher.desktop"
DESKTOP_FILENAME = "codex-account-switcher.desktop"
DESKTOP_ENTRYPOINT_PLACEHOLDER = "__SYSTRAY_CDX_SWITCHER__"
ICON_FILENAME = "codex-account-switcher.svg"
ICON_TEMPLATE = PROJECT_ROOT / "icons" / ICON_FILENAME
ICON_WARNING_FILENAME = "codex-account-switcher-warning.svg"
ICON_WARNING_TEMPLATE = PROJECT_ROOT / "icons" / ICON_WARNING_FILENAME
DEFAULT_DEST_BIN = Path.home() / ".local" / "bin"
DEFAULT_DEST_APPS = Path.home() / ".local" / "share" / "applications"
DEFAULT_DEST_ICONS = (
    Path.home() / ".local" / "share" / "icons" / "hicolor" / "scalable" / "apps"
)
_BIN_SCRIPTS = (
    "account_lock_cli.py",
    "cdx.py",
    "cld.py",
    "claudex.py",
    "gateway_account_cli.py",
    "systray_codex_switcher.py",
    "statusline.py",
)
_COMMAND_LINKS = (
    ("account_lock_cli.py", "account-lock"),
    ("cld.py", "cld"),
    ("claudex.py", "claudex"),
    ("gateway_account_cli.py", "systray-gateway"),
)


def _legacy_cld_wrapper() -> str:
    source = Path.home() / "Projects" / "overdeck" / "modules" / "systray" / "cld.py"
    return f'#!/usr/bin/env bash\nexec python3 {source} "$@"\n'


@dataclass(frozen=True, slots=True)
class InstallReport:
    created: tuple[Path, ...]
    existing: tuple[Path, ...]


def install(
    dest_bin: Path = DEFAULT_DEST_BIN,
    dest_apps: Path = DEFAULT_DEST_APPS,
    dest_icons: Path = DEFAULT_DEST_ICONS,
) -> InstallReport:
    dest_bin = Path(dest_bin)
    dest_apps = Path(dest_apps)
    dest_icons = Path(dest_icons)
    created: list[Path] = []
    existing: list[Path] = []
    for filename in _BIN_SCRIPTS:
        target = dest_bin / filename
        if _ensure_symlink(PROJECT_ROOT / filename, target):
            created.append(target)
        else:
            existing.append(target)
    for source_name, target_name in _COMMAND_LINKS:
        target = dest_bin / target_name
        if target_name == "cld" and target.is_file() and not target.is_symlink():
            try:
                legacy = target.read_text(encoding="utf-8")
            except (OSError, UnicodeDecodeError):
                legacy = ""
            if legacy == _legacy_cld_wrapper():
                target.unlink()
        if _ensure_symlink(PROJECT_ROOT / source_name, target):
            created.append(target)
        else:
            existing.append(target)
    desktop_target = dest_apps / DESKTOP_FILENAME
    if dest_apps.exists():
        if not dest_apps.is_dir():
            raise NotADirectoryError(dest_apps)
        if _ensure_desktop_entry(desktop_target, dest_bin):
            created.append(desktop_target)
        else:
            existing.append(desktop_target)
    if dest_icons.exists() and not dest_icons.is_dir():
        raise NotADirectoryError(dest_icons)
    icon_target = dest_icons / ICON_FILENAME
    _ensure_copied_file(ICON_TEMPLATE, icon_target)
    _ensure_copied_file(ICON_WARNING_TEMPLATE, dest_icons / ICON_WARNING_FILENAME)
    _refresh_icon_cache(dest_icons)
    return InstallReport(created=tuple(created), existing=tuple(existing))


def uninstall(
    dest_bin: Path = DEFAULT_DEST_BIN,
    dest_apps: Path = DEFAULT_DEST_APPS,
    dest_icons: Path = DEFAULT_DEST_ICONS,
) -> None:
    dest_bin = Path(dest_bin)
    dest_apps = Path(dest_apps)
    dest_icons = Path(dest_icons)
    for filename in _BIN_SCRIPTS:
        _remove_symlink(PROJECT_ROOT / filename, dest_bin / filename)
    for source_name, target_name in _COMMAND_LINKS:
        _remove_symlink(PROJECT_ROOT / source_name, dest_bin / target_name)
    if dest_apps.exists():
        if not dest_apps.is_dir():
            raise NotADirectoryError(dest_apps)
        _remove_desktop_entry(dest_apps / DESKTOP_FILENAME)
    if dest_icons.exists():
        if not dest_icons.is_dir():
            raise NotADirectoryError(dest_icons)
        _remove_copied_file(ICON_TEMPLATE, dest_icons / ICON_FILENAME)
        _remove_copied_file(ICON_WARNING_TEMPLATE, dest_icons / ICON_WARNING_FILENAME)


def _ensure_symlink(source: Path, target: Path) -> bool:
    source = Path(source)
    target = Path(target)
    if not source.exists():
        raise FileNotFoundError(source)

    target.parent.mkdir(parents=True, exist_ok=True)

    if target.is_symlink():
        if target.resolve(strict=False) == source.resolve(strict=False):
            return False
        if not _is_managed_systray_source(target.resolve(strict=False), source.name):
            raise FileExistsError(target)
    elif target.exists():
        raise FileExistsError(target)

    temp_path = target.parent / f".{target.name}.tmp-{os.getpid()}"
    if temp_path.exists() or temp_path.is_symlink():
        temp_path.unlink()
    temp_path.symlink_to(source)
    os.replace(temp_path, target)
    return True


def _is_managed_systray_source(source: Path, filename: str) -> bool:
    if source.name != filename or not source.is_file():
        return False
    try:
        manifest = json.loads(
            (source.parent / "deck.module.json").read_text(encoding="utf-8")
        )
    except (OSError, json.JSONDecodeError):
        return False
    return isinstance(manifest, dict) and manifest.get("name") == "systray"


def _remove_symlink(source: Path, target: Path) -> None:
    source = Path(source)
    target = Path(target)
    if not target.is_symlink():
        return
    if target.resolve(strict=False) != source.resolve(strict=False):
        return
    target.unlink()


def _ensure_desktop_entry(target: Path, dest_bin: Path) -> bool:
    target = Path(target)
    dest_bin = Path(dest_bin)
    if not DESKTOP_TEMPLATE.exists():
        raise FileNotFoundError(DESKTOP_TEMPLATE)

    template_text = DESKTOP_TEMPLATE.read_text(encoding="utf-8")
    rendered = template_text.replace(
        DESKTOP_ENTRYPOINT_PLACEHOLDER,
        str(dest_bin / "systray_codex_switcher.py"),
    )
    if rendered == template_text:
        raise ValueError(f"missing placeholder in {DESKTOP_TEMPLATE}")

    if target.exists():
        if target.read_text(encoding="utf-8") == rendered:
            return False
        raise FileExistsError(target)

    temp_path = target.parent / f".{target.name}.tmp-{os.getpid()}"
    if temp_path.exists() or temp_path.is_symlink():
        temp_path.unlink()
    temp_path.write_text(rendered, encoding="utf-8")
    os.replace(temp_path, target)
    return True


def _ensure_copied_file(source: Path, target: Path) -> bool:
    source = Path(source)
    target = Path(target)
    if not source.exists():
        raise FileNotFoundError(source)

    target.parent.mkdir(parents=True, exist_ok=True)

    source_bytes = source.read_bytes()
    if target.exists() or target.is_symlink():
        if target.is_file() and not target.is_symlink() and target.read_bytes() == source_bytes:
            return False
        raise FileExistsError(target)

    temp_path = target.parent / f".{target.name}.tmp-{os.getpid()}"
    if temp_path.exists() or temp_path.is_symlink():
        temp_path.unlink()
    temp_path.write_bytes(source_bytes)
    os.replace(temp_path, target)
    return True


def _remove_desktop_entry(target: Path) -> None:
    target = Path(target)
    if target.exists():
        target.unlink()


def _remove_copied_file(source: Path, target: Path) -> None:
    source = Path(source)
    target = Path(target)
    if not target.exists() or target.is_symlink():
        return
    if target.read_bytes() != source.read_bytes():
        return
    target.unlink()


def _refresh_icon_cache(dest_icons: Path) -> None:
    icon_cache = shutil.which("gtk-update-icon-cache")
    if not icon_cache:
        return
    theme_root = Path(dest_icons).parent.parent.parent
    try:
        subprocess.run(
            [icon_cache, "-f", str(theme_root)],
            check=False,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
    except (OSError, subprocess.SubprocessError):
        return


if __name__ == "__main__":
    install()
