import os
import json
import logging
import time
import threading
import signal
from datetime import datetime, timezone
from pathlib import Path

from flask import Flask, jsonify, Response
import requests

API_URL = os.getenv("GAL2_API_URL", "https://api-v2.gal-2.com/time")
API_KEY = os.getenv("GAL2_API_KEY")
CONFIG_FILE = os.getenv("GAL2_CONFIG_FILE", "/etc/gal2/daemon.env")
CONFIG_PRESENT = os.getenv("GAL2_CONFIG_PRESENT", "unknown")
API_KEY_PRESENT = bool(API_KEY)
BIND_HOST = os.getenv("GAL2_BIND_HOST", "127.0.0.1")
BIND_PORT = int(os.getenv("GAL2_BIND_PORT", "9095"))
CACHE_FILE = Path(os.getenv("GAL2_CACHE_FILE", "gal2_cache.json"))
USAGE_FILE = Path(os.getenv("GAL2_USAGE_FILE", "gal2_usage.json"))

ACTIVE_REFRESH_SEC = float(os.getenv("GAL2_ACTIVE_REFRESH_SEC", "30"))
IDLE_REFRESH_SEC = float(os.getenv("GAL2_IDLE_REFRESH_SEC", "120"))
HOLDOVER_RETRY_SEC = float(os.getenv("GAL2_HOLDOVER_RETRY_SEC", "10"))
IDLE_AFTER_SEC = float(os.getenv("GAL2_IDLE_AFTER_SEC", "60"))
REQUESTS_LIMIT = int(os.getenv("GAL2_REQUESTS_LIMIT", "100000"))
HTTP_TIMEOUT = float(os.getenv("GAL2_HTTP_TIMEOUT", "10"))
MAX_SLEW_RATE = float(os.getenv("GAL2_MAX_SLEW_RATE", "0.25"))  # sec correction per sec

CONTRACT_LIVE_VALID_SEC = float(os.getenv("GAL2_CONTRACT_LIVE_VALID_SEC", "45"))
CONTRACT_HOLDOVER_SOFT_SEC = float(os.getenv("GAL2_CONTRACT_HOLDOVER_SOFT_SEC", "300"))
CONTRACT_HOLDOVER_HARD_SEC = float(os.getenv("GAL2_CONTRACT_HOLDOVER_HARD_SEC", "900"))
CONTRACT_MAX_API_LATENCY_MS = float(os.getenv("GAL2_CONTRACT_MAX_API_LATENCY_MS", "5000"))
CONTRACT_REJOIN_GUARD_SEC = float(os.getenv("GAL2_CONTRACT_REJOIN_GUARD_SEC", "10"))
DAEMON_VERSION = os.getenv("GAL2_DAEMON_VERSION", "1.0.0-rc3")
CONTRACT_VERSION = os.getenv("GAL2_CONTRACT_VERSION", "1.0.0-contract-rc3")
STRUCTURED_LOGS = os.getenv("GAL2_STRUCTURED_LOGS", "1") not in ("0", "false", "False", "no", "NO")

app = Flask(__name__)
_lock = threading.RLock()
_shutdown = False
_wake_refresher = threading.Event()

_state = {
    "last_ok": None,
    "last_error": None,
    "backoff": ACTIVE_REFRESH_SEC,
    "cache": None,
    "anchor_gal2_epoch": None,
    "anchor_utc_epoch": None,
    "anchor_mono": None,
    "last_served_epoch": None,
    "slew_total_sec": 0.0,
    "slew_start_mono": None,
    "slew_end_mono": None,
    "slew_rate": 0.0,
    "last_live_gal2_epoch": None,
    "last_live_utc_epoch": None,
    "last_client_hit_monotonic": None,
    "usage_period_start": None,
    "requests_used": 0,
    "quota_status": "ok",
    "quota_exhausted": False,
    "contract_sequence": 0,
    "last_api_latency_ms": None,
    "last_api_receive_time": None,
    "rejoin_guard_until_mono": None,
    "rejoin_guard_reason": None,
    "last_recovery_holdover_age_sec": None,
    "last_logged_contract_mode": None,
    "last_logged_contract_reason": None,
}


def configure_http_server_logging() -> None:
    """
    Keep stdout JSON-only for infrastructure log ingestion.
    Flask/Werkzeug startup banners and access logs are suppressed.
    GAL-2 structured logs remain emitted through json_log().
    """
    try:
        logging.getLogger("werkzeug").disabled = True
        logging.getLogger("werkzeug").setLevel(logging.ERROR)
        logging.getLogger("werkzeug").propagate = False
    except Exception:
        pass

    try:
        import flask.cli as flask_cli
        flask_cli.show_server_banner = lambda *args, **kwargs: None
    except Exception:
        pass


def json_log(event: str, level: str = "INFO", **fields) -> None:
    if not STRUCTURED_LOGS:
        return

    payload = {
        "timestamp": datetime.now(timezone.utc).isoformat(timespec="microseconds"),
        "level": level,
        "event": event,
        "service": "gal2d",
        "daemon_version": DAEMON_VERSION,
        "contract_version": CONTRACT_VERSION,
    }

    for key, value in fields.items():
        if key is None:
            continue

        k = str(key).lower()

        if "key" in k or "secret" in k or "token" in k or "password" in k:
            if k in ("api_key_present",):
                payload[key] = bool(value)
            else:
                payload[key] = "[REDACTED]"
        else:
            payload[key] = value

    try:
        print(json.dumps(payload, sort_keys=True, separators=(",", ":")), flush=True)
    except Exception:
        pass


