import os
import hashlib
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", "30"))
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
MIN_SLEW_DELTA_SEC = float(os.getenv("GAL2_MIN_SLEW_DELTA_SEC", "0.25"))  # ignore normal network/API jitter below this threshold

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", "21600"))
CONTRACT_HOLDOVER_HARD_SEC = float(os.getenv("GAL2_CONTRACT_HOLDOVER_HARD_SEC", "259200"))
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.1.3-rc5-ixoye-witness-validity-deadband-observability-fix")
CONTRACT_VERSION = os.getenv("GAL2_CONTRACT_VERSION", "1.1.3-contract-rc5-ixoye-witness-validity-deadband-observability-fix")
RC5_POLICY_PROFILE = os.getenv("GAL2_POLICY_PROFILE", "rc5_ixoye_witness_policy_v0_1_derived_from_rc4_72h")
RC5_HOLDOVER_POLICY_VERSION = os.getenv("GAL2_HOLDOVER_POLICY_VERSION", "rc5_ixoye_witness_holdover_policy_v0_1_derived_from_rc4_72h")
RC5_OSCILLATOR_CLASS_DECLARED = os.getenv("GAL2_OSCILLATOR_CLASS_DECLARED", "unspecified_consumer_hardware")
RC5_UNCERTAINTY_MS_BASIS = os.getenv("GAL2_UNCERTAINTY_MS_BASIS", "conservative_model_v1_not_external_metrology_validated")
RC5_UNCERTAINTY_GROWTH_RATE_MS_PER_HOUR = float(os.getenv("GAL2_UNCERTAINTY_GROWTH_RATE_MS_PER_HOUR", "100.0"))

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-rc5-ixoye-witness",
        "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 = min(CONTRACT_LIVE_VALID_SEC, 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 = min(CONTRACT_LIVE_VALID_SEC, 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["last_sync_delta_sec"] = float(delta)
        _state["last_sync_delta_ms"] = round(float(delta) * 1000.0, 3)
        _state["last_slew_threshold_ms"] = round(float(MIN_SLEW_DELTA_SEC) * 1000.0, 3)

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

        if abs(delta) <= MIN_SLEW_DELTA_SEC:
            _state["slew_total_sec"] = 0.0
            _state["slew_start_mono"] = None
            _state["slew_end_mono"] = None
            _state["slew_rate"] = 0.0
            json_log(
                "api_sync_delta_within_deadband",
                level="INFO",
                sync_trigger=_state.get("last_sync_trigger"),
                trigger_offset_ms=_state.get("last_sync_delta_ms"),
                threshold_applied_ms=_state.get("last_slew_threshold_ms"),
                source_authority="gal2_api_core",
                local_role="contract_enforcement_layer",
            )
        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
            json_log(
                "controlled_slew_started",
                level="INFO",
                sync_trigger=_state.get("last_sync_trigger"),
                trigger_offset_ms=_state.get("last_sync_delta_ms"),
                slew_target_ms=round(float(delta) * 1000.0, 3),
                threshold_applied_ms=_state.get("last_slew_threshold_ms"),
                slew_duration_sec=round(float(duration), 3),
                source_authority="gal2_api_core",
                local_role="contract_enforcement_layer",
            )

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


