"""Daily operations briefing composition and persistence helpers."""

from __future__ import annotations

import json
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from .anomaly import detect_anomalies
from .control_plane import control_plane_store
from .gate_metrics import GateMetricStore
from .llm_provider import LLMProvider


@dataclass
class BriefingSection:
    title: str
    body: str
    priority: int


@dataclass
class DailyBriefing:
    generated_at: str
    sections: list[BriefingSection]
    summary: str


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


def _load_json(path: Path) -> dict[str, Any] | None:
    if not path.exists():
        return None
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return None
    return payload if isinstance(payload, dict) else None


def _within_window(ts: datetime | None, cutoff: datetime) -> bool:
    return ts is not None and ts >= cutoff


def _collect_recent_findings(state_dir: Path, *, cutoff: datetime) -> dict[str, Any]:
    receipts_dir = state_dir / "receipts"
    by_scout: dict[str, int] = {}
    total = 0
    if not receipts_dir.exists():
        return {"total_findings": 0, "by_scout": {}}

    for run_dir in receipts_dir.iterdir():
        if not run_dir.is_dir():
            continue
        scan_payload = _load_json(run_dir / "scan.json")
        if scan_payload is None:
            continue
        scan_ts = _parse_iso(scan_payload.get("ended_at")) or _parse_iso(scan_payload.get("started_at"))
        if not _within_window(scan_ts, cutoff):
            continue
        findings = scan_payload.get("findings")
        if not isinstance(findings, list):
            continue
        for finding in findings:
            if not isinstance(finding, dict):
                continue
            scout_name = str(finding.get("scout_name") or "").strip()
            if not scout_name:
                continue
            findings_count = finding.get("findings_count")
            if isinstance(findings_count, (int, float)):
                count = max(int(findings_count), 0)
            else:
                evidence = finding.get("evidence")
                count = len(evidence) if isinstance(evidence, list) else 0
            by_scout[scout_name] = int(by_scout.get(scout_name, 0) + count)
            total += count
    return {"total_findings": total, "by_scout": by_scout}


def _collect_recent_receipts(state_dir: Path, *, cutoff: datetime) -> dict[str, Any]:
    receipts_dir = state_dir / "receipts"
    counts: dict[str, int] = {}
    module_counts: dict[str, int] = {}
    total = 0
    if not receipts_dir.exists():
        return {"total_receipts": 0, "outcomes": {}, "modules": {}}

    from .modules import infer_module_write_class_from_tool

    for run_dir in receipts_dir.iterdir():
        if not run_dir.is_dir():
            continue
        receipt_payload = _load_json(run_dir / "receipt.json")
        if receipt_payload is None:
            continue
        receipt_ts = _parse_iso(receipt_payload.get("completed_at")) or _parse_iso(receipt_payload.get("started_at"))
        if not _within_window(receipt_ts, cutoff):
            continue
        total += 1
        outcome = str(receipt_payload.get("outcome") or "unknown").strip().lower() or "unknown"
        counts[outcome] = int(counts.get(outcome, 0) + 1)
        changes = receipt_payload.get("changes")
        if not isinstance(changes, list):
            continue
        for change in changes:
            if not isinstance(change, dict):
                continue
            tool_name = str(change.get("tool") or "").strip()
            module_id, _write_class = infer_module_write_class_from_tool(tool_name)
            if module_id:
                module_counts[module_id] = int(module_counts.get(module_id, 0) + 1)
    return {"total_receipts": total, "outcomes": counts, "modules": module_counts}


def _latest_gate_snapshot(state_dir: Path) -> dict[str, Any] | None:
    try:
        return GateMetricStore(state_dir / "db" / "gate_metrics.sqlite").latest_snapshot()
    except Exception:
        return None


def _open_inbox_count(state_dir: Path) -> int:
    try:
        store = control_plane_store(state_dir)
        open_items, _cursor = store.list_inbox(status="open", limit=200)
        return len(open_items)
    except Exception:
        return 0


def _pending_queue_depth(state_dir: Path) -> int:
    pending_dir = state_dir / "pending"
    if not pending_dir.exists():
        return 0
    return len([path for path in pending_dir.iterdir() if path.is_dir()])


def _serialize_briefing(briefing: DailyBriefing) -> dict[str, Any]:
    return {
        "generated_at": briefing.generated_at,
        "summary": briefing.summary,
        "sections": [asdict(section) for section in briefing.sections],
    }


def _parse_llm_briefing(raw: str) -> dict[str, Any] | None:
    text = str(raw or "").strip()
    if not text:
        return None
    if "```" in text:
        start = text.find("{")
        end = text.rfind("}")
        if start >= 0 and end >= start:
            text = text[start : end + 1]
    try:
        payload = json.loads(text)
    except Exception:
        return None
    return payload if isinstance(payload, dict) else None