def log_contract_transition_unlocked(mode: str, reason: str, safe_to_consume: bool) -> None:
    previous_mode = _state.get("last_logged_contract_mode")
    previous_reason = _state.get("last_logged_contract_reason")

    if previous_mode != mode or previous_reason != reason:
        json_log(
            "mode_transition",
            previous_mode=previous_mode,
            previous_reason=previous_reason,
            mode=mode,
            reason=reason,
            safe_to_consume=bool(safe_to_consume),
            source_authority="gal2_api_core",
            local_role="contract_enforcement_layer",
        )
        _state["last_logged_contract_mode"] = mode
        _state["last_logged_contract_reason"] = reason


def utcnow_iso() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="microseconds")


def parse_iso_epoch(iso_str: str) -> float:
    dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
    return dt.timestamp()


def iso_from_epoch(epoch: float) -> str:
    return (
        datetime.fromtimestamp(epoch, timezone.utc)
        .isoformat(timespec="microseconds")
        .replace("+00:00", "Z")
    )


def current_period_start_iso() -> str:
    now = datetime.now(timezone.utc)
    return now.replace(day=1, hour=0, minute=0, second=0, microsecond=0).isoformat(timespec="seconds")


def recompute_quota_status_unlocked() -> None:
    used = int(_state["requests_used"] or 0)
    if used >= REQUESTS_LIMIT:
        _state["quota_status"] = "exhausted"
        _state["quota_exhausted"] = True
    elif used >= int(REQUESTS_LIMIT * 0.95):
        _state["quota_status"] = "critical"
        _state["quota_exhausted"] = False
    elif used >= int(REQUESTS_LIMIT * 0.80):
        _state["quota_status"] = "warning"
        _state["quota_exhausted"] = False
    else:
        _state["quota_status"] = "ok"
        _state["quota_exhausted"] = False


def maybe_roll_usage_period_unlocked() -> None:
    current = current_period_start_iso()
    if _state["usage_period_start"] != current:
        _state["usage_period_start"] = current
        _state["requests_used"] = 0
        _state["quota_status"] = "ok"
        _state["quota_exhausted"] = False


def build_usage_snapshot_unlocked() -> dict:
    return {
        "period_start": _state["usage_period_start"],
        "requests_used": _state["requests_used"],
        "requests_limit": REQUESTS_LIMIT,
        "quota_status": _state["quota_status"],
    }


def save_usage() -> None:
    try:
        with _lock:
            payload = build_usage_snapshot_unlocked()
        USAGE_FILE.write_text(json.dumps(payload, ensure_ascii=False))
    except Exception as e:
        with _lock:
            _state["last_error"] = f"usage_save_error: {e}"


def load_usage() -> None:
    if not USAGE_FILE.exists():
        with _lock:
            maybe_roll_usage_period_unlocked()
        return
    try:
        payload = json.loads(USAGE_FILE.read_text())
        with _lock:
            _state["usage_period_start"] = payload.get("period_start")
            _state["requests_used"] = int(payload.get("requests_used", 0))
            maybe_roll_usage_period_unlocked()
            recompute_quota_status_unlocked()
    except Exception as e:
        with _lock:
            maybe_roll_usage_period_unlocked()
            _state["last_error"] = f"usage_load_error: {e}"


def mark_request_used_unlocked() -> None:
    maybe_roll_usage_period_unlocked()
    _state["requests_used"] = int(_state["requests_used"] or 0) + 1
    recompute_quota_status_unlocked()


def cache_age_sec_unlocked() -> float | None:
    if not _state["last_ok"]:
        return None
    try:
        last_ok_dt = datetime.fromisoformat(_state["last_ok"])
        return (datetime.now(timezone.utc) - last_ok_dt).total_seconds()
    except Exception:
        return None


def client_recently_active_unlocked(now_mono: float | None = None) -> bool:
    if now_mono is None:
        now_mono = time.monotonic()
    last_hit = _state["last_client_hit_monotonic"]
    if last_hit is None:
        return False
    return (now_mono - last_hit) <= IDLE_AFTER_SEC


def target_live_refresh_sec_unlocked(now_mono: float | None = None) -> float:
    if client_recently_active_unlocked(now_mono):
        return ACTIVE_REFRESH_SEC
    return IDLE_REFRESH_SEC


def mode_unlocked(now_mono: float | None = None) -> str:
    if _state["cache"] is None or _state["anchor_gal2_epoch"] is None:
        return "warming"
    age = cache_age_sec_unlocked()
    if _state["last_ok"] is None:
        return "holdover"
    holdover_threshold = target_live_refresh_sec_unlocked(now_mono) * 1.5
    if age is not None and age > holdover_threshold:
        return "holdover"
    if (_state["slew_start_mono"] is not None and
        _state["slew_end_mono"] is not None and
        float(_state["slew_total_sec"] or 0.0) != 0.0):
        return "slewing"
    return "live"


def slew_correction_unlocked(now_mono: float) -> float:
    start = _state["slew_start_mono"]
    end = _state["slew_end_mono"]
    total = float(_state["slew_total_sec"] or 0.0)
    rate = float(_state["slew_rate"] or 0.0)

    if start is None or end is None:
        return 0.0
    if now_mono <= start:
        return 0.0
    if now_mono >= end:
        return total
    return (now_mono - start) * rate


