"""Heartbeat, log, and artifact POST helpers (skeleton)."""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Mapping, Sequence
import json
import time
import urllib.request
import urllib.error
import ssl
import http.client
import sys

# Defaults are tuned to be tolerant of transient backend stalls.
READ_TIMEOUT_SECONDS = 30
MAX_ATTEMPTS = 3
BASE_DELAY_SECONDS = 0.5
MAX_LOG_MESSAGE_BYTES = 25 * 1024 * 1024
LOG_DROPPED_MESSAGE = "Log entry dropped: too large"


def utcnow() -> datetime:
    """Return a timezone-aware UTC timestamp."""
    return datetime.now(tz=timezone.utc)


def post_heartbeat(
    server_url: str, run_id: str, token: str, summary: str | None
) -> None:
    """Send a heartbeat to the backend."""
    _post(
        f"{server_url.rstrip('/')}/runs/{run_id}/heartbeat",
        token,
        {"observed_at": utcnow().isoformat(), "summary": summary},
    )


def post_log(
    server_url: str,
    run_id: str,
    token: str,
    level: str,
    message: str,
    extra: Mapping[str, object] | None = None,
) -> None:
    """Send a log entry to the backend."""
    _, status = _post(
        f"{server_url.rstrip('/')}/runs/{run_id}/logs",
        token,
        {
            "created_at": utcnow().isoformat(),
            "level": level,
            "message": message,
            "extra": extra or {},
        },
        non_retryable_statuses={413},
    )
    if status == 413:
        placeholder = {
            "created_at": utcnow().isoformat(),
            "level": "warning",
            "message": LOG_DROPPED_MESSAGE,
            "extra": {
                "dropped": True,
                "drop_reason": "payload_too_large",
                "message_bytes": len(message.encode("utf-8")),
                "limit_bytes": MAX_LOG_MESSAGE_BYTES,
                "original_level": level,
            },
        }
        _post(
            f"{server_url.rstrip('/')}/runs/{run_id}/logs",
            token,
            placeholder,
            non_retryable_statuses={413},
            attempts=1,
            base_delay_seconds=0,
        )


def post_artifacts(
    server_url: str,
    run_id: str,
    token: str,
    artifacts: Sequence[Mapping[str, object]],
) -> None:
    """Send artifact manifest entries to the backend."""
    for artifact in artifacts:
        payload: Mapping[str, object] | object = artifact
        if isinstance(artifact, Mapping):
            inner = artifact.get("payload", artifact)
            if isinstance(inner, Mapping):
                payload = inner
        _post(
            f"{server_url.rstrip('/')}/runs/{run_id}/artifacts",
            token,
            {
                "artifact_type": str(artifact.get("artifact_type", "unknown")),
                "payload": payload,
            },
        )


def post_completion(server_url: str, run_id: str, token: str, summary: str) -> None:
    """Mark run complete."""
    _post(
        f"{server_url.rstrip('/')}/runs/{run_id}/complete",
        token,
        {"summary": summary},
    )


def post_error(
    server_url: str, run_id: str, token: str, reason: str, summary: str | None = None
) -> None:
    """Mark run errored."""
    _post(
        f"{server_url.rstrip('/')}/runs/{run_id}/error",
        token,
        {"summary": summary or "", "reason": reason},
    )


def poll_run_control(
    server_url: str,
    run_id: str,
    token: str,
    *,
    timeout_seconds: int = 1,
    attempts: int = 1,
    base_delay_seconds: float = 0.2,
) -> dict[str, object]:
    """Fetch the next pending run control command for this bootstrap process."""
    url = f"{server_url.rstrip('/')}/runs/{run_id}/control/poll"
    req = urllib.request.Request(url, headers={"X-Run-Token": token}, method="GET")
    payload = _urlopen_json_with_retries(
        req,
        timeout_seconds=timeout_seconds,
        attempts=attempts,
        base_delay_seconds=base_delay_seconds,
    )
    return payload


def ack_run_control(
    server_url: str,
    run_id: str,
    token: str,
    command_id: str,
    status: str,
    detail: str | None = None,
) -> None:
    """Acknowledge that a polled run control command was processed."""
    body: dict[str, object] = {"command_id": command_id, "status": status}
    if detail:
        body["detail"] = detail
    _post(
        f"{server_url.rstrip('/')}/runs/{run_id}/control/ack",
        token,
        body,
    )


def _post(
    url: str,
    token: str,
    body: Mapping[str, object],
    *,
    non_retryable_statuses: set[int] | None = None,
    attempts: int = MAX_ATTEMPTS,
    base_delay_seconds: float = BASE_DELAY_SECONDS,
) -> tuple[bool, int | None]:
    data = json.dumps(body).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Content-Type": "application/json",
            "X-Run-Token": token,
        },
        method="POST",
    )
    success, status = _urlopen_with_retries(
        req,
        timeout_seconds=READ_TIMEOUT_SECONDS,
        attempts=attempts,
        base_delay_seconds=base_delay_seconds,
        non_retryable_statuses=non_retryable_statuses,
    )
    if not success and (
        not non_retryable_statuses or status not in non_retryable_statuses
    ):
        # Best-effort: log locally but do not raise, to avoid failing the run on transient stalls.
        print(f"telemetry POST to {url} failed after retries", file=sys.stderr)
    return success, status


def _urlopen_with_retries(
    req: urllib.request.Request,
    timeout_seconds: int,
    attempts: int,
    base_delay_seconds: float,
    non_retryable_statuses: set[int] | None = None,
) -> tuple[bool, int | None]:
    """Best-effort POST with retries for transient network errors."""
    for i in range(attempts):
        try:
            with urllib.request.urlopen(
                req, timeout=timeout_seconds
            ) as resp:  # pragma: no cover - network
                resp.read()
            return True, getattr(resp, "status", None)
        except urllib.error.HTTPError as exc:
            if non_retryable_statuses and exc.code in non_retryable_statuses:
                return False, exc.code
            if i == attempts - 1:
                return False, exc.code
            delay = base_delay_seconds * (2**i)
            time.sleep(delay)
        except (
            TimeoutError,
            urllib.error.URLError,
            http.client.RemoteDisconnected,
            ssl.SSLError,
        ):
            if i == attempts - 1:
                break
            delay = base_delay_seconds * (2**i)
            time.sleep(delay)
    return False, None


def _urlopen_json_with_retries(
    req: urllib.request.Request,
    timeout_seconds: int,
    attempts: int,
    base_delay_seconds: float,
) -> dict[str, object]:
    """Best-effort JSON request with retries for transient network errors."""
    last_exc: Exception | None = None
    for i in range(attempts):
        try:
            with urllib.request.urlopen(req, timeout=timeout_seconds) as resp:
                body = resp.read().decode("utf-8")
                parsed = json.loads(body)
                if isinstance(parsed, dict):
                    return parsed
                return {"data": parsed}
        except (
            TimeoutError,
            urllib.error.URLError,
            http.client.RemoteDisconnected,
            ssl.SSLError,
            json.JSONDecodeError,
        ) as exc:
            last_exc = exc
            if i == attempts - 1:
                break
            delay = base_delay_seconds * (2**i)
            time.sleep(delay)
    assert last_exc is not None
    raise last_exc