def _deterministic_sections(raw: dict[str, Any]) -> list[BriefingSection]:
    findings = raw.get("findings") if isinstance(raw.get("findings"), dict) else {}
    receipts = raw.get("receipts") if isinstance(raw.get("receipts"), dict) else {}
    gate = raw.get("gate") if isinstance(raw.get("gate"), dict) else {}
    inbox_open = int(raw.get("inbox_open") or 0)
    pending_depth = int(raw.get("pending_depth") or 0)
    anomalies = raw.get("anomalies") if isinstance(raw.get("anomalies"), list) else []

    top_scouts = sorted(
        [
            (name, int(count))
            for name, count in (findings.get("by_scout") or {}).items()
            if isinstance(name, str) and isinstance(count, int)
        ],
        key=lambda item: item[1],
        reverse=True,
    )[:5]
    scout_lines = [f"- `{name}`: {count} findings" for name, count in top_scouts] or ["- No scout findings in window."]
    findings_priority = 1 if int(findings.get("total_findings") or 0) > 0 else 0

    outcomes = receipts.get("outcomes") if isinstance(receipts.get("outcomes"), dict) else {}
    outcome_lines = [f"- `{name}`: {int(value)}" for name, value in sorted(outcomes.items()) if isinstance(value, int)]
    if not outcome_lines:
        outcome_lines = ["- No execution receipts in window."]
    receipt_priority = 1 if int(outcomes.get("failed", 0)) > 0 or int(outcomes.get("partial_failure", 0)) > 0 else 0

    gate_payload = gate.get("payload") if isinstance(gate.get("payload"), dict) else {}
    gate_current = gate_payload.get("checks") if isinstance(gate_payload.get("checks"), dict) else {}
    failed_checks = [
        str(name).replace("_", " ")
        for name, check in gate_current.items()
        if isinstance(check, dict) and check.get("passed") is False
    ]
    gate_passed = bool(gate_payload.get("gate_passed"))
    gate_body_lines = [f"- Gate passed: `{gate_passed}`"]
    if failed_checks:
        gate_body_lines.append(f"- Failed checks: {', '.join(failed_checks[:5])}")
    gate_priority = 1 if failed_checks else 0

    sections = [
        BriefingSection(
            title="Scout Findings",
            body="\n".join([f"- Total findings: {int(findings.get('total_findings') or 0)}", *scout_lines]),
            priority=findings_priority,
        ),
        BriefingSection(
            title="Module Receipts",
            body="\n".join([f"- Receipts observed: {int(receipts.get('total_receipts') or 0)}", *outcome_lines]),
            priority=receipt_priority,
        ),
        BriefingSection(
            title="Gate Metrics",
            body="\n".join(gate_body_lines),
            priority=gate_priority,
        ),
        BriefingSection(
            title="Inbox",
            body=f"- Open inbox items: {inbox_open}",
            priority=1 if inbox_open > 0 else 0,
        ),
        BriefingSection(
            title="Approval Queue",
            body=f"- Pending action queue depth: {pending_depth}",
            priority=1 if pending_depth > 0 else 0,
        ),
    ]

    if pending_depth > 0:
        sections.append(
            BriefingSection(
                title="Config Drift Alerts",
                body=(
                    f"- {pending_depth} pending actions indicate unresolved configuration or policy drift.\n"
                    "- Review pending plans in Control Tower before promoting automation scope."
                ),
                priority=1,
            )
        )

    if anomalies:
        lines = []
        for alert in anomalies[:8]:
            if not isinstance(alert, dict):
                continue
            lines.append(f"- {str(alert.get('message') or '').strip()}")
        if lines:
            sections.append(
                BriefingSection(
                    title="Anomalies",
                    body="\n".join(lines),
                    priority=2,
                )
            )

    return sections


def _deterministic_summary(raw: dict[str, Any]) -> str:
    findings = raw.get("findings") if isinstance(raw.get("findings"), dict) else {}
    receipts = raw.get("receipts") if isinstance(raw.get("receipts"), dict) else {}
    pending_depth = int(raw.get("pending_depth") or 0)
    inbox_open = int(raw.get("inbox_open") or 0)
    total_findings = int(findings.get("total_findings") or 0)
    total_receipts = int(receipts.get("total_receipts") or 0)
    return (
        f"Over the last window, scouts reported {total_findings} findings and {total_receipts} receipts were "
        f"recorded. The inbox has {inbox_open} open items and the approval queue depth is {pending_depth}."
    )