def maybe_finalize_slew_unlocked(now_mono: float) -> None:
    end = _state["slew_end_mono"]
    if end is None or now_mono < end:
        return

    gal2_epoch, utc_epoch = model_epochs_unlocked(end)
    _state["anchor_gal2_epoch"] = gal2_epoch
    _state["anchor_utc_epoch"] = utc_epoch
    _state["anchor_mono"] = end
    _state["slew_total_sec"] = 0.0
    _state["slew_start_mono"] = None
    _state["slew_end_mono"] = None
    _state["slew_rate"] = 0.0


def model_epochs_unlocked(now_mono: float | None = None) -> tuple[float | None, float | None]:
    if now_mono is None:
        now_mono = time.monotonic()

    anchor_gal2 = _state["anchor_gal2_epoch"]
    anchor_utc = _state["anchor_utc_epoch"]
    anchor_mono = _state["anchor_mono"]

    if anchor_gal2 is None or anchor_mono is None:
        return None, None

    if anchor_utc is None:
        anchor_utc = anchor_gal2

    elapsed = now_mono - anchor_mono
    corr = slew_correction_unlocked(now_mono)

    gal2_epoch = anchor_gal2 + elapsed + corr
    utc_epoch = anchor_utc + elapsed + corr
    return gal2_epoch, utc_epoch


def build_cache_snapshot_unlocked() -> dict:
    return {
        "schema": "gal2d_v2_test",
        "saved_at": utcnow_iso(),
        "last_ok": _state["last_ok"],
        "last_error": _state["last_error"],
        "backoff": _state["backoff"],
        "cache": _state["cache"],
        "anchor_gal2_epoch": _state["anchor_gal2_epoch"],
        "anchor_utc_epoch": _state["anchor_utc_epoch"],
        "last_served_epoch": _state["last_served_epoch"],
        "last_live_gal2_epoch": _state["last_live_gal2_epoch"],
        "last_live_utc_epoch": _state["last_live_utc_epoch"],
        "contract_sequence": _state["contract_sequence"],
        "last_api_latency_ms": _state["last_api_latency_ms"],
        "last_api_receive_time": _state["last_api_receive_time"],
    }


def save_cache() -> None:
    try:
        with _lock:
            payload = build_cache_snapshot_unlocked()
        CACHE_FILE.write_text(json.dumps(payload, ensure_ascii=False))
    except Exception as e:
        with _lock:
            _state["last_error"] = f"cache_save_error: {e}"


def seed_from_payload_unlocked(payload: dict) -> None:
    data = payload.get("cache", payload)
    if not isinstance(data, dict):
        return

    gal2_time = data.get("gal2_time")
    utc_time = data.get("utc_time") or gal2_time
    if not gal2_time:
        return

    gal2_epoch = parse_iso_epoch(gal2_time)
    utc_epoch = parse_iso_epoch(utc_time)

    persisted_last_live_gal2 = payload.get("last_live_gal2_epoch", gal2_epoch)
    persisted_last_live_utc = payload.get("last_live_utc_epoch", utc_epoch)

    floor_gal2 = max(
        v for v in [gal2_epoch, persisted_last_live_gal2]
        if v is not None
    )
    delta = gal2_epoch - utc_epoch
    floor_utc = floor_gal2 - delta

    _state["cache"] = data
    _state["anchor_gal2_epoch"] = floor_gal2
    _state["anchor_utc_epoch"] = floor_utc
    _state["anchor_mono"] = time.monotonic()
    _state["last_served_epoch"] = floor_gal2
    _state["last_ok"] = payload.get("last_ok")
    _state["last_error"] = payload.get("last_error")
    _state["last_live_gal2_epoch"] = persisted_last_live_gal2
    _state["last_live_utc_epoch"] = persisted_last_live_utc
    _state["contract_sequence"] = int(payload.get("contract_sequence", 0) or 0)
    _state["last_api_latency_ms"] = payload.get("last_api_latency_ms")
    _state["last_api_receive_time"] = payload.get("last_api_receive_time")
    _state["slew_total_sec"] = 0.0
    _state["slew_start_mono"] = None
    _state["slew_end_mono"] = None
    _state["slew_rate"] = 0.0


def load_cache() -> None:
    if not CACHE_FILE.exists():
        return
    try:
        payload = json.loads(CACHE_FILE.read_text())
        with _lock:
            seed_from_payload_unlocked(payload)
    except Exception as e:
        with _lock:
            _state["last_error"] = f"cache_load_error: {e}"


