"""Cron job status service — read OpenClaw cron data from the gateway.

Calls ``cron.list`` and ``cron.status`` RPC methods and normalises the results.
This is read-only; cron toggle/run endpoints are out of scope for now.

Parser is deliberately defensive:
- Unknown keys are ignored (schema drift doesn't break anything).
- Missing name → job entry skipped entirely.
- All other missing fields default to None.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from app.core.logging import get_logger
from app.services.openclaw.gateway_rpc import (
    GatewayConfig,
    OpenClawGatewayError,
    openclaw_call,
)

logger = get_logger(__name__)


# ---------------------------------------------------------------------------
# Internal data type
# ---------------------------------------------------------------------------

@dataclass
class CronJob:
    """Normalised representation of one cron job entry."""
    name: str
    schedule: str = ""
    enabled: bool = True
    last_run: str | None = None      # ISO-8601 string or None
    next_run: str | None = None      # ISO-8601 string or None
    last_duration_ms: int | None = None
    last_status: str | None = None   # "success", "error", "running", None
    last_error: str | None = None


# ---------------------------------------------------------------------------
# Status helper
# ---------------------------------------------------------------------------

_STATUS_MAP = {
    "success": "ok",
    "ok": "ok",
    "done": "ok",
    "error": "error",
    "failed": "error",
    "fail": "error",
    "running": "running",
    "active": "running",
}


def compute_job_status(job: CronJob) -> str:
    """Return a normalised status string for display.

    Returns: ``"disabled"`` | ``"pending"`` | ``"ok"`` | ``"error"`` |
             ``"running"`` | ``"unknown"``
    """
    if not job.enabled:
        return "disabled"
    if job.last_run is None:
        return "pending"
    raw = (job.last_status or "").lower().strip()
    return _STATUS_MAP.get(raw, "unknown")


# ---------------------------------------------------------------------------
# Parser — pure function
# ---------------------------------------------------------------------------

def _get_str(d: dict[str, Any], *keys: str) -> str | None:
    for k in keys:
        v = d.get(k)
        if v is not None and str(v).strip():
            return str(v).strip()
    return None


def _get_int(d: dict[str, Any], *keys: str) -> int | None:
    for k in keys:
        v = d.get(k)
        if v is not None:
            try:
                return int(float(v))
            except (TypeError, ValueError):
                pass
    return None


def _get_bool(d: dict[str, Any], *keys: str, default: bool = True) -> bool:
    for k in keys:
        v = d.get(k)
        if v is None:
            continue
        if isinstance(v, bool):
            return v
        if isinstance(v, (int, float)):
            return bool(v)
        if isinstance(v, str):
            return v.lower() not in {"false", "0", "no", "off", "disabled"}
    return default


def parse_cron_jobs(raw: object) -> list[CronJob]:
    """Parse a ``cron.list`` response into a list of CronJob objects.

    Handles both camelCase and snake_case key variants.
    Entries without a recognisable name are silently dropped.
    Non-list input returns an empty list.
    """
    if not isinstance(raw, list):
        return []

    jobs: list[CronJob] = []
    for item in raw:
        if not isinstance(item, dict):
            continue

        name = _get_str(item, "name", "title", "id", "key")
        if not name:
            continue

        job = CronJob(
            name=name,
            schedule=_get_str(item, "schedule", "cron", "expression") or "",
            enabled=_get_bool(item, "enabled", "active", "isEnabled"),
            last_run=_get_str(item, "lastRun", "last_run", "lastRunAt", "ran_at"),
            next_run=_get_str(item, "nextRun", "next_run", "nextRunAt", "next_at"),
            last_duration_ms=_get_int(
                item, "lastDuration", "last_duration", "duration_ms", "durationMs"
            ),
            last_status=_get_str(
                item, "lastStatus", "last_status", "result", "status"
            ),
            last_error=_get_str(item, "lastError", "last_error", "error"),
        )
        jobs.append(job)

    return jobs


# ---------------------------------------------------------------------------
# Gateway fetch
# ---------------------------------------------------------------------------

async def fetch_cron_jobs(config: GatewayConfig) -> list[CronJob]:
    """Fetch and parse cron jobs from the gateway.

    Returns an empty list if the gateway does not support cron,
    the file is missing, or any error occurs.
    """
    try:
        raw = await openclaw_call("cron.list", config=config)
        jobs = parse_cron_jobs(raw)
        logger.debug("cron_status.fetched count=%d", len(jobs))
        return jobs
    except (OpenClawGatewayError, TimeoutError, OSError, RuntimeError) as exc:
        logger.debug("cron_status.fetch_failed error=%s", exc)
        return []
    except Exception as exc:
        logger.warning("cron_status.fetch_unexpected error=%s", exc)
        return []
