"""Auto-update support for long-running workers."""

from __future__ import annotations

from collections.abc import Callable
from collections.abc import Sequence
import importlib.metadata
import json
import os
import re
import subprocess
import sys
import threading
import urllib.request

from a2a_agent import __version__


PACKAGE_NAME = "a2a-agent"
PYPI_JSON_URL = "https://pypi.org/pypi/a2a-agent/json"
FALSE_VALUES = {"0", "false", "no", "off", "disable", "disabled"}
DEFAULT_UPDATE_INTERVAL_SECONDS = 3600.0


def auto_update_enabled() -> bool:
    value = os.environ.get("A2A_AUTO_UPDATE", "1").strip().lower()
    return value not in FALSE_VALUES


def auto_update_interval_seconds() -> float:
    value = os.environ.get("A2A_UPDATE_INTERVAL_SECONDS")
    if value is None:
        return DEFAULT_UPDATE_INTERVAL_SECONDS
    try:
        return max(0.0, float(value))
    except ValueError:
        print(
            f"invalid A2A_UPDATE_INTERVAL_SECONDS={value!r}; "
            f"using {DEFAULT_UPDATE_INTERVAL_SECONDS:g}",
            flush=True,
        )
        return DEFAULT_UPDATE_INTERVAL_SECONDS


def fetch_latest_version(
    url: str | None = None,
    *,
    timeout: float | None = None,
) -> str:
    endpoint = url or os.environ.get("A2A_UPDATE_URL", PYPI_JSON_URL)
    request_timeout = timeout
    if request_timeout is None:
        request_timeout = float(os.environ.get("A2A_UPDATE_TIMEOUT", "5"))
    with urllib.request.urlopen(endpoint, timeout=request_timeout) as response:
        payload = json.loads(response.read().decode("utf-8"))
    return str(payload["info"]["version"])


def is_editable_install(package_name: str = PACKAGE_NAME) -> bool:
    try:
        distribution = importlib.metadata.distribution(package_name)
    except importlib.metadata.PackageNotFoundError:
        return False

    direct_url = distribution.read_text("direct_url.json")
    if not direct_url:
        return False
    try:
        payload = json.loads(direct_url)
    except json.JSONDecodeError:
        return False
    return bool(payload.get("dir_info", {}).get("editable"))


def is_newer_version(latest_version: str, current_version: str) -> bool:
    latest_key = _numeric_version_key(latest_version)
    current_key = _numeric_version_key(current_version)
    size = max(len(latest_key), len(current_key))
    latest_key = latest_key + (0,) * (size - len(latest_key))
    current_key = current_key + (0,) * (size - len(current_key))
    return latest_key > current_key


def restart_args(argv: Sequence[str] | None = None) -> list[str]:
    args = list(argv if argv is not None else sys.argv)
    return [sys.executable, "-m", "a2a_agent.cli", *args[1:]]


def maybe_schedule_auto_update(
    argv: Sequence[str] | None = None,
    *,
    current_version: str = __version__,
    fetch_latest: Callable[[], str] = fetch_latest_version,
    editable_install: Callable[[], bool] = is_editable_install,
    spawn_update: Callable[[Sequence[str]], None] | None = None,
    skip_restarted: bool = True,
    update_action: str = "updating before run",
) -> bool:
    if not auto_update_enabled():
        return False
    if skip_restarted and os.environ.get("A2A_UPDATE_RESTARTED") == "1":
        return False
    if editable_install():
        return False

    try:
        latest_version = fetch_latest()
    except Exception as exc:
        print(f"auto-update check skipped: {exc}", flush=True)
        return False

    if not is_newer_version(latest_version, current_version):
        return False

    command = restart_args(argv)
    print(
        f"a2a-agent update available: {current_version} -> {latest_version}; {update_action}",
        flush=True,
    )
    (spawn_update or spawn_update_process)(command)
    return True


def run_runtime_update_check(
    stop_event: threading.Event,
    argv: Sequence[str] | None = None,
    **kwargs: object,
) -> bool:
    scheduled = maybe_schedule_auto_update(
        argv,
        skip_restarted=False,
        update_action="updating and restarting worker",
        **kwargs,
    )
    if scheduled:
        stop_event.set()
    return scheduled


def start_runtime_auto_update_monitor(
    stop_event: threading.Event,
    argv: Sequence[str] | None = None,
    *,
    interval: float | None = None,
) -> threading.Thread | None:
    if not auto_update_enabled():
        return None

    seconds = auto_update_interval_seconds() if interval is None else max(0.0, interval)
    if seconds <= 0:
        return None

    def monitor() -> None:
        while not stop_event.wait(seconds):
            run_runtime_update_check(stop_event, argv)

    thread = threading.Thread(
        target=monitor,
        name="a2a-agent-auto-update",
        daemon=True,
    )
    thread.start()
    return thread


def spawn_update_process(command: Sequence[str]) -> None:
    env = os.environ.copy()
    env["A2A_UPDATE_RESTARTED"] = "1"
    update_command = [
        sys.executable,
        "-m",
        "pip",
        "install",
        "--upgrade",
        "--no-cache-dir",
        "--no-input",
        "--disable-pip-version-check",
        PACKAGE_NAME,
    ]
    restart_delay = float(os.environ.get("A2A_UPDATE_RESTART_DELAY", "2"))
    script = _update_script(update_command, list(command), restart_delay)
    popen_kwargs: dict[str, object] = {
        "env": env,
        "stdin": subprocess.DEVNULL,
    }
    if os.name == "nt":
        popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
    else:
        popen_kwargs["start_new_session"] = True
    subprocess.Popen([sys.executable, "-c", script], **popen_kwargs)


def _update_script(
    update_command: Sequence[str],
    command: Sequence[str],
    restart_delay: float,
) -> str:
    return "\n".join(
        [
            "import os, subprocess, sys, time",
            # json.dumps 결과(JSON 리스트)는 그대로 파이썬 리스트 리터럴로 유효하다.
            # 여기에 !r을 붙이면 리스트가 아니라 문자열이 되어 subprocess가 깨진다(0.1.8 버그).
            f"update_command = {json.dumps(list(update_command), ensure_ascii=False)}",
            f"restart_command = {json.dumps(list(command), ensure_ascii=False)}",
            f"time.sleep({restart_delay!r})",
            "code = subprocess.call(update_command)",
            "if code != 0:",
            "    print('a2a-agent auto-update failed; restarting current install', flush=True)",
            "else:",
            "    print('a2a-agent auto-update completed; restarting', flush=True)",
            "os.execv(restart_command[0], restart_command)",
        ]
    )


def _numeric_version_key(version: str) -> tuple[int, ...]:
    parts = [part for part in re.split(r"\D+", version) if part]
    return tuple(int(part) for part in parts)