def install_live_anchor_unlocked(data: dict, now_mono: float) -> None:
    live_gal2_epoch = parse_iso_epoch(data["gal2_time"])
    live_utc_epoch = parse_iso_epoch(data.get("utc_time") or data["gal2_time"])

    last_ok_age = cache_age_sec_unlocked()
    holdover_threshold = target_live_refresh_sec_unlocked(now_mono) * 1.5
    force_reanchor = (
        _state["anchor_gal2_epoch"] is None or
        _state["anchor_mono"] is None or
        _state["last_ok"] is None or
        (last_ok_age is not None and last_ok_age > holdover_threshold)
    )

    if force_reanchor:
        _state["anchor_gal2_epoch"] = live_gal2_epoch
        _state["anchor_utc_epoch"] = live_utc_epoch
        _state["anchor_mono"] = now_mono
        _state["last_served_epoch"] = live_gal2_epoch
        _state["slew_total_sec"] = 0.0
        _state["slew_start_mono"] = None
        _state["slew_end_mono"] = None
        _state["slew_rate"] = 0.0
        if last_ok_age is not None and last_ok_age > CONTRACT_HOLDOVER_SOFT_SEC:
            _state["rejoin_guard_until_mono"] = now_mono + CONTRACT_REJOIN_GUARD_SEC
            _state["rejoin_guard_reason"] = "guarded_recovery_after_holdover"
            _state["last_recovery_holdover_age_sec"] = round(float(last_ok_age), 3)
    else:
        maybe_finalize_slew_unlocked(now_mono)
        current_gal2_epoch, current_utc_epoch = model_epochs_unlocked(now_mono)

        if current_gal2_epoch is None or current_utc_epoch is None:
            current_gal2_epoch = live_gal2_epoch
            current_utc_epoch = live_utc_epoch

        last_served = _state["last_served_epoch"]
        if last_served is not None and current_gal2_epoch < last_served:
            shift = last_served - current_gal2_epoch
            current_gal2_epoch += shift
            current_utc_epoch += shift

        delta = live_gal2_epoch - current_gal2_epoch

        _state["anchor_gal2_epoch"] = current_gal2_epoch
        _state["anchor_utc_epoch"] = current_utc_epoch
        _state["anchor_mono"] = now_mono

        if abs(delta) < 1e-9:
            _state["slew_total_sec"] = 0.0
            _state["slew_start_mono"] = None
            _state["slew_end_mono"] = None
            _state["slew_rate"] = 0.0
        else:
            duration = max(abs(delta) / max(MAX_SLEW_RATE, 1e-6), 1.0)
            _state["slew_total_sec"] = delta
            _state["slew_start_mono"] = now_mono
            _state["slew_end_mono"] = now_mono + duration
            _state["slew_rate"] = delta / duration

    _state["cache"] = dict(data)
    _state["last_live_gal2_epoch"] = live_gal2_epoch
    _state["last_live_utc_epoch"] = live_utc_epoch


def fetch_once() -> bool:
    if not API_KEY:
        with _lock:
            _state["last_error"] = "missing_api_key"
            _state["backoff"] = HOLDOVER_RETRY_SEC
        json_log(
            "api_sync_skipped_missing_key",
            level="WARN",
            reason="missing_api_key",
            api_key_present=False,
            source_authority="gal2_api_core",
            local_role="contract_enforcement_layer",
        )
        return False

    with _lock:
        maybe_roll_usage_period_unlocked()
        if _state["quota_exhausted"]:
            _state["last_error"] = "quota_exhausted"
            _state["backoff"] = IDLE_REFRESH_SEC
            json_log(
                "api_sync_skipped_quota_exhausted",
                level="WARN",
                reason="quota_exhausted",
                quota_status=_state["quota_status"],
                requests_used=_state["requests_used"],
                requests_limit=REQUESTS_LIMIT,
                source_authority="gal2_api_core",
                local_role="contract_enforcement_layer",
            )
            return False
        mark_request_used_unlocked()
        _state["backoff"] = HOLDOVER_RETRY_SEC
    save_usage()

    try:
        req_start_mono = time.monotonic()
        r = requests.get(API_URL, headers={"x-api-key": API_KEY}, timeout=HTTP_TIMEOUT)
        api_latency_ms = (time.monotonic() - req_start_mono) * 1000.0
        api_receive_time = utcnow_iso()

        if r.status_code != 200:
            with _lock:
                _state["last_error"] = f"http_{r.status_code}"
                _state["backoff"] = HOLDOVER_RETRY_SEC
                _state["last_api_latency_ms"] = round(api_latency_ms, 3)
                _state["last_api_receive_time"] = api_receive_time
            json_log(
                "api_sync_failure",
                level="WARN",
                reason=f"http_{r.status_code}",
                http_status=r.status_code,
                api_latency_ms=round(api_latency_ms, 3),
                source_authority="gal2_api_core",
                local_role="contract_enforcement_layer",
            )
            save_usage()
            return False

        data = r.json()
        now_mono = time.monotonic()

        with _lock:
            install_live_anchor_unlocked(data, now_mono)
            _state["last_ok"] = utcnow_iso()
            _state["last_error"] = None
            _state["backoff"] = target_live_refresh_sec_unlocked(now_mono)
            _state["last_api_latency_ms"] = round(api_latency_ms, 3)
            _state["last_api_receive_time"] = api_receive_time
            mode_now = mode_unlocked(now_mono)
            quota_status_now = _state["quota_status"]
            requests_used_now = _state["requests_used"]

        json_log(
            "api_sync_success",
            api_latency_ms=round(api_latency_ms, 3),
            mode=mode_now,
            quota_status=quota_status_now,
            requests_used=requests_used_now,
            requests_limit=REQUESTS_LIMIT,
            source_authority="gal2_api_core",
            local_role="contract_enforcement_layer",
        )

        save_cache()
        save_usage()
        return True

    except Exception as e:
        with _lock:
            _state["last_error"] = f"req_error: {e}"
            _state["backoff"] = HOLDOVER_RETRY_SEC
        json_log(
            "api_sync_failure",
            level="WARN",
            reason="request_exception",
            error_type=type(e).__name__,
            source_authority="gal2_api_core",
            local_role="contract_enforcement_layer",
        )
        save_usage()
        return False


