"""Ego context builder — assembles EGO_CONTEXT.md from live system state.

The ego gets the same base context as any foreground session (SOUL.md,
USER.md, STEERING.md) plus this operational briefing. This module builds
the briefing from live data sources.
"""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from typing import Any

import aiosqlite

logger = logging.getLogger(__name__)


class EgoContextBuilder:
    """Builds the EGO_CONTEXT.md operational briefing for the ego session.

    Reads from:
    - HealthDataService.snapshot() for live subsystem state
    - capabilities.json for registered capabilities
    - awareness_ticks for recent signal values
    - observations for unresolved items
    - ego_proposals for recent proposal history
    - ego_cycles for recent cycle outputs (compacted + recent)
    """

    def __init__(
        self,
        *,
        db: aiosqlite.Connection,
        health_data: Any | None = None,
        capabilities: dict[str, str] | None = None,
    ) -> None:
        self._db = db
        self._health_data = health_data
        self._capabilities = capabilities or {}

    async def build(self) -> str:
        """Assemble the full EGO_CONTEXT.md content."""
        sections: list[str] = []
        sections.append("# EGO_CONTEXT — Operational Briefing\n")
        sections.append(
            "*You are Genesis. This is the state of your world. "
            "What do you do?*\n"
        )

        sections.append(await self._capability_section())
        sections.append(await self._system_health_section())
        sections.append(await self._signals_section())
        sections.append(await self._observations_section())
        sections.append(await self._follow_ups_section())
        sections.append(await self._cost_section())
        sections.append(await self._proposal_history_section())
        sections.append(await self._intervention_history_section())
        sections.append(await self._self_model_section())
        sections.append(await self._user_corrections_section())
        sections.append(await self._output_contract_section())

        return "\n".join(sections)

    # -- Section builders --

    async def _capability_section(self) -> str:
        """Registered capabilities with live status."""
        lines = ["## Capabilities\n"]
        if not self._capabilities:
            lines.append("*No capabilities registered.*\n")
            return "\n".join(lines)

        # Get bootstrap manifest for live status
        manifest = await self._get_bootstrap_manifest()

        for name, description in sorted(self._capabilities.items()):
            status = manifest.get(name, "unknown")
            icon = {"ok": "+", "degraded": "~"}.get(status, "!")
            lines.append(f"- [{icon}] **{name}**: {description} ({status})")

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

    async def _system_health_section(self) -> str:
        """Live system health from health_data snapshot."""
        lines = ["## System Health\n"]

        if not self._health_data:
            lines.append("*Health data service not available.*\n")
            return "\n".join(lines)

        try:
            snap = await self._health_data.snapshot()
        except Exception:
            logger.error("Failed to get health snapshot", exc_info=True)
            lines.append("*Health snapshot failed.*\n")
            return "\n".join(lines)

        # Infrastructure
        infra = snap.get("infrastructure", {})
        lines.append("### Infrastructure")
        for key, val in sorted(infra.items()):
            if isinstance(val, dict):
                status = val.get("status", "?")
                latency = val.get("latency_ms")
                detail = f"{status}"
                if latency is not None:
                    detail += f" ({latency:.0f}ms)"
                lines.append(f"- **{key}**: {detail}")
            else:
                lines.append(f"- **{key}**: {val}")

        # Resilience state
        resilience = snap.get("resilience", "unknown")
        lines.append(f"\n**Composite state**: {resilience}")

        # Queues
        queues = snap.get("queues", {})
        if queues:
            lines.append("\n### Queues")
            deferred = queues.get("deferred_work_queue", {})
            dead_letter = queues.get("dead_letter_queue", {})
            if isinstance(deferred, dict):
                lines.append(
                    f"- Deferred work: {deferred.get('pending', 0)} pending"
                )
            if isinstance(dead_letter, dict):
                lines.append(
                    f"- Dead letter: {dead_letter.get('count', 0)} items"
                )

        # Surplus
        surplus = snap.get("surplus", {})
        if surplus:
            lines.append("\n### Surplus")
            lines.append(
                f"- Queue depth: {surplus.get('queue_depth', 0)}"
            )
            last_dispatch = surplus.get("last_dispatch")
            if last_dispatch:
                lines.append(f"- Last dispatch: {last_dispatch}")

        # User activity signals intentionally omitted — the cadence manager's
        # idle gate handles timing.  Showing the ego raw user-activity metrics
        # (last_user_message_age_s, recent_user_turns) triggers LLM deference
        # bias and self-suppression.  The ego should focus outward on the
        # system, not inward on the user's availability.

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

    async def _signals_section(self) -> str:
        """Recent awareness loop signal values."""
        lines = ["## Awareness Signals (latest tick)\n"]

        try:
            cursor = await self._db.execute(
                "SELECT signals_json, classified_depth, created_at "
                "FROM awareness_ticks "
                "ORDER BY created_at DESC LIMIT 1"
            )
            row = await cursor.fetchone()
        except Exception:
            logger.error("Failed to query awareness_ticks", exc_info=True)
            lines.append("*No awareness data available.*\n")
            return "\n".join(lines)

        if not row:
            lines.append("*No awareness ticks recorded.*\n")
            return "\n".join(lines)

        signals_json, depth, created_at = row
        lines.append(f"**Last tick**: {created_at} (depth: {depth})\n")

        try:
            signals = json.loads(signals_json) if signals_json else {}
        except (json.JSONDecodeError, TypeError):
            signals = {}

        # signals_json is stored as a list of {name, value, source} dicts,
        # not a dict keyed by name. Normalize to dict for display.
        if isinstance(signals, list):
            signals = {
                s["name"]: s
                for s in signals
                if isinstance(s, dict) and "name" in s
            }

        if signals:
            lines.append("| Signal | Value | Source |")
            lines.append("|--------|-------|--------|")
            for sig_name, sig_info in sorted(signals.items()):
                if isinstance(sig_info, dict):
                    val = sig_info.get("value", "?")
                    src = sig_info.get("source", "?")
                    if isinstance(val, float):
                        val = f"{val:.3f}"
                    lines.append(f"| {sig_name} | {val} | {src} |")
                else:
                    lines.append(f"| {sig_name} | {sig_info} | ? |")

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

    async def _observations_section(self) -> str:
        """Recent unresolved observations — what needs attention."""
        lines = ["## Unresolved Observations (last 48h, max 20)\n"]

        try:
            cursor = await self._db.execute(
                "SELECT source, type, category, content, priority, created_at "
                "FROM observations "
                "WHERE resolved = 0 "
                "AND created_at >= datetime('now', '-48 hours') "
                "ORDER BY "
                "  CASE priority "
                "    WHEN 'critical' THEN 0 "
                "    WHEN 'high' THEN 1 "
                "    WHEN 'medium' THEN 2 "
                "    ELSE 3 "
                "  END, "
                "  created_at DESC "
                "LIMIT 20"
            )
            rows = await cursor.fetchall()
        except Exception:
            logger.error("Failed to query observations", exc_info=True)
            lines.append("*Could not query observations.*\n")
            return "\n".join(lines)

        if not rows:
            lines.append("*No unresolved observations in last 48h.*\n")
            return "\n".join(lines)

        lines.append(f"**{len(rows)} items** (sorted by priority):\n")
        for source, obs_type, category, content, priority, _created_at in rows:
            # Truncate content to keep context manageable
            short = content[:200] + "..." if len(content) > 200 else content
            short = short.replace("\n", " ")
            cat_str = f"/{category}" if category else ""
            lines.append(
                f"- [{priority}] **{source}{cat_str}** ({obs_type}): "
                f"{short}"
            )

        # Surfacing awareness: how many observations await morning report delivery
        try:
            from genesis.db.crud.observations import unsurfaced_counts_by_priority
            counts = await unsurfaced_counts_by_priority(self._db)
            if counts:
                total = sum(counts.values())
                breakdown = ", ".join(
                    f"{c} {p}" for p, c in sorted(
                        counts.items(),
                        key=lambda x: {"critical": 0, "high": 1, "medium": 2}.get(x[0], 3),
                    ) if c > 0
                )
                lines.append(f"*Unsurfaced: {total} observations ({breakdown}) awaiting morning report.*\n")
        except Exception:
            pass  # surfaced_at column may not exist yet

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

    async def _follow_ups_section(self) -> str:
        """Pending and failed follow-ups that need ego attention."""
        lines = ["## Follow-ups Requiring Attention\n"]

        try:
            from genesis.db.crud import follow_ups as follow_up_crud

            actionable = await follow_up_crud.get_actionable(self._db)
        except Exception:
            logger.error("Failed to query follow-ups", exc_info=True)
            lines.append("*Could not query follow-ups.*\n")
            return "\n".join(lines)

        if not actionable:
            lines.append("*No follow-ups requiring attention.*\n")
            return "\n".join(lines)

        lines.append(f"**{len(actionable)} items** needing action:\n")
        for fu in actionable[:15]:  # Cap to avoid flooding context
            status = fu.get("status", "?")
            priority = fu.get("priority", "medium")
            content = fu.get("content", "")[:200]
            content = content.replace("\n", " ")
            source = fu.get("source", "?")
            strategy = fu.get("strategy", "?")
            blocked = fu.get("blocked_reason", "")

            pin_tag = " [pinned]" if fu.get("pinned") else ""
            line = f"- [{priority}/{status}]{pin_tag} ({source}) **{strategy}**: {content}"
            if blocked:
                line += f" — BLOCKED: {blocked[:100]}"
            lines.append(line)

        lines.append(
            "\nYou can RESOLVE follow-ups via resolved_follow_ups output. "
            "You CANNOT create new follow-ups — use proposals for actions "
            "and observations for noting things. Follow-ups are a user-facing "
            "tracking mechanism managed by foreground sessions only.\n"
        )
        return "\n".join(lines)

    async def _cost_section(self) -> str:
        """Daily spend and budget status."""
        lines = ["## Cost Status\n"]

        try:
            today = datetime.now(UTC).strftime("%Y-%m-%d")
            cursor = await self._db.execute(
                "SELECT COALESCE(SUM(cost_usd), 0.0) FROM cost_events "
                "WHERE created_at >= ?",
                (today,),
            )
            row = await cursor.fetchone()
            daily_spend = row[0] if row else 0.0
        except Exception:
            logger.error("Failed to query cost_events", exc_info=True)
            lines.append("*Cost data unavailable.*\n")
            return "\n".join(lines)

        lines.append(f"- **Today's spend**: ${daily_spend:.4f}")

        # Check ego-specific spend
        try:
            cursor = await self._db.execute(
                "SELECT COALESCE(SUM(cost_usd), 0.0) FROM ego_cycles "
                "WHERE created_at >= ?",
                (today,),
            )
            row = await cursor.fetchone()
            ego_spend = row[0] if row else 0.0
            lines.append(f"- **Ego spend today**: ${ego_spend:.4f}")
        except Exception:
            pass  # ego_cycles table may not exist yet

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

    def _format_proposal_row(
        self,
        action_type: str,
        category: str,
        content: str,
        status: str,
        response: str | None,
    ) -> str:
        """Format a single proposal row for the context table."""
        short = content[:60] + "..." if len(content) > 60 else content
        short = short.replace("\n", " ").replace("|", "/")
        # Parse outcome from user_response: "session:xxx|failed:reason"
        resp = response or ""
        if "|failed:" in resp:
            outcome = "FAILED: " + resp.split("|failed:", 1)[1][:60]
        elif "|completed:" in resp:
            outcome = "OK: " + resp.split("|completed:", 1)[1][:60]
        elif status == "executed" and resp and not resp.startswith("dispatching"):
            outcome = "dispatched"
        else:
            outcome = "—"
        outcome = outcome.replace("|", "/").replace("\n", " ")
        return f"| {action_type} | {category} | {status} | {outcome} | {short} |"

    async def _proposal_history_section(self) -> str:
        """Recent proposal outcomes for calibration.

        Split into two sections so active proposals are always visible
        regardless of how many withdrawn/tabled proposals exist.
        """
        lines = ["## Active Proposals\n"]
        table_header = (
            "| Action | Category | Status | Outcome | Content |\n"
            "|--------|----------|--------|---------|---------|"
        )

        try:
            # Section 1: Active proposals (pending, approved, executed)
            cursor = await self._db.execute(
                "SELECT action_type, action_category, content, status, "
                "user_response, created_at "
                "FROM ego_proposals "
                "WHERE created_at >= datetime('now', '-7 days') "
                "AND status IN ('pending', 'approved', 'executed') "
                "ORDER BY created_at DESC "
                "LIMIT 15"
            )
            active_rows = await cursor.fetchall()

            if not active_rows:
                lines.append("*No active proposals.*\n")
            else:
                lines.append(table_header)
                for row in active_rows:
                    lines.append(self._format_proposal_row(*row[:5]))
                lines.append("")

            # Section 2: Recently tried (withdrawn, tabled, rejected, failed)
            lines.append("## Recently Tried (do not re-propose)\n")
            cursor2 = await self._db.execute(
                "SELECT action_type, action_category, content, status, "
                "user_response, created_at "
                "FROM ego_proposals "
                "WHERE created_at >= datetime('now', '-7 days') "
                "AND status IN ('withdrawn', 'tabled', 'rejected', 'failed', 'expired') "
                "ORDER BY created_at DESC "
                "LIMIT 10"
            )
            tried_rows = await cursor2.fetchall()

            if not tried_rows:
                lines.append("*No recently tried proposals.*\n")
            else:
                lines.append(table_header)
                for row in tried_rows:
                    lines.append(self._format_proposal_row(*row[:5]))
                lines.append("")

        except Exception:
            lines.append("*No proposal history available.*\n")

        return "\n".join(lines)

    async def _intervention_history_section(self) -> str:
        """Recent intervention outcomes — what happened after proposals resolved."""
        lines = ["## Intervention History (7d)\n"]

        try:
            from genesis.db.crud import intervention_journal as journal_crud

            resolved = await journal_crud.recent_resolved(self._db, days=7, limit=10)
            pending = await journal_crud.unresolved_count(self._db)
        except Exception:
            lines.append("*No intervention data available.*\n")
            return "\n".join(lines)

        if not resolved and not pending:
            lines.append("*No interventions recorded yet.*\n")
            return "\n".join(lines)

        if resolved:
            lines.append("| Action | Expected | Outcome | Status |")
            lines.append("|--------|----------|---------|--------|")
            for entry in resolved:
                action = entry["action_type"]
                expected = (entry["expected_outcome"] or "—")[:60].replace("\n", " ").replace("|", "/")
                actual = (entry["actual_outcome"] or "—")[:60].replace("\n", " ").replace("|", "/")
                status = entry["outcome_status"]
                lines.append(f"| {action} | {expected} | {actual} | {status} |")
            lines.append("")

        if pending:
            lines.append(f"*{pending} proposals pending resolution.*\n")

        return "\n".join(lines)

    async def _self_model_section(self) -> str:
        """Capability self-portrait — what the ego is good/bad at."""
        lines = ["## Self-Model\n"]

        try:
            from genesis.db.crud import capability_map as cap_crud
            entries = await cap_crud.get_all(self._db)
        except Exception:
            lines.append("*No capability data available yet.*\n")
            return "\n".join(lines)

        if not entries:
            lines.append("*Capability map is empty — run aggregation to populate.*\n")
            return "\n".join(lines)

        lines.append("| Domain | Confidence | Trend | Evidence |")
        lines.append("|--------|-----------|-------|----------|")
        for entry in entries[:15]:  # cap at 15 rows
            domain = entry["domain"]
            conf = f"{entry['confidence']:.0%}"
            trend = entry.get("trend", "stable")
            evidence = (entry.get("evidence_summary") or "—")[:80].replace("|", "/")
            lines.append(f"| {domain} | {conf} | {trend} | {evidence} |")

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

    async def _user_corrections_section(self) -> str:
        """Recent user corrections from the ego_proposals Telegram topic."""
        lines = ["## User Corrections (recent)\n"]

        try:
            from genesis.runtime import GenesisRuntime

            rt = GenesisRuntime.instance()
            retriever = rt._hybrid_retriever
            if retriever is None:
                lines.append("*Memory retriever not available.*\n")
                return "\n".join(lines)

            results = await retriever.recall(
                "user correction ego",
                limit=10,
            )
            # Filter for corrections tagged by the ego correction handler
            corrections = [
                r for r in results
                if "user_correction" in (r.payload.get("tags") or [])
            ]
        except Exception:
            logger.debug("Failed to recall user corrections", exc_info=True)
            lines.append("*Could not query user corrections.*\n")
            return "\n".join(lines)

        if not corrections:
            lines.append("*No user corrections recorded.*\n")
            return "\n".join(lines)

        lines.append(f"**{len(corrections)} corrections** from the user:\n")
        for c in corrections[:10]:
            content = (c.content or "")[:300]
            content = content.replace("\n", " ")
            created = c.payload.get("created_at", "?")
            lines.append(f"- [{created}] {content}")

        lines.append(
            "\nThese are user inputs — not commands. Triangulate against "
            "live system state before accepting or rejecting.\n"
        )
        return "\n".join(lines)

    async def _output_contract_section(self) -> str:
        """Remind the ego of its required output format."""
        return (
            "## Output Contract\n\n"
            "You MUST respond with valid JSON matching this structure:\n"
            "```json\n"
            "{\n"
            '  "proposals": [\n'
            "    {\n"
            '      "action_type": "investigate|outreach|maintenance|...",\n'
            '      "action_category": "category for graduation tracking",\n'
            '      "content": "what you want to do",\n'
            '      "rationale": "why",\n'
            '      "confidence": 0.85,\n'
            '      "urgency": "low|normal|high|critical",\n'
            '      "alternatives": "what else you considered"\n'
            "    }\n"
            "  ],\n"
            '  "focus_summary": "one-line: what Genesis is focused on",\n'
            '  "follow_ups": [],\n'
            '  "morning_report": "only if this is a morning trigger"\n'
            "}\n"
            "```\n\n"
            "If you have nothing to propose, return an empty proposals "
            "array. The focus_summary is always required.\n"
        )

    # -- Helpers --

    async def _get_bootstrap_manifest(self) -> dict[str, str]:
        """Get the bootstrap manifest from runtime if available."""
        try:
            from genesis.runtime import GenesisRuntime

            rt = GenesisRuntime.instance()
            return dict(rt._bootstrap_manifest) if rt else {}
        except Exception:
            return {}
