"""Implementation of ``mld update`` — update mld-sdk to the latest patch version."""

import json
import re
import subprocess
import sys
from collections.abc import Callable
from pathlib import Path
from urllib.error import URLError
from urllib.request import urlopen

try:
    import tomllib
except ModuleNotFoundError:
    import tomli as tomllib  # type: ignore[no-redef]

PYPI_URL = "https://pypi.org/pypi/mld-sdk/json"
NPM_URL = "https://registry.npmjs.org/@morscherlab/mld-sdk"


def _parse_version(version: str) -> tuple[int, int, int] | None:
    """Parse a semver string into (major, minor, patch). Returns None for pre-releases."""
    m = re.match(r"^(\d+)\.(\d+)\.(\d+)$", version.strip())
    if not m:
        return None
    return int(m.group(1)), int(m.group(2)), int(m.group(3))


def _latest_patch(versions: list[str], current: tuple[int, int, int]) -> str | None:
    """Find the highest patch version matching current major.minor."""
    major, minor, _ = current
    best: tuple[int, int, int] | None = None
    for v in versions:
        parsed = _parse_version(v)
        if parsed and parsed[0] == major and parsed[1] == minor and parsed >= current:
            if best is None or parsed > best:
                best = parsed
    if best and best != current:
        return f"{best[0]}.{best[1]}.{best[2]}"
    return None


def _fetch_pypi_versions() -> list[str]:
    """Fetch available mld-sdk versions from PyPI."""
    try:
        with urlopen(PYPI_URL, timeout=10) as resp:
            data = json.loads(resp.read())
        return list(data.get("releases", {}).keys())
    except (URLError, json.JSONDecodeError, OSError) as e:
        print(f"Warning: could not query PyPI: {e}", file=sys.stderr)
        return []


def _fetch_npm_versions() -> list[str]:
    """Fetch available @morscherlab/mld-sdk versions from npm."""
    try:
        with urlopen(NPM_URL, timeout=10) as resp:
            data = json.loads(resp.read())
        return list(data.get("versions", {}).keys())
    except (URLError, json.JSONDecodeError, OSError) as e:
        print(f"Warning: could not query npm: {e}", file=sys.stderr)
        return []


def _read_current_python_version(project_dir: Path) -> str | None:
    """Extract the current mld-sdk version pin from pyproject.toml."""
    pyproject = project_dir / "pyproject.toml"
    if not pyproject.exists():
        return None
    text = pyproject.read_text()
    m = re.search(r'mld-sdk[>~=!]*=\s*([0-9]+\.[0-9]+\.[0-9]+)', text)
    return m.group(1) if m else None


def _read_current_js_version(project_dir: Path) -> str | None:
    """Extract the current @morscherlab/mld-sdk version from frontend/package.json."""
    pkg = project_dir / "frontend" / "package.json"
    if not pkg.exists():
        return None
    data = json.loads(pkg.read_text())
    spec = data.get("dependencies", {}).get("@morscherlab/mld-sdk", "")
    m = re.search(r'([0-9]+\.[0-9]+\.[0-9]+)', spec)
    return m.group(1) if m else None


def _update_pyproject(project_dir: Path, new_version: str) -> None:
    """Replace the mld-sdk version pin in pyproject.toml."""
    pyproject = project_dir / "pyproject.toml"
    text = pyproject.read_text()
    updated = re.sub(
        r'(mld-sdk[>~=!]*=)\s*[0-9]+\.[0-9]+\.[0-9]+',
        rf'\g<1>{new_version}',
        text,
    )
    pyproject.write_text(updated)


def _update_package_json(project_dir: Path, new_version: str) -> None:
    """Replace the @morscherlab/mld-sdk version in frontend/package.json."""
    pkg = project_dir / "frontend" / "package.json"
    data = json.loads(pkg.read_text())
    data.setdefault("dependencies", {})["@morscherlab/mld-sdk"] = f"^{new_version}"
    pkg.write_text(json.dumps(data, indent=2) + "\n")


def _check_and_update(
    label: str,
    current: str | None,
    fetch_versions: Callable[[], list[str]],
    apply_update: Callable[[str], None],
    missing_msg: str,
    dry_run: bool,
) -> bool:
    if not current:
        print(missing_msg)
        return False
    parsed = _parse_version(current)
    if not parsed:
        return False
    latest = _latest_patch(fetch_versions(), parsed)
    if latest:
        print(f"{label}: {current} -> {latest}")
        if not dry_run:
            apply_update(latest)
        return True
    print(f"{label}: {current} (already latest patch)")
    return False


def cmd_update(args) -> None:
    """Execute the ``mld update`` command."""
    project_dir = Path(args.path).resolve()
    dry_run = args.dry_run

    updated_py = _check_and_update(
        label="mld-sdk (Python)",
        current=_read_current_python_version(project_dir),
        fetch_versions=_fetch_pypi_versions,
        apply_update=lambda v: _update_pyproject(project_dir, v),
        missing_msg="No mld-sdk dependency found in pyproject.toml, skipping.",
        dry_run=dry_run,
    )
    updated_js = _check_and_update(
        label="@morscherlab/mld-sdk (npm)",
        current=_read_current_js_version(project_dir),
        fetch_versions=_fetch_npm_versions,
        apply_update=lambda v: _update_package_json(project_dir, v),
        missing_msg="No frontend SDK dependency found, skipping.",
        dry_run=dry_run,
    )

    if dry_run:
        if updated_py or updated_js:
            print("\nDry run — no files were modified.")
        return

    if not (updated_py or updated_js):
        return

    if args.no_sync:
        return

    if updated_py and (project_dir / "pyproject.toml").exists():
        print("Running uv sync...")
        subprocess.run(["uv", "sync"], cwd=project_dir)

    frontend_dir = project_dir / "frontend"
    if updated_js and frontend_dir.exists():
        from mld_sdk.cli import _detect_package_manager
        pm_name, pm_cmd = _detect_package_manager(frontend_dir)
        print(f"Running {pm_name} install...")
        subprocess.run([pm_cmd, "install"], cwd=frontend_dir)