def refresher() -> None:
    global _shutdown
    while not _shutdown:
        with _lock:
            maybe_roll_usage_period_unlocked()
            recompute_quota_status_unlocked()
            quota_exhausted = _state["quota_exhausted"]

        if quota_exhausted:
            save_usage()
            _wake_refresher.clear()
            _wake_refresher.wait(timeout=IDLE_REFRESH_SEC)
            continue

        ok = fetch_once()

        with _lock:
            now_mono = time.monotonic()
            if ok:
                sleep_for = target_live_refresh_sec_unlocked(now_mono)
            else:
                sleep_for = HOLDOVER_RETRY_SEC if mode_unlocked(now_mono) == "holdover" else _state["backoff"]

        _wake_refresher.clear()
        _wake_refresher.wait(timeout=sleep_for)



def contract_mode_from_daemon_mode_unlocked(mode: str) -> str:
    if mode == "live":
        return "LIVE"
    if mode == "holdover":
        return "HOLDOVER"
    if mode == "slewing":
        return "REJOIN"
    if mode == "warming":
        return "WARMING"
    return "DEGRADED"


def contract_snapshot_unlocked(now_mono: float) -> dict:
    maybe_finalize_slew_unlocked(now_mono)

    local_serve_time = utcnow_iso()
    daemon_mode = mode_unlocked(now_mono)
    contract_mode = contract_mode_from_daemon_mode_unlocked(daemon_mode)
    age = cache_age_sec_unlocked()

    gal2_epoch, utc_epoch = model_epochs_unlocked(now_mono)
    has_time = gal2_epoch is not None and utc_epoch is not None

    if has_time:
        last_served = _state["last_served_epoch"]
        if last_served is not None and gal2_epoch < last_served:
            delta = gal2_epoch - utc_epoch
            gal2_epoch = last_served
            utc_epoch = gal2_epoch - delta
        _state["last_served_epoch"] = gal2_epoch

    _state["contract_sequence"] = int(_state["contract_sequence"] or 0) + 1
    seq = _state["contract_sequence"]

    api_latency_ms = _state["last_api_latency_ms"]
    api_latency_ok = api_latency_ms is None or float(api_latency_ms) <= CONTRACT_MAX_API_LATENCY_MS
    quota_ok = not _state["quota_exhausted"]

    remaining_slew = 0.0
    if _state["slew_end_mono"] is not None:
        remaining_slew = max(0.0, _state["slew_end_mono"] - now_mono)

    guard_active = False
    guard_until = _state.get("rejoin_guard_until_mono")
    if guard_until is not None:
        if now_mono < float(guard_until):
            guard_active = True
            contract_mode = "REJOIN"
        else:
            _state["rejoin_guard_until_mono"] = None
            _state["rejoin_guard_reason"] = None

    rejoin_state = "none"
    if contract_mode == "REJOIN":
        if guard_active:
            rejoin_state = _state.get("rejoin_guard_reason") or "guarded_recovery"
        else:
            rejoin_state = "slewing" if remaining_slew > 0 else "stable"

    if not has_time:
        contract_mode = "WARMING"
        safe = False
        health = "red"
        reason = "warming_no_cache"
        valid_for = 0.0
    elif _state["quota_exhausted"]:
        contract_mode = "FAIL_CLOSED"
        safe = False
        health = "red"
        reason = "quota_exhausted"
        valid_for = 0.0
    elif age is not None and age > CONTRACT_HOLDOVER_HARD_SEC:
        contract_mode = "FAIL_CLOSED"
        safe = False
        health = "red"
        reason = "api_unreachable_holdover_expired"
        valid_for = 0.0
    elif contract_mode == "LIVE" and not api_latency_ok:
        contract_mode = "DEGRADED"
        safe = False
        health = "red"
        reason = "latency_exceeded_policy"
        valid_for = 0.0
    elif contract_mode == "LIVE":
        safe = True
        health = "green"
        reason = "fresh_api_sync"
        valid_for = CONTRACT_LIVE_VALID_SEC
    elif contract_mode == "HOLDOVER" and age is not None and age > CONTRACT_HOLDOVER_SOFT_SEC:
        contract_mode = "DEGRADED"
        safe = True
        health = "yellow"
        reason = "holdover_soft_limit_exceeded_within_hard_policy"
        remaining_hard = CONTRACT_HOLDOVER_HARD_SEC - float(age or 0.0)
        valid_for = max(0.0, min(CONTRACT_LIVE_VALID_SEC, remaining_hard))
    elif contract_mode == "HOLDOVER":
        safe = True
        health = "yellow"
        reason = "api_unreachable_holdover_within_soft_policy"
        remaining_hard = CONTRACT_HOLDOVER_HARD_SEC - float(age or 0.0)
        valid_for = max(0.0, min(CONTRACT_LIVE_VALID_SEC, remaining_hard))
    elif contract_mode == "REJOIN":
        safe = True
        health = "yellow"
        if guard_active:
            reason = "guarded_recovery_after_stale_holdover"
        else:
            reason = "controlled_rejoin_slew_active" if remaining_slew > 0 else "controlled_rejoin_stable"
        valid_for = CONTRACT_LIVE_VALID_SEC
    else:
        safe = False
        health = "red"
        reason = "state_degraded"
        valid_for = 0.0

    local_serve_epoch = parse_iso_epoch(local_serve_time)
    valid_until = iso_from_epoch(local_serve_epoch + valid_for)

    latency_component = float(api_latency_ms or 0.0)
    age_component = float(age or 0.0)
    operational_bound_ms = round(max(1.0, latency_component) + age_component, 3)

    return {
        "schema": "gal2-daemon-time-contract-v1",
        "version": CONTRACT_VERSION,
        "service": "gal2d",
        "mode": contract_mode,
        "daemon_mode": daemon_mode,
        "safe_to_consume": bool(safe),
        "reason": reason,
        "health": health,
        "gal2_time": None if gal2_epoch is None else iso_from_epoch(gal2_epoch),
        "utc_time": None if utc_epoch is None else iso_from_epoch(utc_epoch),
        "monotonic_sequence": seq,
        "last_good_sync": _state["last_ok"],
        "cache_age_seconds": None if age is None else round(age, 3),
        "api_latency_ms": api_latency_ms,
        "local_receive_time": _state["last_api_receive_time"],
        "local_serve_time": local_serve_time,
        "valid_until": valid_until,
        "operational_bound_ms": operational_bound_ms,
        "rejoin_state": rejoin_state,
        "rejoin_guard_active": bool(guard_active),
        "last_recovery_holdover_age_sec": _state.get("last_recovery_holdover_age_sec"),
        "slew_remaining_sec": round(remaining_slew, 3),
        "request_id": f"local-{seq:016d}",
        "quota_status": _state["quota_status"],
        "source_lineage": ["gal2_api", "gal2_daemon", "contract_v1"],
        "policy": {
            "contract_live_valid_sec": CONTRACT_LIVE_VALID_SEC,
            "contract_holdover_soft_sec": CONTRACT_HOLDOVER_SOFT_SEC,
            "contract_holdover_hard_sec": CONTRACT_HOLDOVER_HARD_SEC,
            "contract_max_api_latency_ms": CONTRACT_MAX_API_LATENCY_MS,
            "contract_rejoin_guard_sec": CONTRACT_REJOIN_GUARD_SEC,
        },
    }


