"""Activity Logger — centralized event collector for deep agent awareness.

Every service in AgntSpace emits structured events here. The logger:
1. Writes important events to agent observations (markdown)
2. Buffers all events in memory for daily summaries
3. Provides get_daily_summary() for the memory/reflection pipeline
4. Tracks feedback signals (approval, rejection, correction, silence)
5. Maintains mechanical counters for user behavior analysis
6. Detects silence/absence patterns
7. Tracks agent predictions vs outcomes

This closes the observability gap: tool calls, subagent dispatches,
LLM errors, knowledge changes, contract enforcement, and user patterns
all become visible to the agent's reflection and memory consolidation.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from agntspace.infra.timezone import local_today, now_local

if TYPE_CHECKING:
    from agntspace.journal.service import JournalService
    from agntspace.skills.loader import SkillLoader

log = logging.getLogger(__name__)

# Fallback values used when the activity-logging skill config is unavailable.
_FALLBACK_SECTION_MAP: dict[str, str] = {  # arch:deterministic — fallback when activity-logging skill config/activity.yaml is unavailable; primary source is the YAML
    "chat": "Observations",
    "tool": "Tool Activity",
    "dispatch": "Subagent Activity",
    "llm": "System Health",
    "contract": "System Health",
    "queue": "System Health",
    "knowledge": "Knowledge Changes",
    "trust": "Observations",
    "memory": "Observations",
    "system": "Observations",
    "user": "User Patterns",
    "user_override": "User Patterns",
    "feedback": "Observations",
    "prediction": "Observations",
}

_FALLBACK_SILENCE_THRESHOLDS: dict[str, int] = {  # arch:deterministic — fallback when activity-logging skill config/activity.yaml is unavailable
    "no_chat": 48,
    "no_journal": 72,
    "no_activity": 72,
}

_FALLBACK_CATEGORY_ORDER: list[str] = [  # arch:deterministic — fallback when activity-logging skill config/activity.yaml is unavailable
    "chat", "tool", "dispatch", "knowledge", "feedback",
    "user_override",
    "llm", "contract", "queue", "trust", "memory",
    "prediction", "user", "system",
]


def _load_activity_config(skill_loader: SkillLoader | None) -> dict | None:
    """Load activity.yaml from the activity-logging skill."""
    if not skill_loader:
        return None
    try:
        return skill_loader.load_skill_config_file("activity-logging", "activity.yaml")
    except Exception:
        return None


@dataclass
class ActivityEvent:
    """A structured event from any service."""

    category: str  # chat, tool, dispatch, llm, knowledge, contract, trust, memory, queue, system, user, feedback, prediction
    action: str  # verb: invoked, failed, blocked, dispatched, extracted, etc.
    summary: str  # human-readable one-liner
    details: dict = field(default_factory=dict)
    timestamp: str = ""
    importance: str = "medium"  # low, medium, high
    correlation_id: str = ""  # shared across every event in a causal chain

    def __post_init__(self) -> None:
        if not self.timestamp:
            from agntspace.infra.timezone import format_iso_local
            self.timestamp = format_iso_local(datetime.now(UTC))


@dataclass
class Prediction:
    """A tracked prediction the agent made about the user."""

    id: str
    prediction: str  # what the agent predicted
    context: str  # why it predicted this
    made_at: str  # ISO date
    outcome: str = ""  # what actually happened
    resolved_at: str = ""  # when outcome was observed
    correct: bool | None = None  # True/False/None(pending)


class ActivityLogger:
    """Centralized event collector that bridges all services to the observation pipeline.

    Beyond event logging, provides:
    - Feedback signal tracking (approval/rejection/correction rates)
    - Mechanical counters for behavior analysis (no LLM needed)
    - Silence/absence detection
    - Agent prediction tracking (predict → observe → learn)
    """

    def __init__(
        self,
        journal: JournalService | None = None,
        skill_loader: SkillLoader | None = None,
    ) -> None:
        self._journal = journal
        self._buffer: list[ActivityEvent] = []
        self._buffer_date: str = ""
        self._counts: dict[str, int] = {}  # category → count for current day

        # Load config from skill YAML, fall back to hardcoded defaults
        cfg = _load_activity_config(skill_loader)
        self._section_map: dict[str, str] = (
            cfg.get("section_map") if cfg and isinstance(cfg.get("section_map"), dict) else None
        ) or _FALLBACK_SECTION_MAP
        self._silence_thresholds: dict[str, int] = (
            cfg.get("silence_thresholds") if cfg and isinstance(cfg.get("silence_thresholds"), dict) else None
        ) or _FALLBACK_SILENCE_THRESHOLDS
        self._category_order: list[str] = (
            cfg.get("category_order") if cfg and isinstance(cfg.get("category_order"), list) else None
        ) or _FALLBACK_CATEGORY_ORDER

        # ── Feedback tracking ──
        self._feedback: dict[str, list[dict]] = {
            "approvals": [],
            "rejections": [],
            "corrections": [],
        }
        self._feedback_date: str = ""

        # ── Mechanical counters (no LLM needed) ──
        self._counters: dict[str, int | float] = {
            "chat_messages_today": 0,
            "tool_calls_today": 0,
            "dispatches_today": 0,
            "memory_approved": 0,
            "memory_rejected": 0,
            "contract_blocks": 0,
            "llm_errors": 0,
            "triples_extracted": 0,
            "user_words_today": 0,  # rough engagement measure
        }

        # ── Silence detection ──
        self._last_user_chat: str = ""  # ISO datetime of last user message
        self._last_journal_write: str = ""  # ISO datetime of last journal entry
        self._last_user_activity: str = ""  # ISO datetime of any user action

        # ── Prediction tracking ──
        self._predictions: list[Prediction] = []

    # ── Core event logging ──

    def log(
        self,
        category: str,
        action: str,
        summary: str,
        details: dict | None = None,
        importance: str = "medium",
        correlation_id: str | None = None,
    ) -> None:
        """Log a structured activity event.

        correlation_id is inherited from the current async context if not
        passed explicitly, so events inside a causal chain (chat turn,
        channel message, cron) automatically carry the same ID.
        """
        # Pick up the context-var correlation ID unless caller passed one
        if correlation_id is None:
            try:
                from agntspace.activity.decisions import current_correlation_id
                correlation_id = current_correlation_id()
            except Exception:
                correlation_id = ""
        event = ActivityEvent(
            category=category,
            action=action,
            summary=summary,
            details=details or {},
            importance=importance,
            correlation_id=correlation_id or "",
        )

        # Buffer management — reset on date change (local time boundary)
        today = local_today()
        if today != self._buffer_date:
            self._buffer.clear()
            self._counts.clear()
            self._reset_daily_counters()
            self._buffer_date = today

        self._buffer.append(event)
        self._counts[category] = self._counts.get(category, 0) + 1

        # Update mechanical counters
        self._update_counters(event)

        # Write to agent observations file for medium/high importance
        if importance in ("medium", "high") and self._journal:
            section = self._section_map.get(category, "Observations")
            ts = now_local().strftime("%H:%M")
            tag = f"[{category}]"
            marker = " **!!**" if importance == "high" else ""
            line = f"- {ts} {tag} — {summary}{marker}"
            try:
                self._journal._append_to_agent_section(section, line)
            except Exception:
                log.debug("Failed to write activity to observations", exc_info=True)

    def _update_counters(self, event: ActivityEvent) -> None:
        """Update mechanical counters from an event (no LLM needed)."""
        cat = event.category
        action = event.action

        if cat == "chat":
            self._counters["chat_messages_today"] += 1
            # Track word count for engagement measurement
            word_count = event.details.get("user_words", 0)
            if word_count:
                self._counters["user_words_today"] += word_count
        elif cat == "tool":
            self._counters["tool_calls_today"] += 1
        elif cat == "dispatch":
            self._counters["dispatches_today"] += 1
        elif cat == "llm" and action == "error":
            self._counters["llm_errors"] += 1
        elif cat == "contract" and action == "blocked":
            self._counters["contract_blocks"] += 1
        elif cat == "knowledge" and action == "extracted":
            count = event.details.get("count", 1)
            self._counters["triples_extracted"] += count

    def _reset_daily_counters(self) -> None:
        """Reset counters that are per-day."""
        for key in ("chat_messages_today", "tool_calls_today", "dispatches_today",
                     "llm_errors", "contract_blocks", "triples_extracted",
                     "user_words_today"):
            self._counters[key] = 0

    # ── Feedback tracking ──

    def record_feedback(
        self,
        feedback_type: str,
        context: str,
        details: dict | None = None,
    ) -> None:
        """Record explicit user feedback.

        Args:
            feedback_type: "approval", "rejection", or "correction"
            context: What was approved/rejected/corrected (e.g., "memory entry about health")
            details: Additional structured data
        """
        today = local_today()
        if today != self._feedback_date:
            # Archive yesterday's feedback to counters before clearing
            self._feedback = {"approvals": [], "rejections": [], "corrections": []}
            self._feedback_date = today

        entry = {
            "context": context,
            "details": details or {},
            "timestamp": now_local().strftime("%H:%M"),
        }

        if feedback_type == "approval":
            self._feedback["approvals"].append(entry)
            self._counters["memory_approved"] += 1
            self.log("feedback", "approved", context, details, importance="low")
        elif feedback_type == "rejection":
            self._feedback["rejections"].append(entry)
            self._counters["memory_rejected"] += 1
            self.log("feedback", "rejected", context, details, importance="medium")
        elif feedback_type == "correction":
            self._feedback["corrections"].append(entry)
            self.log("feedback", "corrected", context, details, importance="high")

    def get_feedback_summary(self) -> dict:
        """Get today's feedback summary for behavior analysis."""
        approved = len(self._feedback.get("approvals", []))
        rejected = len(self._feedback.get("rejections", []))
        corrected = len(self._feedback.get("corrections", []))
        total = approved + rejected + corrected
        return {
            "approved": approved,
            "rejected": rejected,
            "corrected": corrected,
            "total": total,
            "approval_rate": approved / total if total > 0 else None,
            "corrections": [c["context"] for c in self._feedback.get("corrections", [])],
            "rejections": [r["context"] for r in self._feedback.get("rejections", [])],
        }

    # ── Silence / absence detection ──

    def record_user_activity(self, activity_type: str = "chat") -> None:
        """Record that the user did something (updates silence tracking)."""
        now = datetime.now(UTC).isoformat()
        self._last_user_activity = now
        if activity_type == "chat":
            self._last_user_chat = now
        elif activity_type == "journal":
            self._last_journal_write = now

    def get_silence_status(self) -> dict:
        """Check for user silence/absence patterns.

        Returns hours since last activity and silence flags.
        """
        now = datetime.now(UTC)
        result: dict = {"flags": []}

        def _hours_since(iso_str: str) -> float | None:
            if not iso_str:
                return None
            try:
                last = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
                if last.tzinfo is None:
                    last = last.replace(tzinfo=UTC)
                return (now - last).total_seconds() / 3600
            except Exception:
                return None

        chat_hours = _hours_since(self._last_user_chat)
        journal_hours = _hours_since(self._last_journal_write)
        activity_hours = _hours_since(self._last_user_activity)

        result["hours_since_chat"] = chat_hours
        result["hours_since_journal"] = journal_hours
        result["hours_since_activity"] = activity_hours

        # Silence flags (thresholds from activity-logging skill config)
        chat_thresh = self._silence_thresholds.get("no_chat", 48)
        journal_thresh = self._silence_thresholds.get("no_journal", 72)
        activity_thresh = self._silence_thresholds.get("no_activity", 72)
        if chat_hours and chat_hours > chat_thresh:
            result["flags"].append(f"no_chat_{chat_thresh}h")
        if journal_hours and journal_hours > journal_thresh:
            result["flags"].append(f"no_journal_{journal_thresh}h")
        if activity_hours and activity_hours > activity_thresh:
            result["flags"].append(f"no_activity_{activity_thresh}h")

        return result

    # ── Prediction tracking ──

    def record_prediction(self, prediction_id: str, prediction: str, context: str = "") -> None:
        """Record an agent prediction for later validation.

        Args:
            prediction_id: Unique ID (e.g., "briefing_format_2026-04-10")
            prediction: What the agent predicts (e.g., "User will prefer shorter briefing")
            context: Why (e.g., "User skipped last 3 detailed briefings")
        """
        self._predictions.append(Prediction(
            id=prediction_id,
            prediction=prediction,
            context=context,
            made_at=local_today(),
        ))
        self.log("prediction", "made", f"{prediction}", {"id": prediction_id}, importance="low")

    def resolve_prediction(self, prediction_id: str, outcome: str, correct: bool) -> None:
        """Record the outcome of a prediction.

        Args:
            prediction_id: The prediction to resolve
            outcome: What actually happened
            correct: Whether the prediction was right
        """
        for p in self._predictions:
            if p.id == prediction_id and p.correct is None:
                p.outcome = outcome
                p.correct = correct
                p.resolved_at = local_today()
                importance = "low" if correct else "medium"
                self.log("prediction", "resolved",
                         f"{'Correct' if correct else 'Wrong'}: {p.prediction} → {outcome}",
                         {"id": prediction_id, "correct": correct},
                         importance=importance)
                return

    def get_prediction_accuracy(self) -> dict:
        """Get prediction accuracy stats."""
        resolved = [p for p in self._predictions if p.correct is not None]
        correct = sum(1 for p in resolved if p.correct)
        pending = sum(1 for p in self._predictions if p.correct is None)
        total = len(resolved)
        return {
            "total_resolved": total,
            "correct": correct,
            "wrong": total - correct,
            "pending": pending,
            "accuracy": correct / total if total > 0 else None,
            "recent_wrong": [
                {"prediction": p.prediction, "outcome": p.outcome, "date": p.made_at}
                for p in reversed(resolved)
                if not p.correct
            ][:5],
        }

    # ── Counters access ──

    def get_counters(self) -> dict[str, int | float]:
        """Get current mechanical counter values."""
        return dict(self._counters)

    # ── Daily summary for memory pipeline ──

    def get_daily_summary(self, date: str | None = None) -> str:
        """Structured activity summary for memory engine consumption.

        Returns organized text by category with counts, highlights,
        feedback signals, and silence status.
        """
        today = local_today()
        if not date:
            date = today

        # For today: use the rich in-memory buffer
        if date == today and self._buffer:
            return self._summarize_buffer()

        # For past dates: read from the observations file
        if self._journal:
            obs = self._journal.get_agent_observations(date)
            if obs:
                return obs.content

        return ""

    def _summarize_buffer(self) -> str:
        """Build a structured summary from the in-memory event buffer."""
        if not self._buffer:
            return ""

        # Group events by category
        by_category: dict[str, list[ActivityEvent]] = {}
        for event in self._buffer:
            by_category.setdefault(event.category, []).append(event)

        parts: list[str] = []

        # Stats header
        total = len(self._buffer)
        high_count = sum(1 for e in self._buffer if e.importance == "high")
        stats = f"**Activity: {total} events"
        if high_count:
            stats += f" ({high_count} high-importance)"
        stats += "**\n"
        parts.append(stats)

        # Category ordering (from activity-logging skill config)
        for cat in self._category_order:
            events = by_category.get(cat)
            if not events:
                continue

            section = self._section_map.get(cat, "Other")
            parts.append(f"\n### {section} ({len(events)} events)")

            high = [e for e in events if e.importance == "high"]
            medium = [e for e in events if e.importance == "medium"]
            low_count = sum(1 for e in events if e.importance == "low")

            for e in high:
                parts.append(f"- **{e.action}**: {e.summary}")
                if e.details:
                    key_details = ", ".join(f"{k}={v}" for k, v in list(e.details.items())[:5])
                    parts.append(f"  ({key_details})")

            for e in medium:
                parts.append(f"- {e.action}: {e.summary}")

            if low_count:
                parts.append(f"- ({low_count} routine events)")

        # Append feedback summary
        fb = self.get_feedback_summary()
        if fb["total"] > 0:
            parts.append("\n### Feedback Signals")
            parts.append(f"- Approved: {fb['approved']}, Rejected: {fb['rejected']}, Corrections: {fb['corrected']}")
            if fb["approval_rate"] is not None:
                parts.append(f"- Approval rate: {fb['approval_rate']:.0%}")
            for c in fb["corrections"]:
                parts.append(f"- Correction: {c}")
            for r in fb["rejections"]:
                parts.append(f"- Rejection: {r}")

        # Append silence status
        silence = self.get_silence_status()
        if silence["flags"]:
            parts.append("\n### Silence Flags")
            for flag in silence["flags"]:
                parts.append(f"- {flag}")

        # Append prediction accuracy
        pred = self.get_prediction_accuracy()
        if pred["total_resolved"] > 0:
            parts.append("\n### Prediction Accuracy")
            parts.append(f"- {pred['correct']}/{pred['total_resolved']} correct ({pred['accuracy']:.0%})")
            for w in pred["recent_wrong"]:
                parts.append(f"- Wrong: predicted '{w['prediction']}', actual '{w['outcome']}'")

        return "\n".join(parts)

    def get_event_counts(self, date: str | None = None) -> dict[str, int]:
        """Quick stats: {category: count} for a date."""
        today = local_today()
        if not date or date == today:
            return dict(self._counts)
        return {}

    def get_high_importance_events(self, date: str | None = None) -> list[ActivityEvent]:
        """Get only high-importance events for a date (for reflection pipeline)."""
        today = local_today()
        if not date or date == today:
            return [e for e in self._buffer if e.importance == "high"]
        return []

    def get_recent_events(
        self,
        since_iso: str | None = None,
        categories: list[str] | None = None,
        min_importance: str = "medium",
        limit: int = 50,
    ) -> list[ActivityEvent]:
        """Return events from today's buffer matching the filters.

        Used by the UI's background-work toast poller. Defaults to medium+
        importance so the user only sees events worth surfacing.
        """
        importance_rank = {"low": 0, "medium": 1, "high": 2}
        min_rank = importance_rank.get(min_importance, 1)
        cat_set = set(categories) if categories else None
        out: list[ActivityEvent] = []
        for ev in self._buffer:
            if importance_rank.get(ev.importance, 1) < min_rank:
                continue
            if cat_set and ev.category not in cat_set:
                continue
            if since_iso and ev.timestamp <= since_iso:
                continue
            out.append(ev)
        # Newest last so the UI can iterate in chronological order
        return out[-limit:]

    def get_relationship_context(self) -> str:
        """Get a compact context string for injection into the agent's system prompt.

        Includes: today's counters, feedback summary, silence flags, prediction accuracy.
        Updated every prompt rebuild (5s mtime check).
        """
        lines = []

        # Counters
        c = self._counters
        if c.get("chat_messages_today", 0):
            lines.append(f"Today: {c['chat_messages_today']} chats, {c['tool_calls_today']} tools, {c['dispatches_today']} dispatches")

        # Feedback
        fb = self.get_feedback_summary()
        if fb["total"] > 0:
            lines.append(f"Feedback: {fb['approved']} approved, {fb['rejected']} rejected, {fb['corrected']} corrections")

        # Silence
        silence = self.get_silence_status()
        if silence["flags"]:
            lines.append(f"Silence: {', '.join(silence['flags'])}")

        # Prediction accuracy
        pred = self.get_prediction_accuracy()
        if pred["total_resolved"] > 0:
            lines.append(f"Predictions: {pred['accuracy']:.0%} accuracy ({pred['total_resolved']} resolved)")

        # User overrides today (signals where the user corrected agent decisions)
        overrides = [e for e in self._buffer if e.category == "user_override"]
        if overrides:
            lines.append(f"User overrides today: {len(overrides)} (last: {overrides[-1].summary})")

        return "\n".join(lines) if lines else ""
