"""Proactive Outreach — agent-initiated contact when it notices something important.

The agent doesn't just wait for the user to come to it. When silence detection,
behavior analysis, or important events trigger an outreach, this service dispatches
a message through the user's preferred channel.

Contract-gated: requires `proactive_outreach` action (default: confirm).
"""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from agntspace.activity.logger import ActivityLogger
    from agntspace.security.contract import ContractEnforcer

log = logging.getLogger(__name__)


# Phase 4 (Rule 12) migration: outreach trigger config previously
# hardcoded as the ``_OUTREACH_TRIGGERS`` dict at module level. Now
# loaded from defaults/skills/core/proactive-outreach/config/triggers.yaml
# at server startup via ``set_outreach_triggers(...)`` from main.py.
# Same module-level dataclass + setter pattern as chunked_extract.py
# and trust/service.py.

# Documented degraded-mode defaults that mirror the pre-Phase-4
# hardcoded values exactly. Used only when the YAML is missing.
_FALLBACK_TRIGGERS = {  # arch:deterministic -- degraded-mode mirror of triggers.yaml; only consulted when the skill loader / YAML is unavailable, see Phase 4 migration docstring
    "silence_checkin": {
        "description": "User has been quiet for an extended period",
        "cooldown_hours": 48,
    },
    "important_event": {
        "description": "Something happened the user should know about",
        "cooldown_hours": 4,
    },
    "goal_stall": {
        "description": "A goal has stalled without activity",
        "cooldown_hours": 168,
    },
    "weekly_nudge": {
        "description": "Gentle weekly relationship maintenance",
        "cooldown_hours": 168,
    },
}


_OUTREACH_TRIGGERS_LIVE: dict = dict(_FALLBACK_TRIGGERS)  # arch:deterministic -- mutable runtime state populated from the proactive-outreach skill at startup; values come from triggers.yaml, see Phase 4 migration docstring


def set_outreach_triggers(cfg: dict | None) -> None:
    """Install outreach trigger config from the proactive-outreach skill.

    Called once at server startup from main.py after the SkillLoader
    has loaded triggers.yaml. The YAML's ``triggers`` block is a dict
    of ``{trigger_name: {description, cooldown_hours}}``. Empty/missing
    config keeps the pre-Phase-4 fallback so degraded mode is exact
    behavior parity.
    """
    global _OUTREACH_TRIGGERS_LIVE
    if not cfg or not isinstance(cfg, dict):
        return
    triggers_block = cfg.get("triggers")
    if not isinstance(triggers_block, dict):
        return
    new_state: dict = dict(_FALLBACK_TRIGGERS)
    for name, defn in triggers_block.items():
        if not isinstance(defn, dict):
            continue
        new_state[str(name)] = {
            "description": str(defn.get("description", "")),
            "cooldown_hours": int(defn.get("cooldown_hours", 24)),
        }
    _OUTREACH_TRIGGERS_LIVE = new_state
    log.info(
        "Outreach triggers loaded: %s",
        {k: v.get("cooldown_hours") for k, v in _OUTREACH_TRIGGERS_LIVE.items()},
    )


def get_outreach_triggers() -> dict:
    """Return the current trigger config (used by tests + diagnostics)."""
    return _OUTREACH_TRIGGERS_LIVE