def _prom_label(value) -> str:
    s = "" if value is None else str(value)
    return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")


def metrics_contract_state_unlocked(now_mono: float) -> dict:
    age = cache_age_sec_unlocked()
    daemon_mode = mode_unlocked(now_mono)
    contract_mode = contract_mode_from_daemon_mode_unlocked(daemon_mode)

    api_latency = _state.get("last_api_latency_ms")
    api_latency_ok = (
        api_latency is None or float(api_latency) <= CONTRACT_MAX_API_LATENCY_MS
    )

    rejoin_guard_active = (
        _state.get("rejoin_guard_until_mono") is not None
        and now_mono < float(_state.get("rejoin_guard_until_mono") or 0.0)
    )

    if rejoin_guard_active and contract_mode == "LIVE":
        contract_mode = "REJOIN"

    if (
        _state["cache"] is None
        or _state["anchor_gal2_epoch"] is None
        or _state["last_ok"] is None
    ):
        return {
            "mode": "WARMING",
            "safe_to_consume": False,
            "reason": "warming_no_cache",
            "cache_age_seconds": age,
            "api_latency_ms": api_latency,
            "daemon_mode": daemon_mode,
            "rejoin_guard_active": rejoin_guard_active,
        }

    if age is not None and age > CONTRACT_HOLDOVER_HARD_SEC:
        return {
            "mode": "FAIL_CLOSED",
            "safe_to_consume": False,
            "reason": "api_unreachable_holdover_expired",
            "cache_age_seconds": age,
            "api_latency_ms": api_latency,
            "daemon_mode": daemon_mode,
            "rejoin_guard_active": rejoin_guard_active,
        }

    if contract_mode == "LIVE" and not api_latency_ok:
        return {
            "mode": "DEGRADED",
            "safe_to_consume": False,
            "reason": "api_latency_exceeds_policy",
            "cache_age_seconds": age,
            "api_latency_ms": api_latency,
            "daemon_mode": daemon_mode,
            "rejoin_guard_active": rejoin_guard_active,
        }

    if contract_mode == "LIVE":
        return {
            "mode": "LIVE",
            "safe_to_consume": True,
            "reason": "fresh_api_sync",
            "cache_age_seconds": age,
            "api_latency_ms": api_latency,
            "daemon_mode": daemon_mode,
            "rejoin_guard_active": rejoin_guard_active,
        }

    if contract_mode == "HOLDOVER" and age is not None and age > CONTRACT_HOLDOVER_SOFT_SEC:
        return {
            "mode": "DEGRADED",
            "safe_to_consume": True,
            "reason": "api_unreachable_holdover_beyond_soft_policy",
            "cache_age_seconds": age,
            "api_latency_ms": api_latency,
            "daemon_mode": daemon_mode,
            "rejoin_guard_active": rejoin_guard_active,
        }

    if contract_mode == "HOLDOVER":
        return {
            "mode": "HOLDOVER",
            "safe_to_consume": True,
            "reason": "api_unreachable_holdover_within_soft_policy",
            "cache_age_seconds": age,
            "api_latency_ms": api_latency,
            "daemon_mode": daemon_mode,
            "rejoin_guard_active": rejoin_guard_active,
        }

    if contract_mode == "REJOIN":
        return {
            "mode": "REJOIN",
            "safe_to_consume": True,
            "reason": "controlled_rejoin_slew_active",
            "cache_age_seconds": age,
            "api_latency_ms": api_latency,
            "daemon_mode": daemon_mode,
            "rejoin_guard_active": rejoin_guard_active,
        }

    return {
        "mode": contract_mode,
        "safe_to_consume": False,
        "reason": "unknown_state",
        "cache_age_seconds": age,
        "api_latency_ms": api_latency,
        "daemon_mode": daemon_mode,
        "rejoin_guard_active": rejoin_guard_active,
    }


