"""Staleness assessment for thread-scoped structured task plans.

Uses :func:`chat_storage.get_task_plan_with_meta`, message timestamps only (no
content decrypt), and :func:`utils.task_plan.plan_has_open_work`. Does not
mutate plans.
"""
from __future__ import annotations

import os
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any


def _parse_iso_utc(s: str | None) -> datetime | None:
    if not s or not isinstance(s, str):
        return None
    text = s.strip()
    if text.endswith("Z"):
        text = text[:-1] + "+00:00"
    try:
        dt = datetime.fromisoformat(text)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt.astimezone(timezone.utc)
    except Exception:
        return None


def _env_int(name: str, default: int) -> int:
    raw = os.environ.get(name, "").strip()
    if not raw:
        return default
    try:
        return int(raw)
    except ValueError:
        return default


@dataclass
class StaleAssessment:
    """Result of :func:`assess_plan_staleness`."""

    is_stale: bool
    reasons: list[str] = field(default_factory=list)
    plan_updated_at: str | None = None
    last_any_created_at: str | None = None
    last_assistant_created_at: str | None = None
    plan_age_minutes: float = 0.0
    assistant_idle_minutes: float = 0.0


def _default_thresholds() -> tuple[float, float]:
    """Return (plan_age_minutes, assistant_idle_minutes) thresholds."""
    plan_m = float(_env_int("MUGI_TASK_PLAN_STALE_PLAN_AGE_MINUTES", 360))
    asst_m = float(_env_int("MUGI_TASK_PLAN_STALE_ASSISTANT_IDLE_MINUTES", 120))
    return max(1.0, plan_m), max(1.0, asst_m)


def assess_plan_staleness(
    thread_id: str,
    *,
    now: datetime | None = None,
) -> StaleAssessment | None:
    """Return assessment when the thread has a plan with open work; else ``None``."""
    tid = (thread_id or "").strip()
    if not tid:
        return None
    try:
        from chat_storage import get_task_plan_with_meta, get_thread_message_last_created_ats
        from utils.task_plan import plan_has_open_work
    except Exception:
        return None

    plan, updated_at = get_task_plan_with_meta(tid)
    if not plan_has_open_work(plan):
        return None

    now_dt = now or datetime.now(timezone.utc)
    if now_dt.tzinfo is None:
        now_dt = now_dt.replace(tzinfo=timezone.utc)
    else:
        now_dt = now_dt.astimezone(timezone.utc)

    plan_dt = _parse_iso_utc(updated_at)
    last_any, last_asst = get_thread_message_last_created_ats(tid)
    any_dt = _parse_iso_utc(last_any)
    asst_dt = _parse_iso_utc(last_asst)

    reasons: list[str] = []
    plan_age_m = 0.0
    if plan_dt:
        plan_age_m = max(0.0, (now_dt - plan_dt).total_seconds() / 60.0)
    asst_idle_m = 0.0
    if asst_dt:
        asst_idle_m = max(0.0, (now_dt - asst_dt).total_seconds() / 60.0)
    elif plan_dt:
        # No assistant messages: treat idle as plan age
        asst_idle_m = plan_age_m
        reasons.append("no_assistant_messages")
    else:
        reasons.append("missing_timestamps")

    need_plan, need_asst = _default_thresholds()
    is_stale = plan_age_m >= need_plan and asst_idle_m >= need_asst
    if is_stale:
        reasons.append("plan_aged_and_assistant_idle")

    return StaleAssessment(
        is_stale=is_stale,
        reasons=reasons,
        plan_updated_at=updated_at,
        last_any_created_at=last_any,
        last_assistant_created_at=last_asst,
        plan_age_minutes=plan_age_m,
        assistant_idle_minutes=asst_idle_m,
    )


def format_stale_hint(assessment: StaleAssessment | None) -> str:
    """Short string for HTTP / UI; empty when not stale."""
    if assessment is None or not assessment.is_stale:
        return ""
    return (
        f"Plan may be stalled: open work unchanged ~{assessment.plan_age_minutes:.0f}m "
        f"with assistant idle ~{assessment.assistant_idle_minutes:.0f}m."
    )


def format_idle_tick_plan_nudge(assessment: StaleAssessment | None) -> str:
    """Server-generated paragraph for idle tick (no raw step text)."""
    if assessment is None or not assessment.is_stale:
        return ""
    return (
        "Structured task plan: open steps remain; the plan has been quiet for a while and "
        f"assistant replies have been idle (~{assessment.assistant_idle_minutes:.0f} minutes). "
        "If appropriate, use get_task_plan and update_task_plan to advance or close steps."
    )


def stale_activity_summary(assessment: StaleAssessment) -> tuple[str, dict[str, Any]]:
    """Redacted aggregate summary and meta for Activity emission."""
    summary = (
        f"Task plan may be stalled: open work (~{assessment.plan_age_minutes:.0f}m since plan update, "
        f"~{assessment.assistant_idle_minutes:.0f}m since last assistant message)."
    )
    meta = {
        "plan_age_minutes": round(assessment.plan_age_minutes, 1),
        "assistant_idle_minutes": round(assessment.assistant_idle_minutes, 1),
        "reasons": assessment.reasons[:8],
    }
    return summary, meta