def compose_briefing(
    state_dir: Path,
    *,
    llm: LLMProvider | None = None,
    lookback_hours: int = 24,
) -> DailyBriefing:
    """Gather local operational data and compose a daily briefing."""

    resolved_state = Path(state_dir).resolve()
    bounded_hours = max(1, min(int(lookback_hours), 24 * 14))
    now = datetime.now(timezone.utc)
    cutoff = now - timedelta(hours=bounded_hours)

    findings = _collect_recent_findings(resolved_state, cutoff=cutoff)
    receipts = _collect_recent_receipts(resolved_state, cutoff=cutoff)
    gate_snapshot = _latest_gate_snapshot(resolved_state)
    inbox_open = _open_inbox_count(resolved_state)
    pending_depth = _pending_queue_depth(resolved_state)
    anomalies = detect_anomalies(resolved_state)

    gate_payload = {}
    if isinstance(gate_snapshot, dict):
        gate_payload = gate_snapshot.get("payload") if isinstance(gate_snapshot.get("payload"), dict) else {}
    raw_data = {
        "generated_at": now.isoformat(),
        "lookback_hours": bounded_hours,
        "findings": findings,
        "receipts": receipts,
        "gate": {
            "generated_at": gate_snapshot.get("generated_at") if isinstance(gate_snapshot, dict) else None,
            "payload": gate_payload,
        },
        "inbox_open": inbox_open,
        "pending_depth": pending_depth,
        "anomalies": [asdict(alert) for alert in anomalies],
    }

    sections = _deterministic_sections(raw_data)
    summary = _deterministic_summary(raw_data)

    if llm is not None:
        prompt = (
            "You are generating a concise daily operations briefing for a CRM automation control plane.\n"
            "Use only the provided JSON facts. Return ONLY JSON with:\n"
            "{\n"
            '  "summary": "one paragraph",\n'
            '  "sections": [{"title": "string", "body": "markdown bullets", "priority": 0|1|2}]\n'
            "}\n"
            "Do not invent data.\n\n"
            f"Facts:\n{json.dumps(raw_data, indent=2)}"
        )
        llm_raw = llm.chat(
            [
                {"role": "system", "content": "You produce factual operations briefings."},
                {"role": "user", "content": prompt},
            ],
            max_tokens=1200,
        )
        parsed = _parse_llm_briefing(llm_raw)
        if isinstance(parsed, dict):
            llm_summary = str(parsed.get("summary") or "").strip()
            if llm_summary:
                summary = llm_summary
            llm_sections = parsed.get("sections")
            if isinstance(llm_sections, list):
                by_title = {section.title: section for section in sections}
                for raw_section in llm_sections:
                    if not isinstance(raw_section, dict):
                        continue
                    title = str(raw_section.get("title") or "").strip()
                    body = str(raw_section.get("body") or "").strip()
                    priority = raw_section.get("priority")
                    try:
                        priority_value = max(0, min(int(priority), 2))
                    except Exception:
                        priority_value = 0
                    if not title or not body:
                        continue
                    if title in by_title:
                        by_title[title].body = body
                        by_title[title].priority = priority_value
                    else:
                        sections.append(
                            BriefingSection(
                                title=title,
                                body=body,
                                priority=priority_value,
                            )
                        )

    return DailyBriefing(generated_at=now.isoformat(), sections=sections, summary=summary)


def briefing_path_for_day(state_dir: Path, day_key: str) -> Path:
    return Path(state_dir) / "briefings" / f"{day_key}.json"


def write_briefing(state_dir: Path, briefing: DailyBriefing, *, day_key: str | None = None) -> Path:
    resolved_state = Path(state_dir).resolve()
    key = day_key or datetime.now(timezone.utc).date().isoformat()
    path = briefing_path_for_day(resolved_state, key)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(_serialize_briefing(briefing), indent=2), encoding="utf-8")
    return path


def load_latest_briefing(state_dir: Path) -> DailyBriefing | None:
    briefings_dir = Path(state_dir).resolve() / "briefings"
    if not briefings_dir.exists():
        return None
    latest_path: Path | None = None
    for path in briefings_dir.glob("*.json"):
        if not path.is_file():
            continue
        if latest_path is None or path.name > latest_path.name:
            latest_path = path
    if latest_path is None:
        return None
    payload = _load_json(latest_path)
    if payload is None:
        return None
    sections_payload = payload.get("sections")
    sections: list[BriefingSection] = []
    if isinstance(sections_payload, list):
        for raw in sections_payload:
            if not isinstance(raw, dict):
                continue
            title = str(raw.get("title") or "").strip()
            body = str(raw.get("body") or "").strip()
            try:
                priority = max(0, min(int(raw.get("priority", 0)), 2))
            except Exception:
                priority = 0
            if not title or not body:
                continue
            sections.append(BriefingSection(title=title, body=body, priority=priority))
    generated_at = str(payload.get("generated_at") or "").strip() or datetime.now(timezone.utc).isoformat()
    summary = str(payload.get("summary") or "").strip()
    return DailyBriefing(generated_at=generated_at, sections=sections, summary=summary)


__all__ = [
    "BriefingSection",
    "DailyBriefing",
    "compose_briefing",
    "briefing_path_for_day",
    "write_briefing",
    "load_latest_briefing",
]