def fetch_once(sync_trigger: str = "scheduled") -> 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",
            sync_trigger=sync_trigger,
            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",
            sync_trigger=sync_trigger,
                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",
            sync_trigger=sync_trigger,
                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:
            _state["last_sync_trigger"] = sync_trigger
            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",
            sync_trigger=sync_trigger,
            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

        with _lock:
            sync_trigger = _state.get("refresh_wake_reason") or "scheduled"
            _state["refresh_wake_reason"] = None

        ok = fetch_once(sync_trigger=sync_trigger)

        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)
    last_good_sync_epoch = parse_iso_epoch(_state["last_ok"]) if _state.get("last_ok") else None
    valid_until_basis = "local_serve_time_plus_valid_for"

    if valid_for <= 0.0:
        valid_until = iso_from_epoch(local_serve_epoch)
        valid_until_basis = "local_serve_time_zero_validity"
    elif contract_mode == "LIVE" and last_good_sync_epoch is not None:
        valid_until = iso_from_epoch(last_good_sync_epoch + CONTRACT_LIVE_VALID_SEC)
        valid_until_basis = "last_good_sync_plus_contract_live_valid_sec"
    elif contract_mode == "REJOIN" and last_good_sync_epoch is not None:
        valid_until = iso_from_epoch(min(local_serve_epoch + valid_for, last_good_sync_epoch + CONTRACT_LIVE_VALID_SEC))
        valid_until_basis = "min_local_serve_window_and_last_good_sync_live_valid_sec"
    elif contract_mode in ("HOLDOVER", "DEGRADED") and last_good_sync_epoch is not None:
        valid_until = iso_from_epoch(min(local_serve_epoch + valid_for, last_good_sync_epoch + CONTRACT_HOLDOVER_HARD_SEC))
        valid_until_basis = "local_serve_window_bounded_by_holdover_hard_limit"
    else:
        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)
    holdover_age_sec = None if age is None or contract_mode == "LIVE" else round(float(age), 3)
    max_holdover_sec = float(CONTRACT_HOLDOVER_HARD_SEC)
    uncertainty_growth_rate_ms_per_hour = float(RC5_UNCERTAINTY_GROWTH_RATE_MS_PER_HOUR)
    uncertainty_ms = round(max(1.0, latency_component) + ((age_component / 3600.0) * uncertainty_growth_rate_ms_per_hour), 3)
    operational_bound_ms = uncertainty_ms
    last_upstream_anchor_time = _state.get("last_ok")
    last_upstream_anchor_material = "|".join([
        str(_state.get("last_ok")),
        str(_state.get("last_api_receive_time")),
        str(_state.get("last_gal2_epoch")),
        str(_state.get("last_utc_epoch")),
    ])
    last_upstream_anchor_hash = hashlib.sha256(last_upstream_anchor_material.encode("utf-8")).hexdigest() if last_upstream_anchor_time else None

    return {
        "schema": "gal2-daemon-time-contract-v1",
        "version": CONTRACT_VERSION,
        "service": "gal2d-rc5-ixoye-witness",
        "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,
        "monotonic_sequence_semantics": "per_contract_serve",
        "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,
        "valid_until_basis": valid_until_basis,
        "operational_bound_ms": operational_bound_ms,
        "rejoin_state": rejoin_state,
        "rejoin_guard_active": bool(guard_active),
        "last_sync_trigger": _state.get("last_sync_trigger"),
        "last_sync_delta_ms": _state.get("last_sync_delta_ms"),
        "slew_threshold_ms": _state.get("last_slew_threshold_ms") if _state.get("last_slew_threshold_ms") is not None else round(float(MIN_SLEW_DELTA_SEC) * 1000.0, 3),
        "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"],
        "policy_profile": RC5_POLICY_PROFILE,
        "holdover_policy_version": RC5_HOLDOVER_POLICY_VERSION,
        "holdover_age_sec": holdover_age_sec,
        "max_holdover_sec": max_holdover_sec,
        "uncertainty_ms": uncertainty_ms,
        "uncertainty_growth_rate_ms_per_hour": uncertainty_growth_rate_ms_per_hour,
        "uncertainty_ms_basis": RC5_UNCERTAINTY_MS_BASIS,
        "oscillator_class_declared": RC5_OSCILLATOR_CLASS_DECLARED,
        "last_upstream_anchor_time": last_upstream_anchor_time,
        "last_upstream_anchor_hash": last_upstream_anchor_hash,
        "source_lineage": ["gal2_api", "gal2_daemon_rc3_base", "rc4_72h_holdover_policy", "rc5_ixoye_witness_contract"],
        "witness_ref": {
            "enabled": True,
            "layer": "IXOYE",
            "policy": "advisory_only",
            "role": "out_of_band_attestation",
            "discovery": "not_available_in_rc5_1",
            "effect_on_safe_to_consume": "none",
        },
        "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
        age_for_wake = cache_age_sec_unlocked()
        if age_for_wake is None or age_for_wake >= min(ACTIVE_REFRESH_SEC, CONTRACT_LIVE_VALID_SEC):
            _state["refresh_wake_reason"] = "client_contract"
            _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
        age_for_wake = cache_age_sec_unlocked()
        if age_for_wake is None or age_for_wake >= min(ACTIVE_REFRESH_SEC, CONTRACT_LIVE_VALID_SEC):
            _state["refresh_wake_reason"] = "client_now"
            _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()
