"""Server port discovery.

Enables zero-config port coordination between the server, SDK, and dashboard.

Flow:
    1. Server writes its actual bound URL to ~/.awaithumans-dev.json on startup.
    2. SDK reads that file on every call, verifies the server process is
       still alive (via PID), and uses its URL.
    3. Dashboard reads it via a small API route at build/runtime.
    4. Server deletes the file on graceful shutdown.
    5. If the server crashes and leaves a stale file, readers detect the
       dead PID and ignore the file.

Location:
    ~/.awaithumans-dev.json (user home, stable across cwd changes)

Format:
    {
        "url": "http://localhost:3004",
        "host": "0.0.0.0",
        "port": 3004,
        "pid": 13345,
        "started_at": "2026-04-15T10:25:55Z",
        "admin_token": "<auto-generated in dev mode>"
    }

`admin_token` is the admin API token auto-generated by `awaithumans
dev` — present only in dev mode and only written when the operator
didn't supply their own token via env var. The SDK reads it to
authenticate against the authed task endpoints. Production
deployments must set `AWAITHUMANS_ADMIN_API_TOKEN` on the agent
host explicitly; the discovery file isn't the right distribution
channel for cross-host secrets.

Environment variable AWAITHUMANS_URL always takes precedence over discovery.
"""

from __future__ import annotations

import json
import logging
import os
from contextlib import suppress
from datetime import datetime, timezone
from pathlib import Path

from awaithumans.utils.constants import DISCOVERY_FILE_NAME

logger = logging.getLogger("awaithumans.discovery")


def get_discovery_file_path() -> Path:
    """Return the path to the discovery file (~/.awaithumans-dev.json)."""
    return Path.home() / DISCOVERY_FILE_NAME


def _is_process_alive(pid: int) -> bool:
    """Check if a process with the given PID is still running.

    Uses os.kill(pid, 0) which sends the null signal — doesn't actually
    kill, just checks if the process exists and we have permission to signal it.
    """
    try:
        os.kill(pid, 0)
        return True
    except ProcessLookupError:
        return False
    except PermissionError:
        # Process exists but we can't signal it — treat as alive
        return True


def write_discovery(*, host: str, port: int, admin_token: str | None = None) -> None:
    """Write the server's bound URL (and optional admin token) to the
    discovery file.

    Called by the server on startup. The URL uses `localhost` for client
    access even when bound to `0.0.0.0`, since SDKs connect locally.

    When `admin_token` is provided the SDK will auto-authenticate
    against the server — required after PR A3 made task endpoints
    auth-gated. Writes the file at mode 0600 when supplying a token so
    the secret isn't world-readable.
    """
    client_host = "localhost" if host in ("0.0.0.0", "::") else host
    data: dict[str, object] = {
        "url": f"http://{client_host}:{port}",
        "host": host,
        "port": port,
        "pid": os.getpid(),
        "started_at": datetime.now(timezone.utc).isoformat(),
    }
    if admin_token:
        data["admin_token"] = admin_token

    path = get_discovery_file_path()
    try:
        path.write_text(json.dumps(data, indent=2))
        # Restrict perms whenever the file carries a secret. Best-
        # effort on systems that don't honor chmod.
        if admin_token:
            with suppress(OSError):
                path.chmod(0o600)
        logger.info("Wrote discovery file at %s (url=%s)", path, data["url"])
    except OSError as e:
        logger.warning("Failed to write discovery file at %s: %s", path, e)


def read_discovery() -> dict | None:
    """Read the discovery file. Returns None if it doesn't exist, is invalid,
    or points to a dead PID (stale file from a crashed server).
    """
    path = get_discovery_file_path()
    if not path.exists():
        return None
    try:
        data = json.loads(path.read_text())
    except (OSError, json.JSONDecodeError):
        return None

    # Basic validation
    if not isinstance(data, dict) or "url" not in data:
        return None

    # Verify the server process is still alive. If not, the file is stale.
    pid = data.get("pid")
    if pid is not None and not _is_process_alive(int(pid)):
        logger.info(
            "Discovery file at %s is stale (PID %d no longer running) — ignoring.",
            path,
            pid,
        )
        # Clean up the stale file so we don't re-check it next time
        with suppress(OSError):
            path.unlink(missing_ok=True)
        return None

    return data


def delete_discovery() -> None:
    """Delete the discovery file. Called on graceful shutdown."""
    path = get_discovery_file_path()
    try:
        path.unlink(missing_ok=True)
        logger.info("Deleted discovery file at %s", path)
    except OSError:
        pass


def resolve_server_url(*, explicit_url: str | None = None) -> str:
    """Resolve the awaithumans server URL with this precedence:

    1. Explicit `server_url` argument passed to the SDK call.
    2. `AWAITHUMANS_URL` environment variable.
    3. Discovery file at ~/.awaithumans-dev.json (only if the server PID is alive).
    4. Default: http://localhost:3001.
    """
    if explicit_url:
        return explicit_url.rstrip("/")

    env_url = os.environ.get("AWAITHUMANS_URL")
    if env_url:
        return env_url.rstrip("/")

    discovered = read_discovery()
    if discovered:
        return discovered["url"].rstrip("/")

    return "http://localhost:3001"


def resolve_admin_token(*, explicit_token: str | None = None) -> str | None:
    """Resolve the admin API token the SDK should send as
    `Authorization: Bearer <token>`, with this precedence:

    1. Explicit `api_key` argument passed to the SDK call.
    2. `AWAITHUMANS_ADMIN_API_TOKEN` environment variable (same name
       the server uses — share when both are on one machine).
    3. Discovery file at ~/.awaithumans-dev.json (only in dev — the
       file is local-only, written by `awaithumans dev`).
    4. None — no auth header sent. Works for anonymous public routes
       but the task endpoints will 401.
    """
    if explicit_token:
        return explicit_token

    env_token = os.environ.get("AWAITHUMANS_ADMIN_API_TOKEN")
    if env_token:
        return env_token

    discovered = read_discovery()
    if discovered and isinstance(discovered.get("admin_token"), str):
        return discovered["admin_token"]

    return None