class ProactiveOutreach:
    """Decides when and how to reach out to the user proactively."""

    def __init__(
        self,
        activity: ActivityLogger | None = None,
        contract: ContractEnforcer | None = None,
        channel_manager: object | None = None,
        vault_dir: Path | None = None,
    ) -> None:
        self._activity = activity
        self._contract = contract
        self._channels = channel_manager
        self._vault_dir = vault_dir
        self._last_outreach: dict[str, str] = self._load_cooldown_state()

    async def check_and_reach_out(self, agent: object | None = None) -> dict | None:
        """Check if proactive outreach is warranted and dispatch if so.

        Called by the heartbeat (every 15 minutes). Returns outreach details
        if one was sent, None otherwise.

        Wrapped in a ``outreach-*`` correlation scope (Rule #17 Tier A)
        so every decision, channel send, and activity event produced by
        this outreach attempt shares one causal chain id, distinguishable
        from heartbeat-*, channel-*, and watcher-* chains.

        Args:
            agent: The main Agent instance, used to generate the outreach message.
        """
        if not self._activity:
            return None

        # Contract gate
        if self._contract:
            result = self._contract.check("proactive_outreach")
            if not result.allowed:
                return None

        from agntspace.activity.decisions import (
            correlation_scope,
            new_correlation_id,
        )

        now = datetime.now(UTC)
        with correlation_scope(new_correlation_id("outreach")):
            # Check silence
            silence = self._activity.get_silence_status()
            if "no_activity_72h" in silence.get("flags", []):
                if self._cooldown_ok("silence_checkin", now):
                    return await self._dispatch_outreach(
                        "silence_checkin",
                        "Hey — noticed you've been quiet for a few days. Everything okay? "
                        "No pressure, just checking in.",
                        agent=agent,
                    )

            # Check high-importance events the user might not have seen
            high_events = self._activity.get_high_importance_events()
            if high_events and self._cooldown_ok("important_event", now):
                summaries = [e.summary for e in high_events[:3]]
                return await self._dispatch_outreach(
                    "important_event",
                    f"Heads up — {len(high_events)} important events today: {'; '.join(summaries)}",
                    agent=agent,
                )

            return None

    # ── Cooldown state persistence ───────────────────────────────

    @property
    def _cooldown_path(self) -> Path | None:
        if self._vault_dir:
            return Path(self._vault_dir) / ".outreach-cooldown.json"
        return None

    def _load_cooldown_state(self) -> dict[str, str]:
        """Load persisted cooldown state from disk."""
        try:
            p = self._cooldown_path
            if p and p.exists():
                return json.loads(p.read_text(encoding="utf-8"))
        except Exception:
            log.debug("Could not load outreach cooldown state, starting fresh")
        return {}

    def _save_cooldown_state(self) -> None:
        """Persist cooldown state so restarts don't violate cooldown windows."""
        try:
            p = self._cooldown_path
            if p:
                p.write_text(json.dumps(self._last_outreach), encoding="utf-8")
        except Exception:
            log.debug("Could not save outreach cooldown state", exc_info=True)

    def _cooldown_ok(self, trigger_type: str, now: datetime) -> bool:
        """Check if enough time has passed since the last outreach of this type."""
        last = self._last_outreach.get(trigger_type)
        if not last:
            return True
        try:
            last_dt = datetime.fromisoformat(last.replace("Z", "+00:00"))
            if last_dt.tzinfo is None:
                last_dt = last_dt.replace(tzinfo=UTC)
            hours = (now - last_dt).total_seconds() / 3600
            cooldown = _OUTREACH_TRIGGERS_LIVE.get(trigger_type, {}).get("cooldown_hours", 24)
            return hours >= cooldown
        except Exception:
            return True

    async def _dispatch_outreach(
        self,
        trigger_type: str,
        message: str,
        agent: object | None = None,
    ) -> dict:
        """Send an outreach message through the best available channel."""
        self._last_outreach[trigger_type] = datetime.now(UTC).isoformat()
        self._save_cooldown_state()

        # Log the outreach
        if self._activity:
            self._activity.log("system", "outreach",
                               f"Proactive outreach ({trigger_type}): {message[:80]}",
                               {"trigger": trigger_type}, importance="medium")

        # Try to dispatch via connected channel (WhatsApp preferred, then others)
        dispatched_via = None
        if self._channels and hasattr(self._channels, "get_connected"):
            connected = self._channels.get_connected()
            for ch_name in ("whatsapp", "telegram", "discord", "slack", "matrix", "signal"):
                if ch_name in connected:
                    try:
                        channel = connected[ch_name]
                        await channel.send_message(message)
                        dispatched_via = ch_name
                        break
                    except Exception:
                        log.debug("Outreach via %s failed", ch_name, exc_info=True)

        result = {
            "trigger": trigger_type,
            "message": message,
            "dispatched_via": dispatched_via,
            "timestamp": datetime.now(UTC).isoformat(),
        }

        if not dispatched_via:
            log.info("Proactive outreach queued (no channel available): %s", trigger_type)
            result["status"] = "queued"  # will show in next briefing instead
        else:
            log.info("Proactive outreach sent via %s: %s", dispatched_via, trigger_type)
            result["status"] = "sent"

        return result
