"""Handoff context pack service — generates compact escalation/transfer briefs.

A handoff pack answers:
- Who is the customer?
- What key profile/account facts matter?
- What is the current active issue?
- What has already been tried?
- What related issue history matters?
- What is resolved and should be deprioritized?
- What should the next agent know immediately?
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any

import tiktoken
from sqlalchemy.ext.asyncio import AsyncSession

from server.core.config import settings
from server.db import repositories as repo
from server.schemas.responses import HandoffResponse, HealthFactorResponse, ResolutionSummaryItem
from server.services import policy as policy_service
from server.services import receipts as receipts_service
from server.services.compilers.heuristic import extract_payload_text
from server.services.health import compute_health
from server.services.health_alerts import check_and_alert
from server.services.sla import compute_sla


async def assemble_handoff(
    session: AsyncSession,
    subject_id: str,
    session_id: str,
    reason: str = "escalation",
    max_tokens: int | None = None,
    tenant_id: str | None = None,
    *,
    emit_receipt: bool | None = None,
    query_id: str | None = None,
    task_id: str | None = None,
    parent_receipt_id: str | None = None,
    caller_id: str | None = None,
    caller_type: str | None = None,
) -> HandoffResponse:
    """Build a compact handoff context pack for agent escalation/transfer."""
    budget = max_tokens or 4000
    enc = tiktoken.get_encoding(settings.tiktoken_model)
    as_of = datetime.now(timezone.utc)

    # -- Fetch data ---------------------------------------------------------
    fact_rows = await repo.search_memories(
        session, subject_id, tenant_id=tenant_id, kind="profile_fact", limit=20
    )
    # `newest_first` so the cross-session "recent context" below is drawn from
    # the subject's most-recent 30 episodes, not its oldest 30. PR #205 fixed
    # the active-session path (session_episode_rows) but this subject-wide
    # fetch still bound "recent context" to the oldest-30 window.
    episode_rows = await repo.list_episodes_by_subject(
        session, subject_id, tenant_id=tenant_id, limit=30, newest_first=True
    )
    session_episode_rows = await repo.list_episodes_by_session(
        session, subject_id, session_id, tenant_id=tenant_id, limit=30
    )
    resolution_rows = await repo.list_resolutions(
        session, subject_id, tenant_id=tenant_id, limit=20
    )

    # -- Sensitivity-label policy (#50) --------------------------------------
    # Handoff briefs are sent to agents/operators downstream, so they go
    # through the same policy gate as /v1/context. The active bundle is
    # the same one /v1/context resolves; log_only vs enforce is the same
    # per-tenant flag.
    tenant_config = await receipts_service.load_tenant_receipt_config(session, tenant_id)
    policy_mode = (tenant_config or {}).get("policy_mode", "log_only")
    policy_enforce = policy_mode == "enforce"
    active_bundle = await policy_service.resolve_active_bundle(session, tenant_id)
    policy_context = policy_service.PolicyContext(
        caller_id=caller_id,
        caller_type=caller_type,
        tenant_id=tenant_id,
    )
    policy_decisions: dict[Any, policy_service.MemoryPolicyDecision] = {}
    for row in fact_rows:
        decision = policy_service.evaluate_memory(
            memory_labels=list(getattr(row, "sensitivity_labels", None) or []),
            bundle=active_bundle,
            context=policy_context,
        )
        if decision.action != "allow":
            policy_decisions[row.id] = decision
    if policy_enforce and policy_decisions:
        fact_rows, _ = policy_service.apply_decisions(fact_rows, policy_decisions, enforce=True)

    # -- Key facts ----------------------------------------------------------
    key_facts = [row.content for row in fact_rows if row.status == "active"]

    # -- Active issue (current session episodes) ----------------------------
    # Sourced from a session-scoped query, not the subject-wide list: for
    # subjects with >30 lifetime episodes the active session falls outside
    # the subject window, which would silently empty the active issue and
    # attempted steps. `episode_rows` is still used for cross-session
    # "recent context" below.
    current_session_eps = list(session_episode_rows)
    other_eps = [row for row in episode_rows if getattr(row, "session_id", None) != session_id]

    # Build active issue description from current session
    active_lines: list[str] = []
    for ep in current_session_eps[:10]:  # cap for compactness
        text = extract_payload_text(ep.payload)
        if text:
            active_lines.append(f"[{ep.source}/{ep.type}] {text[:200]}")

    active_issue = ""
    if active_lines:
        active_issue = active_lines[0]  # First message is usually the issue statement
        if len(active_lines) > 1:
            active_issue += f" (+ {len(active_lines) - 1} more messages in session)"

    # -- Attempted steps (procedure-like actions in current session) ---------
    attempted_steps: list[str] = []
    for ep in current_session_eps:
        text = extract_payload_text(ep.payload)
        if not text:
            continue
        # Heuristic: agent/system messages that look like actions
        if ep.source in ("agent", "system", "tool") or ep.type in (
            "action",
            "tool_call",
            "resolution_attempt",
        ):
            attempted_steps.append(text[:150])
        elif ep.type == "message" and ep.source == "assistant":
            # Assistant messages often contain actions taken
            attempted_steps.append(text[:150])

    # -- Resolution history -------------------------------------------------
    resolution_history: list[ResolutionSummaryItem] = []
    for r in resolution_rows:
        resolution_history.append(
            ResolutionSummaryItem(
                session_id=r.session_id,
                status=r.status,
                summary=r.resolution_summary,
                resolved_at=r.resolved_at,
            )
        )

    # -- Recent context (non-current session, most recent first) ------------
    # `episode_rows` is the subject's most-recent 30 episodes in ascending
    # order, so `other_eps` (its cross-session subset) is ascending too; take
    # the newest 5 and reverse so the most-recent cross-session episode leads,
    # matching the heading. Computed once and reused for provenance + the
    # receipt's carry-forward set so all three agree on which episodes surface.
    recent_eps = list(reversed(other_eps[-5:]))
    recent_context: list[str] = []
    for ep in recent_eps:
        text = extract_payload_text(ep.payload)
        if text:
            sid_label = f" [{ep.session_id}]" if getattr(ep, "session_id", None) else ""
            recent_context.append(f"[{ep.source}/{ep.type}]{sid_label} {text[:120]}")

    # -- Customer summary ---------------------------------------------------
    customer_summary = _build_customer_summary(key_facts, subject_id)

    # -- Health scoring -----------------------------------------------------
    health = await compute_health(session, subject_id, tenant_id=tenant_id)
    await check_and_alert(session, health, tenant_id=tenant_id)
    health_factors_top = health.factors[:3]  # Top 3 for compactness

    # -- Assemble handoff notes (readable text for next agent) --------------
    parts: list[str] = []
    parts.append(f"# Handoff Brief — {subject_id}")
    parts.append(f"Reason: {reason}")
    parts.append(f"Session: {session_id}")
    parts.append("")

    if customer_summary:
        parts.append(f"## Customer\n{customer_summary}")
        parts.append("")

    # Health section — immediately after customer, before issue details
    _state_icon = {"at_risk": "🔴", "watch": "🟡", "healthy": "🟢"}.get(health.state, "⚪")
    health_line = f"{_state_icon} Health: {health.state.upper()} (score: {health.score})"
    if health_factors_top:
        factor_strs = [f.detail for f in health_factors_top]
        health_line += " — " + "; ".join(factor_strs)
    parts.append(f"## Customer Health\n{health_line}")
    parts.append("")

    # -- SLA summary (compact, only if meaningful) --------------------------
    sla_result = await compute_sla(session, subject_id, tenant_id=tenant_id)
    sla_lines: list[str] = []
    if sla_result.total_sessions > 0:
        if sla_result.first_response_breach_count > 0:
            sla_lines.append(
                f"⚠️ First-response SLA breached in {sla_result.first_response_breach_count} session(s)"
            )
        if sla_result.resolution_breach_count > 0:
            sla_lines.append(
                f"⚠️ Resolution SLA breached in {sla_result.resolution_breach_count} session(s)"
            )
        if sla_result.open_sessions > 0:
            # Find longest open
            open_slas = [s for s in sla_result.sessions if s.status == "open"]
            if open_slas:
                max_open = max(s.open_duration_seconds or 0 for s in open_slas)
                hours = max_open / 3600
                sla_lines.append(f"Open issue age: {hours:.1f}h")
        if sla_result.avg_first_response_seconds is not None:
            avg_min = sla_result.avg_first_response_seconds / 60
            sla_lines.append(f"Avg first response: {avg_min:.1f} min")
    if sla_lines:
        parts.append("## SLA Status")
        for line in sla_lines:
            parts.append(f"- {line}")
        parts.append("")

    if active_issue:
        parts.append(f"## Active Issue\n{active_issue}")
        parts.append("")

    if attempted_steps:
        parts.append("## What Has Been Tried")
        for step in attempted_steps[:5]:
            parts.append(f"- {step}")
        parts.append("")

    if resolution_history:
        resolved = [r for r in resolution_history if r.status == "resolved"]
        unresolved = [r for r in resolution_history if r.status != "resolved"]
        if unresolved:
            parts.append("## Open Issues")
            for r in unresolved:
                parts.append(f"- [{r.session_id}] {r.summary or 'No summary'}")
        if resolved:
            parts.append("## Previously Resolved")
            for r in resolved[:3]:
                parts.append(f"- [{r.session_id}] {r.summary or 'Resolved'}")
        parts.append("")

    if key_facts:
        parts.append("## Key Facts")
        for fact in key_facts[:8]:
            parts.append(f"- {fact}")
        parts.append("")

    if recent_context:
        parts.append("## Recent Context (other sessions)")
        for line in recent_context:
            parts.append(f"- {line}")
        parts.append("")

    handoff_notes = "\n".join(parts)

    # Trim to budget if needed
    tokens = enc.encode(handoff_notes)
    if len(tokens) > budget:
        handoff_notes = enc.decode(tokens[:budget])

    token_estimate = min(len(tokens), budget)

    # -- Provenance ---------------------------------------------------------
    provenance: dict[str, Any] = {
        "fact_ids": [str(row.id) for row in fact_rows if row.status == "active"],
        "episode_ids": [str(ep.id) for ep in current_session_eps[:10]],
        "resolution_ids": [str(r.id) for r in resolution_rows],
        "context_episode_ids": [str(ep.id) for ep in recent_eps],
    }

    # -- Receipt emission ---------------------------------------------------
    # Same decision function as /v1/context. The handoff selected-entries
    # set is the rows already captured in `provenance`: the active facts,
    # the current-session episode cap, the resolution rows, and the
    # carry-forward recent-context episodes. Ranks reflect iteration
    # order, not retrieval scoring — handoff has no scoring pass.
    receipt_id, receipt_emitted = await _maybe_emit_handoff_receipt(
        session=session,
        tenant_id=tenant_id,
        subject_id=subject_id,
        session_id=session_id,
        reason=reason,
        as_of=as_of,
        handoff_notes=handoff_notes,
        token_estimate=token_estimate,
        active_fact_rows=[row for row in fact_rows if row.status == "active"],
        session_episode_rows=current_session_eps[:10],
        carry_forward_episode_rows=recent_eps,
        emit_receipt=emit_receipt,
        query_id=query_id,
        task_id=task_id,
        parent_receipt_id=parent_receipt_id,
        caller_id=caller_id,
        caller_type=caller_type,
        tenant_config=tenant_config,
        policy_bundle=active_bundle,
        policy_mode=policy_mode,
        policy_decisions=policy_decisions,
    )

    return HandoffResponse(
        subject_id=subject_id,
        session_id=session_id,
        reason=reason,
        generated_at=datetime.now(timezone.utc),
        customer_summary=customer_summary,
        active_issue=active_issue,
        attempted_steps=attempted_steps[:5],
        key_facts=key_facts[:8],
        resolution_history=resolution_history,
        recent_context=recent_context,
        health_score=health.score,
        health_state=health.state,
        health_factors=[
            HealthFactorResponse(signal=f.signal, impact=f.impact, detail=f.detail)
            for f in health_factors_top
        ],
        handoff_notes=handoff_notes,
        token_estimate=token_estimate,
        provenance=provenance,
        receipt_id=receipt_id,
        receipt_emitted=receipt_emitted,
    )


async def _maybe_emit_handoff_receipt(
    *,
    session: AsyncSession,
    tenant_id: str | None,
    subject_id: str,
    session_id: str,
    reason: str,
    as_of: datetime,
    handoff_notes: str,
    token_estimate: int,
    active_fact_rows: list[Any],
    session_episode_rows: list[Any],
    carry_forward_episode_rows: list[Any],
    emit_receipt: bool | None,
    query_id: str | None,
    task_id: str | None,
    parent_receipt_id: str | None,
    caller_id: str | None = None,
    caller_type: str | None = None,
    tenant_config: dict[str, Any] | None = None,
    policy_bundle: policy_service.PolicyBundle | None = None,
    policy_mode: str = "log_only",
    policy_decisions: dict[Any, policy_service.MemoryPolicyDecision] | None = None,
) -> tuple[str | None, bool]:
    """Decide-and-write a handoff receipt. Mirrors the /v1/context flow
    but uses `mode="retrieval"` (v1 has one mode; future handoff-specific
    modes can subdivide if needed). Receipt task text is the synthetic
    `handoff:{reason}` string so audit dashboards can filter handoffs
    cleanly from regular retrievals."""
    if tenant_config is None:
        tenant_config = await receipts_service.load_tenant_receipt_config(session, tenant_id)
    decision = receipts_service.decide_emission(
        request_flag=emit_receipt,
        tenant_config=tenant_config,
        policy_decision=None,
    )
    if not decision.emit:
        return None, False

    context_hash, context_size_bytes = receipts_service.canonicalize_context(handoff_notes)
    policy_decisions = policy_decisions or {}
    filters_applied = policy_service.build_filters_applied(policy_decisions)
    filters_skipped = policy_service.build_filters_skipped(policy_decisions, policy_bundle)

    rank = 0
    selected_memories = []
    for row in active_fact_rows:
        rank += 1
        selected_memories.append(
            receipts_service.SelectedMemory(
                memory_id=row.id,
                kind=row.kind,
                valid_from=row.valid_from,
                valid_to=row.valid_to,
                supersession_status=receipts_service.supersession_status_from_row(row),
                source_episode_ids=list(row.source_episode_ids or []),
                rank=rank,
            )
        )
    selected_episodes = []
    for row in list(session_episode_rows) + list(carry_forward_episode_rows):
        rank += 1
        selected_episodes.append(
            receipts_service.SelectedEpisode(
                episode_id=row.id,
                source=row.source,
                type=row.type,
                occurred_at=getattr(row, "occurred_at", None) or row.created_at,
                rank=rank,
            )
        )

    receipt_id = receipts_service.new_ulid()
    # v0.9 #159 — stamp the same policy snapshot the /v1/context path
    # writes so handoff receipts are replayable by /v1/receipts/{id}/replay
    # without a separate code path. Empty-bundle snapshots (null inner
    # pair) replay against the no-policy fallback. The fix landed
    # after #159 shipped — pre-fix handoff receipts will return 422
    # ``unreplayable.missing_policy_snapshot``, same contract as
    # pre-v0.9 retrieval receipts.
    policy_snapshot = receipts_service.build_policy_snapshot(
        bundle_hash=policy_bundle.bundle_hash if policy_bundle else None,
        bundle_yaml=policy_bundle.yaml_content if policy_bundle else None,
    )
    body = receipts_service.build_receipt_body(
        receipt_id=receipt_id,
        mode="retrieval",
        tenant_id=tenant_id,
        subject_id=subject_id,
        task=f"handoff:{reason}:{session_id}",
        as_of=as_of,
        selected_memories=selected_memories,
        selected_episodes=selected_episodes,
        context_hash=context_hash,
        context_size_bytes=context_size_bytes,
        token_estimate=token_estimate,
        query_id=query_id,
        task_id=task_id,
        parent_receipt_id=parent_receipt_id,
        policy_bundle_hash=policy_bundle.bundle_hash if policy_bundle else None,
        policy_mode=policy_mode,
        filters_applied=filters_applied,
        filters_skipped=filters_skipped,
        caller_id=caller_id,
        caller_type=caller_type,
        policy_snapshot=policy_snapshot,
        # v0.9 #161 — same residency stamp as the context path uses;
        # handoff receipts also record where the decision was made.
        region=settings.region,
    )
    written_id = await receipts_service.write_receipt(
        session, receipt_body=body, as_of=as_of, tenant_config=tenant_config
    )
    return (written_id, written_id is not None)


def _build_customer_summary(facts: list[str], subject_id: str) -> str:
    """Build a 1-2 line customer summary from profile facts."""
    if not facts:
        return f"Subject: {subject_id}"
    # Take first 2-3 facts as the summary
    summary_parts = facts[:3]
    return f"{subject_id} — " + "; ".join(summary_parts)