def metrics_text_unlocked(now_mono: float) -> str:
    m = metrics_contract_state_unlocked(now_mono)

    slew_remaining = 0.0
    if _state.get("slew_end_mono") is not None:
        slew_remaining = max(0.0, float(_state["slew_end_mono"]) - now_mono)

    lines = [
        "# HELP gal2_daemon_up GAL-2 daemon process health.",
        "# TYPE gal2_daemon_up gauge",
        "gal2_daemon_up 1",

        "# HELP gal2_build_info GAL-2 daemon and contract version information.",
        "# TYPE gal2_build_info gauge",
        f'gal2_build_info{{daemon_version="{_prom_label(DAEMON_VERSION)}",contract_version="{_prom_label(CONTRACT_VERSION)}"}} 1',

        "# HELP gal2_config_present Whether the documented config file was present at daemon start.",
        "# TYPE gal2_config_present gauge",
        f"gal2_config_present {1 if CONFIG_PRESENT == '1' else 0}",

        "# HELP gal2_api_key_present Whether an API key was present at daemon start. Value is never exposed.",
        "# TYPE gal2_api_key_present gauge",
        f"gal2_api_key_present {1 if API_KEY_PRESENT else 0}",

        "# HELP gal2_cache_present Whether the daemon has a local cache.",
        "# TYPE gal2_cache_present gauge",
        f"gal2_cache_present {1 if _state['cache'] is not None else 0}",

        "# HELP gal2_contract_safe_to_consume Current application-facing safety decision.",
        "# TYPE gal2_contract_safe_to_consume gauge",
        f"gal2_contract_safe_to_consume {1 if m['safe_to_consume'] else 0}",

        "# HELP gal2_contract_mode_info Current contract mode and reason.",
        "# TYPE gal2_contract_mode_info gauge",
        f'gal2_contract_mode_info{{mode="{_prom_label(m["mode"])}",reason="{_prom_label(m["reason"])}"}} 1',

        "# HELP gal2_daemon_mode_info Current daemon mode.",
        "# TYPE gal2_daemon_mode_info gauge",
        f'gal2_daemon_mode_info{{mode="{_prom_label(m["daemon_mode"])}"}} 1',

        "# HELP gal2_cache_age_seconds Age of the last accepted upstream sample.",
        "# TYPE gal2_cache_age_seconds gauge",
        f"gal2_cache_age_seconds {0.0 if m['cache_age_seconds'] is None else float(m['cache_age_seconds'])}",

        "# HELP gal2_api_latency_ms Last observed upstream API latency in milliseconds.",
        "# TYPE gal2_api_latency_ms gauge",
        f"gal2_api_latency_ms {0.0 if m['api_latency_ms'] is None else float(m['api_latency_ms'])}",

        "# HELP gal2_contract_sequence Last served contract sequence. Metrics endpoint does not increment this value.",
        "# TYPE gal2_contract_sequence gauge",
        f"gal2_contract_sequence {int(_state['contract_sequence'] or 0)}",

        "# HELP gal2_api_requests_used API requests used in the current usage period.",
        "# TYPE gal2_api_requests_used gauge",
        f"gal2_api_requests_used {int(_state['requests_used'] or 0)}",

        "# HELP gal2_api_requests_limit API request limit configured for the daemon.",
        "# TYPE gal2_api_requests_limit gauge",
        f"gal2_api_requests_limit {int(REQUESTS_LIMIT)}",

        "# HELP gal2_quota_exhausted Whether the daemon considers quota exhausted.",
        "# TYPE gal2_quota_exhausted gauge",
        f"gal2_quota_exhausted {1 if _state['quota_exhausted'] else 0}",

        "# HELP gal2_slew_active Whether controlled slew is active.",
        "# TYPE gal2_slew_active gauge",
        f"gal2_slew_active {1 if _state.get('slew_end_mono') is not None else 0}",

        "# HELP gal2_slew_remaining_seconds Remaining controlled slew time.",
        "# TYPE gal2_slew_remaining_seconds gauge",
        f"gal2_slew_remaining_seconds {float(slew_remaining)}",

        "# HELP gal2_rejoin_guard_active Whether rejoin guard is active.",
        "# TYPE gal2_rejoin_guard_active gauge",
        f"gal2_rejoin_guard_active {1 if m['rejoin_guard_active'] else 0}",
    ]

    return "\n".join(lines) + "\n"


@app.route("/metrics")
def metrics():
    with _lock:
        now_mono = time.monotonic()
        body = metrics_text_unlocked(now_mono)
        return Response(body, mimetype="text/plain; version=0.0.4; charset=utf-8")


@app.route("/contract")
def contract():
    with _lock:
        now_mono = time.monotonic()
        _state["last_client_hit_monotonic"] = now_mono
        _wake_refresher.set()
        snapshot = contract_snapshot_unlocked(now_mono)
        log_contract_transition_unlocked(
            snapshot.get("mode"),
            snapshot.get("reason"),
            bool(snapshot.get("safe_to_consume")),
        )
        json_log(
            "contract_served",
            request_id=snapshot.get("request_id"),
            mode=snapshot.get("mode"),
            reason=snapshot.get("reason"),
            safe_to_consume=bool(snapshot.get("safe_to_consume")),
            cache_age_seconds=snapshot.get("cache_age_seconds"),
            api_latency_ms=snapshot.get("api_latency_ms"),
            source_authority="gal2_api_core",
            local_role="contract_enforcement_layer",
        )
        save_cache()
        return jsonify(snapshot)


