"""Handoff data models, session classifier, and velocity calculator.

Defines the state tracking models (ContextSample, HandoffState), session type
classification (SessionType), and handoff document generation for all 6 session
types. Documents are dict-based templates (no Jinja dependency).

Session classifier (classify_session): First-match-wins ordering:
  GSD > VOYAGER > BEACON > CODING > DEBUG > RESEARCH

Velocity calculator (calculate_velocity): EMA-smoothed context consumption rate
with ETA computation to target percentage.

Gap G2 (minimum-signal threshold): should_generate_handoff() prevents creating
handoff documents when there's insufficient session activity to extract
meaningful context.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from enum import Enum
from typing import Any

from .calibration import CalibrationConfig, Tier

logger = logging.getLogger(__name__)


class SessionType(Enum):
    """Classification of Claude Code session types.

    String values for direct JSON serialization.
    """

    CODING = "coding"
    DEBUG = "debug"
    GSD_EXECUTION = "gsd_execution"
    VOYAGER = "voyager"
    BEACON = "beacon"
    RESEARCH = "research"


@dataclass
class ContextSample:
    """A single context window observation point.

    Collected periodically during a session to build velocity estimation.

    Attributes:
        used_pct: Context window usage percentage (0-100).
        turn_number: Sequential turn number in the session.
        timestamp: ISO 8601 timestamp of the observation.
        velocity: Computed context consumption rate (pct/turn). None until
            CalibrationConfig.min_samples observations collected.
    """

    used_pct: float
    turn_number: int
    timestamp: str
    velocity: float | None = None


@dataclass
class HandoffState:
    """Tracks handoff readiness and document content for a session.

    Mutable because state transitions occur during the lifecycle:
    NONE -> ADVISORY -> HANDOFF -> (consumed or snoozed)

    Attributes:
        ready: Whether a handoff document is prepared and available.
        tier: Current urgency tier from calibration.
        document: The generated handoff document dict, or None.
        created_at: ISO 8601 timestamp of state creation.
        consumed: Whether the handoff was delivered to the next session.
        session_type: Type of the current session.
        snooze_until_pct: If snoozed, the usage pct to resume checking.
        snooze_count: Number of times the user has snoozed this handoff.
    """

    created_at: str
    session_type: SessionType
    ready: bool = False
    tier: Tier = Tier.NONE
    document: dict[str, Any] | None = None
    consumed: bool = False
    snooze_until_pct: float | None = None
    snooze_count: int = 0


# Minimum tool calls to consider a session "active enough" for handoff (Gap G2)
_MIN_TOOL_CALLS = 5


def should_generate_handoff(tool_count_total: int, memory_count: int) -> bool:
    """Check if session has enough activity to justify a handoff document.

    Implements Gap G2 (minimum-signal threshold): prevents creating empty or
    near-empty handoff documents when the session had minimal activity.

    A handoff is justified when EITHER:
    - Total tool calls >= 5 (session was actively working)
    - Any memories were stored (session produced durable context)

    Args:
        tool_count_total: Total number of tool calls in the session.
        memory_count: Number of memories stored during the session.

    Returns:
        True if handoff generation is warranted.
    """
    return tool_count_total >= _MIN_TOOL_CALLS or memory_count > 0


# ---------------------------------------------------------------------------
# Session classifier
# ---------------------------------------------------------------------------

# Keywords for VOYAGER session detection (case-insensitive)
_VOYAGER_KEYWORDS = ("session_type:voyager", "voyager", "research_meta")

# Keywords for BEACON session detection (case-insensitive)
_BEACON_KEYWORDS = ("session_type:beacon", "reporter_search", "beacon")

# Keywords for DEBUG session detection (case-insensitive)
_DEBUG_KEYWORDS = ("error", "bug", "traceback", "exception")

# Minimum count of debug-keyword memories to classify as DEBUG
_DEBUG_MIN_MATCHES = 2

# Threshold for CODING: sum of Write+Edit+MultiEdit must exceed this
_CODING_WRITE_THRESHOLD = 5


def classify_session(
    memories: list[dict[str, Any]],
    tool_counts: dict[str, int],
    git_branch: str | None,
) -> SessionType:
    """Classify a Claude Code session into one of 6 types.

    Uses first-match-wins ordering:
      1. GSD_EXECUTION: git_branch starts with "gsd/" (case-insensitive)
      2. VOYAGER: memory content matches voyager keywords
      3. BEACON: memory content matches beacon/reporter keywords
      4. CODING: Write+Edit+MultiEdit count > 5
      5. DEBUG: 2+ memories with error/bug/traceback/exception keywords
      6. RESEARCH: default (no other signal matches)

    All matching is defensive — malformed inputs never raise exceptions.

    Args:
        memories: List of memory dicts. Expected key: content (str).
        tool_counts: Dict mapping tool name to call count.
        git_branch: Current git branch name, or None.

    Returns:
        The classified SessionType.
    """
    try:
        # 1. GSD_EXECUTION: branch starts with "gsd/"
        if git_branch and git_branch.lower().startswith("gsd/"):
            return SessionType.GSD_EXECUTION
    except Exception:  # noqa: BLE001
        logger.debug("Failed to check git branch for GSD detection")

    # Collect lowered content from memories (once, reused for multiple checks)
    contents_lower: list[str] = []
    for m in memories:
        try:
            content = m.get("content")
            if content is not None:
                contents_lower.append(str(content).lower())
            else:
                contents_lower.append("")
        except Exception:  # noqa: BLE001
            contents_lower.append("")

    try:
        # 2. VOYAGER: memory content matches voyager keywords
        for content in contents_lower:
            for kw in _VOYAGER_KEYWORDS:
                if kw in content:
                    return SessionType.VOYAGER
    except Exception:  # noqa: BLE001
        logger.debug("Failed to check voyager keywords")

    try:
        # 3. BEACON: memory content matches beacon/reporter keywords
        for content in contents_lower:
            for kw in _BEACON_KEYWORDS:
                if kw in content:
                    return SessionType.BEACON
    except Exception:  # noqa: BLE001
        logger.debug("Failed to check beacon keywords")

    try:
        # 4. CODING: Write + Edit + MultiEdit count > threshold
        write_count = (
            tool_counts.get("Write", 0)
            + tool_counts.get("Edit", 0)
            + tool_counts.get("MultiEdit", 0)
        )
        if write_count > _CODING_WRITE_THRESHOLD:
            return SessionType.CODING
    except Exception:  # noqa: BLE001
        logger.debug("Failed to check coding write count")

    try:
        # 5. DEBUG: 2+ memories with error/bug/traceback/exception keywords
        debug_matches = 0
        for content in contents_lower:
            for kw in _DEBUG_KEYWORDS:
                if kw in content:
                    debug_matches += 1
                    break  # One keyword per memory is enough
        if debug_matches >= _DEBUG_MIN_MATCHES:
            return SessionType.DEBUG
    except Exception:  # noqa: BLE001
        logger.debug("Failed to check debug keywords")

    # 6. RESEARCH: default
    return SessionType.RESEARCH


# ---------------------------------------------------------------------------
# Velocity calculator
# ---------------------------------------------------------------------------


def calculate_velocity(
    samples: list[ContextSample],
    config: CalibrationConfig,
) -> tuple[float | None, float | None]:
    """Calculate EMA-smoothed context consumption velocity and ETA.

    Uses Exponential Moving Average over per-turn velocity measurements
    (delta_pct / delta_turns for consecutive sample pairs).

    Args:
        samples: Ordered list of context window observations.
        config: Calibration config providing ema_alpha, min_samples, target_pct.

    Returns:
        Tuple of (velocity, eta_to_target):
        - velocity: EMA-smoothed pct/turn rate, or None if insufficient samples.
        - eta_to_target: Estimated turns to reach target_pct, or None if
          velocity <= 0 or insufficient samples.
    """
    if len(samples) < config.min_samples:
        return (None, None)

    # Calculate per-pair velocities
    ema: float | None = None
    alpha = config.ema_alpha

    for i in range(1, len(samples)):
        prev = samples[i - 1]
        curr = samples[i]
        delta_turns = curr.turn_number - prev.turn_number
        if delta_turns == 0:
            continue  # Skip duplicate turn numbers
        raw_velocity = (curr.used_pct - prev.used_pct) / delta_turns

        ema = raw_velocity if ema is None else alpha * raw_velocity + (1 - alpha) * ema

    if ema is None:
        return (None, None)

    # Compute ETA from latest sample to target
    latest_pct = samples[-1].used_pct
    eta = compute_eta(latest_pct, ema, config.target_pct)

    return (ema, eta)


def compute_eta(
    current_pct: float,
    velocity: float,
    target_pct: float,
) -> float | None:
    """Compute estimated turns remaining to reach target percentage.

    Args:
        current_pct: Current context window usage (0-100).
        velocity: Context consumption rate (pct/turn).
        target_pct: Target usage percentage.

    Returns:
        Estimated turns to target, or None if velocity <= 0 (context not growing).
    """
    if velocity <= 0:
        return None
    return (target_pct - current_pct) / velocity


def check_snooze(
    state: HandoffState | None,
    current_pct: float,
) -> bool:
    """Check if handoff is currently snoozed.

    Returns True if handoff should be suppressed (still snoozed),
    False if it should proceed (not snoozed or snooze expired).

    Args:
        state: Current handoff state, or None if no state exists.
        current_pct: Current context window usage percentage.

    Returns:
        True if snoozed (suppress handoff), False otherwise.
    """
    if state is None:
        return False
    if state.snooze_until_pct is None:
        return False
    # Snooze expires when current_pct reaches or exceeds threshold
    return current_pct < state.snooze_until_pct


def generate_handoff_document(
    session_type: SessionType,
    memories: list[dict[str, Any]],
    tool_counts: dict[str, int],
    git_branch: str,
    used_pct: float,
    eta: float,
) -> dict[str, Any]:
    """Generate a handoff document for the given session type.

    Each session type has a specific template with relevant keys. The function
    extracts information from memories and tool_counts to fill template fields.
    Unknown or missing data gets safe defaults (empty lists, "unknown" strings).

    Args:
        session_type: The type of session to generate a handoff for.
        memories: List of memory dicts (raw from storage.search).
            Expected keys: content (str), category (str).
        tool_counts: Dict mapping tool name to call count.
        git_branch: Current git branch name.
        used_pct: Current context window usage percentage.
        eta: Estimated turns remaining to target.

    Returns:
        Dict with session-type-specific keys plus common metadata.
    """
    # Common metadata included in all documents
    metadata = {
        "used_pct": used_pct,
        "eta": eta,
    }

    # Extract categorized content from memories
    decisions = _extract_by_category(memories, "decision")
    lessons = _extract_by_category(memories, "lesson")
    checkpoints = _extract_by_category(memories, "checkpoint")
    all_contents = [m.get("content", "") for m in memories]

    # Dispatch to type-specific template
    generators = {
        SessionType.CODING: _template_coding,
        SessionType.DEBUG: _template_debug,
        SessionType.GSD_EXECUTION: _template_gsd_execution,
        SessionType.VOYAGER: _template_voyager,
        SessionType.BEACON: _template_beacon,
        SessionType.RESEARCH: _template_research,
    }

    generator = generators[session_type]
    doc = generator(
        memories=memories,
        decisions=decisions,
        lessons=lessons,
        checkpoints=checkpoints,
        all_contents=all_contents,
        tool_counts=tool_counts,
        git_branch=git_branch,
    )
    doc.update(metadata)
    return doc


def _extract_by_category(memories: list[dict[str, Any]], category: str) -> list[str]:
    """Extract content strings from memories matching a category."""
    return [m.get("content", "") for m in memories if m.get("category") == category]


def _template_coding(
    *,
    memories: list[dict[str, Any]],
    decisions: list[str],
    lessons: list[str],
    checkpoints: list[str],
    all_contents: list[str],
    tool_counts: dict[str, int],
    git_branch: str,
) -> dict[str, Any]:
    """Generate CODING session handoff template."""
    # Extract file-related info from tool counts
    write_ops = tool_counts.get("Write", 0) + tool_counts.get("Edit", 0)
    files_modified = [
        m.get("content", "") for m in memories if m.get("category") == "file_ownership"
    ]

    return {
        "type": "coding",
        "branch": git_branch,
        "files_modified": files_modified,
        "decisions": decisions,
        "next_steps": _derive_next_steps(checkpoints, lessons),
        "write_operations": write_ops,
    }


def _template_debug(
    *,
    memories: list[dict[str, Any]],
    decisions: list[str],
    lessons: list[str],
    checkpoints: list[str],
    all_contents: list[str],
    tool_counts: dict[str, int],
    git_branch: str,
) -> dict[str, Any]:
    """Generate DEBUG session handoff template."""
    # Try to identify symptom from first checkpoint or content
    symptom = checkpoints[0] if checkpoints else "unknown"

    # Files investigated = those read during debugging
    read_count = tool_counts.get("Read", 0)

    return {
        "type": "debug",
        "symptom": symptom,
        "hypotheses": lessons,
        "files_investigated": [
            m.get("content", "") for m in memories if m.get("category") == "file_ownership"
        ],
        "next_steps": _derive_next_steps(checkpoints, lessons),
        "read_operations": read_count,
    }


def _template_gsd_execution(
    *,
    memories: list[dict[str, Any]],
    decisions: list[str],
    lessons: list[str],
    checkpoints: list[str],
    all_contents: list[str],
    tool_counts: dict[str, int],
    git_branch: str,
) -> dict[str, Any]:
    """Generate GSD_EXECUTION session handoff template."""
    # GSD context from session summaries
    summaries = _extract_by_category(memories, "session_summary")

    return {
        "type": "gsd_execution",
        "phase": "unknown",
        "plan_file": "unknown",
        "current_task": "unknown",
        "completed_tasks": summaries,
        "wave": "unknown",
        "next_steps": _derive_next_steps(checkpoints, decisions),
    }


def _template_voyager(
    *,
    memories: list[dict[str, Any]],
    decisions: list[str],
    lessons: list[str],
    checkpoints: list[str],
    all_contents: list[str],
    tool_counts: dict[str, int],
    git_branch: str,
) -> dict[str, Any]:
    """Generate VOYAGER session handoff template."""
    return {
        "type": "voyager",
        "method": "unknown",
        "topic": "unknown",
        "findings": lessons,
        "sources_consulted": [],
        "next_steps": _derive_next_steps(checkpoints, decisions),
    }


def _template_beacon(
    *,
    memories: list[dict[str, Any]],
    decisions: list[str],
    lessons: list[str],
    checkpoints: list[str],
    all_contents: list[str],
    tool_counts: dict[str, int],
    git_branch: str,
) -> dict[str, Any]:
    """Generate BEACON session handoff template."""
    return {
        "type": "beacon",
        "query_context": "unknown",
        "reports_generated": [],
        "key_findings": lessons,
        "next_steps": _derive_next_steps(checkpoints, decisions),
    }


def _template_research(
    *,
    memories: list[dict[str, Any]],
    decisions: list[str],
    lessons: list[str],
    checkpoints: list[str],
    all_contents: list[str],
    tool_counts: dict[str, int],
    git_branch: str,
) -> dict[str, Any]:
    """Generate RESEARCH session handoff template."""
    return {
        "type": "research",
        "topic": "unknown",
        "findings": lessons,
        "questions_open": [],
        "sources_consulted": [],
        "next_steps": _derive_next_steps(checkpoints, decisions),
    }


def _derive_next_steps(checkpoints: list[str], context: list[str]) -> list[str]:
    """Derive next steps from checkpoints and contextual information.

    Uses the most recent checkpoint as primary next-step indicator.
    Falls back to context (decisions/lessons) if no checkpoints.

    Returns:
        List of next step descriptions. Empty list if no signal.
    """
    if checkpoints:
        # Most recent checkpoint is likely the best continuation point
        return [checkpoints[-1]]
    if context:
        return [context[-1]]
    return []
