"""
InsAIts SDK - Message History Manager
======================================
Full bidirectional message log between agents and InsAIts.

Separate from the tamper-evident audit log (audit.py).
Audit log stores hashes for compliance. Message history stores
actual content for human review and the dashboard.

Privacy modes:
    - full: content_preview + content_full stored locally
    - preview_only (default): content_preview stored, content_full NOT stored
    - hashes_only: only content_hash stored, no text at all

Storage: per-session JSONL files in .insaits_sessions/ directory.
File locking: cross-platform (msvcrt on Windows, fcntl on Linux).
"""

import hashlib
import json
import logging
import sys
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

VALID_DIRECTIONS = {"agent_to_insaits", "insaits_to_agent"}
VALID_MESSAGE_TYPES = {
    "tool_input",
    "tool_output",
    "intervention",
    "task_anchor",
    "circuit_breaker_event",
}
VALID_HISTORY_MODES = {"full", "preview_only", "hashes_only"}

PREVIEW_LENGTH = 200


@dataclass
class MessageHistoryEntry:
    """A single entry in the bidirectional message history."""

    timestamp: str
    session_id: str
    agent_id: str
    direction: str
    message_type: str
    content_hash: str
    content_preview: str = ""
    content_full: str = ""
    triggered_by: str = ""
    anomaly_severity: str = ""
    agent_next_action: str = ""
    metadata: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dict, omitting empty fields."""
        d = asdict(self)
        return {k: v for k, v in d.items() if v or k == "content_hash"}


# ---------------------------------------------------------------------------
# File locking (same cross-platform pattern as hook_runner)
# ---------------------------------------------------------------------------

def _lock_file(fh) -> None:
    """Acquire an exclusive lock on a file handle (cross-platform)."""
    try:
        if sys.platform == "win32":
            import msvcrt
            msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1)
        else:
            import fcntl
            fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
    except (IOError, OSError, ImportError):
        pass  # Best-effort


def _unlock_file(fh) -> None:
    """Release the file lock."""
    try:
        if sys.platform == "win32":
            import msvcrt
            msvcrt.locking(fh.fileno(), msvcrt.LK_UNLCK, 1)
        else:
            import fcntl
            fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
    except (IOError, OSError, ImportError):
        pass


# ---------------------------------------------------------------------------
# Content hashing
# ---------------------------------------------------------------------------

def _hash_content(content: str) -> str:
    """SHA-256 hash of message content."""
    return hashlib.sha256(content.encode("utf-8")).hexdigest()


# ---------------------------------------------------------------------------
# MessageHistoryManager
# ---------------------------------------------------------------------------

class MessageHistoryManager:
    """Session-scoped bidirectional message history writer/reader.

    Args:
        session_id: UUID for the current session.
        sessions_dir: Directory for per-session JSONL files.
        history_mode: Privacy mode — "full", "preview_only", or "hashes_only".
        retention_days: Auto-cleanup threshold (0 = never delete).
    """

    def __init__(
        self,
        session_id: str,
        sessions_dir: str = ".insaits_sessions",
        history_mode: str = "preview_only",
        retention_days: int = 30,
    ):
        if history_mode not in VALID_HISTORY_MODES:
            raise ValueError(
                f"Invalid history_mode '{history_mode}'. "
                f"Must be one of: {VALID_HISTORY_MODES}"
            )

        self._session_id = session_id
        self._sessions_dir = Path(sessions_dir)
        self._history_mode = history_mode
        self._retention_days = retention_days

        # Ensure directory exists
        self._sessions_dir.mkdir(parents=True, exist_ok=True)

        # Run cleanup on init (non-blocking, best-effort)
        if retention_days > 0:
            self._cleanup_old_sessions()

    @property
    def session_id(self) -> str:
        return self._session_id

    @property
    def history_mode(self) -> str:
        return self._history_mode

    @property
    def file_path(self) -> Path:
        return self._sessions_dir / f".insaits_message_history_{self._session_id}.jsonl"

    # ----- Write -----

    def log_tool_call(
        self,
        agent_id: str,
        tool_input: str,
        tool_output: str,
        anomaly_type: str = "",
        anomaly_severity: str = "",
        **extra_metadata: Any,
    ) -> None:
        """Log a tool call (both input and output as separate entries)."""
        if tool_input:
            self._write_entry(MessageHistoryEntry(
                timestamp=datetime.now(timezone.utc).isoformat(),
                session_id=self._session_id,
                agent_id=agent_id,
                direction="agent_to_insaits",
                message_type="tool_input",
                content_hash=_hash_content(tool_input),
                content_preview=tool_input[:PREVIEW_LENGTH],
                content_full=tool_input,
                metadata=dict(extra_metadata) if extra_metadata else {},
            ))

        if tool_output:
            self._write_entry(MessageHistoryEntry(
                timestamp=datetime.now(timezone.utc).isoformat(),
                session_id=self._session_id,
                agent_id=agent_id,
                direction="agent_to_insaits",
                message_type="tool_output",
                content_hash=_hash_content(tool_output),
                content_preview=tool_output[:PREVIEW_LENGTH],
                content_full=tool_output,
                triggered_by=anomaly_type,
                anomaly_severity=anomaly_severity,
                metadata=dict(extra_metadata) if extra_metadata else {},
            ))

    def log_intervention(
        self,
        agent_id: str,
        content: str,
        triggered_by: str = "",
        anomaly_severity: str = "",
        agent_next_action: str = "",
    ) -> None:
        """Log an InsAIts intervention injected into agent context."""
        self._write_entry(MessageHistoryEntry(
            timestamp=datetime.now(timezone.utc).isoformat(),
            session_id=self._session_id,
            agent_id=agent_id,
            direction="insaits_to_agent",
            message_type="intervention",
            content_hash=_hash_content(content),
            content_preview=content[:PREVIEW_LENGTH],
            content_full=content,
            triggered_by=triggered_by,
            anomaly_severity=anomaly_severity,
            agent_next_action=agent_next_action,
        ))

    def log_task_anchor(
        self,
        agent_id: str,
        content: str,
        anchor_type: str = "user_anchor",
    ) -> None:
        """Log a Task Anchor injection (Phase 3B integration point)."""
        self._write_entry(MessageHistoryEntry(
            timestamp=datetime.now(timezone.utc).isoformat(),
            session_id=self._session_id,
            agent_id=agent_id,
            direction="insaits_to_agent",
            message_type="task_anchor",
            content_hash=_hash_content(content),
            content_preview=content[:PREVIEW_LENGTH],
            content_full=content,
            metadata={"anchor_type": anchor_type},
        ))

    def log_circuit_breaker(
        self,
        agent_id: str,
        old_state: str,
        new_state: str,
        reason: str,
    ) -> None:
        """Log a circuit breaker state change."""
        content = f"Circuit breaker: {old_state} -> {new_state}. Reason: {reason}"
        self._write_entry(MessageHistoryEntry(
            timestamp=datetime.now(timezone.utc).isoformat(),
            session_id=self._session_id,
            agent_id=agent_id,
            direction="insaits_to_agent",
            message_type="circuit_breaker_event",
            content_hash=_hash_content(content),
            content_preview=content[:PREVIEW_LENGTH],
            content_full=content,
            metadata={"old_state": old_state, "new_state": new_state, "reason": reason},
        ))

    def _write_entry(self, entry: MessageHistoryEntry) -> None:
        """Write a single entry, applying privacy mode filtering."""
        d = entry.to_dict()

        # Apply privacy mode
        if self._history_mode == "hashes_only":
            d.pop("content_preview", None)
            d.pop("content_full", None)
        elif self._history_mode == "preview_only":
            d.pop("content_full", None)
        # "full" mode keeps everything

        try:
            with open(self.file_path, "a", encoding="utf-8") as fh:
                _lock_file(fh)
                fh.write(json.dumps(d, default=str) + "\n")
                fh.flush()
                _unlock_file(fh)
        except (IOError, OSError) as exc:
            logger.debug("Could not write message history: %s", exc)

    # ----- Read -----

    def get_message_history(
        self,
        session_id: Optional[str] = None,
        agent_id: Optional[str] = None,
        direction: Optional[str] = None,
        message_type: Optional[str] = None,
        limit: int = 100,
    ) -> List[MessageHistoryEntry]:
        """Query message history with optional filters.

        Args:
            session_id: Filter by session (None = current session).
            agent_id: Filter by agent (None = all agents).
            direction: Filter by direction (None = both).
            message_type: Filter by message type (None = all).
            limit: Max entries to return.

        Returns:
            List of MessageHistoryEntry instances, newest first.
        """
        target_session = session_id or self._session_id
        target_path = self._sessions_dir / f".insaits_message_history_{target_session}.jsonl"

        if not target_path.exists():
            return []

        entries: List[MessageHistoryEntry] = []
        try:
            with open(target_path, "r", encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        d = json.loads(line)
                    except json.JSONDecodeError:
                        continue

                    # Apply filters
                    if agent_id and d.get("agent_id") != agent_id:
                        continue
                    if direction and d.get("direction") != direction:
                        continue
                    if message_type and d.get("message_type") != message_type:
                        continue

                    entries.append(MessageHistoryEntry(
                        timestamp=d.get("timestamp", ""),
                        session_id=d.get("session_id", target_session),
                        agent_id=d.get("agent_id", ""),
                        direction=d.get("direction", ""),
                        message_type=d.get("message_type", ""),
                        content_hash=d.get("content_hash", ""),
                        content_preview=d.get("content_preview", ""),
                        content_full=d.get("content_full", ""),
                        triggered_by=d.get("triggered_by", ""),
                        anomaly_severity=d.get("anomaly_severity", ""),
                        agent_next_action=d.get("agent_next_action", ""),
                        metadata=d.get("metadata", {}),
                    ))
        except (IOError, OSError) as exc:
            logger.debug("Could not read message history: %s", exc)

        # Return newest first, limited
        entries.reverse()
        return entries[:limit]

    def get_all_entries_for_dashboard(self) -> List[Dict[str, Any]]:
        """Return all entries as dicts for the dashboard API."""
        entries = self.get_message_history(limit=500)
        return [e.to_dict() for e in entries]

    # ----- Cleanup -----

    def _cleanup_old_sessions(self) -> None:
        """Remove session files older than retention_days."""
        if self._retention_days <= 0:
            return

        cutoff = time.time() - (self._retention_days * 86400)
        try:
            for f in self._sessions_dir.glob(".insaits_message_history_*.jsonl"):
                try:
                    if f.stat().st_mtime < cutoff:
                        f.unlink()
                        logger.debug("Cleaned up old session: %s", f.name)
                except OSError:
                    pass
        except OSError:
            pass