@app.route("/health")
def health():
    with _lock:
        now_mono = time.monotonic()
        maybe_finalize_slew_unlocked(now_mono)
        age = cache_age_sec_unlocked()
        mode = mode_unlocked(now_mono)
        remaining_slew = 0.0
        if _state["slew_end_mono"] is not None:
            remaining_slew = max(0.0, _state["slew_end_mono"] - now_mono)

        return jsonify(
            {
                "service": "gal2d",
                "status": "error" if _state["quota_exhausted"] else mode,
                "mode": "daemon",
                "last_sync": _state["last_ok"],
                "last_error": _state["last_error"],
                "holdover_since": _state["last_ok"] if mode == "holdover" else None,
                "quota_status": _state["quota_status"],
                "requests_used": _state["requests_used"],
                "requests_limit": REQUESTS_LIMIT,
                "config_source": CONFIG_FILE,
                "config_present": CONFIG_PRESENT == "1",
                "api_key_present": API_KEY_PRESENT,
                "poll_interval_seconds": HOLDOVER_RETRY_SEC if mode == "holdover" else target_live_refresh_sec_unlocked(now_mono),
                "backoff_sec": _state["backoff"],
                "has_cache": _state["cache"] is not None,
                "cache_age_sec": None if age is None else round(age, 3),
                "holdover_active": mode == "holdover",
                "slew_active": _state["slew_end_mono"] is not None,
                "slew_remaining_sec": round(remaining_slew, 3),
                "last_served_epoch": _state["last_served_epoch"],
                "now": utcnow_iso(),
            }
        )


@app.route("/now")
def now():
    with _lock:
        now_mono = time.monotonic()
        _state["last_client_hit_monotonic"] = now_mono
        _wake_refresher.set()
        maybe_finalize_slew_unlocked(now_mono)

        if _state["cache"] is None:
            return jsonify({"status": "no_cache_yet", "now": utcnow_iso()})

        gal2_epoch, utc_epoch = model_epochs_unlocked(now_mono)
        if gal2_epoch is None:
            return jsonify({"status": "no_cache_yet", "now": utcnow_iso()})

        delta = gal2_epoch - utc_epoch
        last_served = _state["last_served_epoch"]
        if last_served is not None and gal2_epoch < last_served:
            gal2_epoch = last_served
            utc_epoch = gal2_epoch - delta

        _state["last_served_epoch"] = gal2_epoch

        mode = mode_unlocked(now_mono)
        age = cache_age_sec_unlocked()
        remaining_slew = 0.0
        if _state["slew_end_mono"] is not None:
            remaining_slew = max(0.0, _state["slew_end_mono"] - now_mono)

        resp = dict(_state["cache"])
        resp["gal2_time"] = iso_from_epoch(gal2_epoch)
        resp["utc_time"] = iso_from_epoch(utc_epoch)
        resp["daemon_mode"] = mode
        resp["daemon_last_ok"] = _state["last_ok"]
        resp["daemon_cache_age_sec"] = None if age is None else round(age, 3)
        resp["daemon_slew_remaining_sec"] = round(remaining_slew, 3)
        resp["quota_status"] = _state["quota_status"]
        resp["requests_used"] = _state["requests_used"]
        resp["requests_limit"] = REQUESTS_LIMIT
        resp["confidence"] = 1.0 if mode == "live" else 0.5 if mode == "holdover" else 0.0

        base_source = resp.get("source") or "unknown"
        resp["source"] = f"{base_source}|daemon_{mode}"

    return jsonify(resp)


@app.route("/status")
def status():
    with _lock:
        now_mono = time.monotonic()
        mode = mode_unlocked(now_mono)
        return jsonify(
            {
                "version": DAEMON_VERSION,
                "status": mode,
                "quota_status": _state["quota_status"],
                "requests_used": _state["requests_used"],
                "requests_limit": REQUESTS_LIMIT,
                "config_source": CONFIG_FILE,
                "config_present": CONFIG_PRESENT == "1",
                "api_key_present": API_KEY_PRESENT,
                "poll_interval_seconds": HOLDOVER_RETRY_SEC if mode == "holdover" else target_live_refresh_sec_unlocked(now_mono),
                "next_poll_seconds": _state["backoff"],
                "last_sync": _state["last_ok"],
                "last_error": _state["last_error"],
                "cache_present": _state["cache"] is not None,
                "usage_period_start": _state["usage_period_start"],
            }
        )


def handle_exit(*_args):
    global _shutdown
    _shutdown = True
    _wake_refresher.set()
    raise SystemExit(0)


def main():
    load_cache()
    load_usage()
    t = threading.Thread(target=refresher, daemon=True)
    t.start()

    signal.signal(signal.SIGTERM, handle_exit)
    signal.signal(signal.SIGINT, handle_exit)

    configure_http_server_logging()
    json_log(
        "daemon_start",
        bind_host=BIND_HOST,
        bind_port=BIND_PORT,
        config_source=CONFIG_FILE,
        config_present=CONFIG_PRESENT == "1",
        api_key_present=API_KEY_PRESENT,
        source_authority="gal2_api_core",
        local_role="contract_enforcement_layer",
    )
    app.run(host=BIND_HOST, port=BIND_PORT, debug=False, use_reloader=False)


if __name__ == "__main__":
    main()
