"""Persistent pseudonymous identity for MPC Tray Watcher update checks."""

from __future__ import annotations

import os
import uuid
from datetime import datetime
from pathlib import Path
from typing import Callable
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit


APP_VENDOR = "AmrumSoftware"
APP_PRODUCT = "MPCTrayWatcher"
INSTALLATION_ID_FILENAME = "installation-id.txt"
REGISTRY_SUBKEY = r"Software\AmrumSoftware\MPCTrayWatcher\RuntimeState"
REGISTRY_VALUE_NAME = "{7D6E3F29-2B74-4F18-A9C1-5E83D247B60A}"


def default_installation_id_path(local_appdata: str | os.PathLike[str] | None = None) -> Path:
    """Return the per-user persistent identity path."""
    base = Path(local_appdata) if local_appdata else Path(
        os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")
    )
    return base / APP_VENDOR / APP_PRODUCT / INSTALLATION_ID_FILENAME


def normalize_installation_id(value: str) -> str:
    """Validate and normalize a UUID4 string."""
    parsed = uuid.UUID(value.strip())
    if parsed.version != 4:
        raise ValueError("installation_id is not UUID4")
    return str(parsed)


def _read_file_installation_id(target: Path) -> str | None:
    try:
        return normalize_installation_id(target.read_text(encoding="ascii"))
    except (FileNotFoundError, OSError, UnicodeError, ValueError, AttributeError):
        return None


def _write_file_installation_id(target: Path, installation_id: str) -> None:
    target.parent.mkdir(parents=True, exist_ok=True)
    temporary = target.with_name(f"{target.name}.{os.getpid()}.tmp")
    try:
        temporary.write_text(installation_id + "\n", encoding="ascii")
        os.replace(temporary, target)
    finally:
        try:
            temporary.unlink(missing_ok=True)
        except OSError:
            pass


def _read_registry_installation_id(
    registry_subkey: str = REGISTRY_SUBKEY,
    registry_value_name: str = REGISTRY_VALUE_NAME,
) -> str | None:
    if os.name != "nt":
        return None
    try:
        import winreg

        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, registry_subkey, 0, winreg.KEY_READ) as key:
            value, _value_type = winreg.QueryValueEx(key, registry_value_name)
        return normalize_installation_id(value)
    except (ImportError, OSError, TypeError, UnicodeError, ValueError, AttributeError):
        return None


def _write_registry_installation_id(
    installation_id: str,
    registry_subkey: str = REGISTRY_SUBKEY,
    registry_value_name: str = REGISTRY_VALUE_NAME,
) -> bool:
    if os.name != "nt":
        return False
    try:
        import winreg

        with winreg.CreateKeyEx(
            winreg.HKEY_CURRENT_USER,
            registry_subkey,
            0,
            winreg.KEY_SET_VALUE,
        ) as key:
            winreg.SetValueEx(key, registry_value_name, 0, winreg.REG_SZ, installation_id)
        return True
    except (ImportError, OSError):
        return False


def get_or_create_installation_id(
    path: str | os.PathLike[str] | None = None,
    *,
    uuid_factory=uuid.uuid4,
    registry_subkey: str = REGISTRY_SUBKEY,
    registry_value_name: str = REGISTRY_VALUE_NAME,
    registry_reader: Callable[[], str | None] | None = None,
    registry_writer: Callable[[str], object] | None = None,
) -> str:
    """Load one UUID4 and keep its file and per-user registry mirror in sync."""
    target = Path(path) if path is not None else default_installation_id_path()
    read_registry = registry_reader or (
        lambda: _read_registry_installation_id(registry_subkey, registry_value_name)
    )
    write_registry = registry_writer or (
        lambda value: _write_registry_installation_id(
            value,
            registry_subkey,
            registry_value_name,
        )
    )

    try:
        registry_id = normalize_installation_id(read_registry() or "")
    except (OSError, TypeError, UnicodeError, ValueError, AttributeError):
        registry_id = None
    file_id = _read_file_installation_id(target)

    if registry_id:
        if file_id != registry_id:
            _write_file_installation_id(target, registry_id)
        return registry_id

    if file_id:
        try:
            write_registry(file_id)
        except OSError:
            pass
        return file_id

    installation_id = normalize_installation_id(str(uuid_factory()))
    _write_file_installation_id(target, installation_id)
    try:
        write_registry(installation_id)
    except OSError:
        pass
    return installation_id


def add_installation_id(url: str, installation_id: str) -> str:
    """Add or replace installation_id while preserving query and fragment."""
    normalized = normalize_installation_id(installation_id)
    parts = urlsplit(url)
    if parts.scheme.casefold() != "https":
        raise ValueError("update URL must use HTTPS")
    query = [(key, value) for key, value in parse_qsl(parts.query, keep_blank_values=True) if key != "installation_id"]
    query.append(("installation_id", normalized))
    return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))


def is_daily_update_check_due(last_check_iso: str, *, now: datetime | None = None) -> bool:
    """Return whether no automatic update request was recorded today."""
    current = now or datetime.now()
    if not last_check_iso:
        return True
    try:
        last_check = datetime.fromisoformat(last_check_iso)
    except (TypeError, ValueError):
        return True
    return last_check.date() != current.date()
