use crate::handoff;
use crate::session_boundary::SessionBoundaryAction;
use crate::state::escalation as escalation_state;
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;
use crate::state::session_gates;

use super::{
    BehavioralDriftState, CandidateUpdate, CheckpointResult, ContextCheckAction,
    ContextCheckPayload, ContextCheckTrigger, ContextCheckUrgency, ContextHealth,
    DriftAggregateStatus, HandoffState, NextStepStatus, RadarSessionStateView, RadarStateReport,
};

pub(super) struct ContextCheckDecision {
    pub(super) action: ContextCheckAction,
    pub(super) reason: &'static str,
    pub(super) recommendation: String,
    pub(super) urgency: ContextCheckUrgency,
    pub(super) suppressed: bool,
    pub(super) next_interval_hint_seconds: u64,
    pub(super) evidence: Vec<String>,
}

/// Lightweight input struct for the context-check decision tree — can be
/// populated from either a full `RadarStateReport` (handover path) or a
/// `CheckpointResult` (lightweight path for low-value triggers like
/// interval/supervisor-poll). This decouples the decision tree from the
/// expensive `evaluate()` codepath (#627).
pub(super) struct ContextCheckInputs<'a> {
    pub(super) checkout_state: Option<&'a handoff::CheckoutStateView>,
    pub(super) escalation: &'a escalation_state::EscalationView,
    pub(super) behavioral_drift: &'a BehavioralDriftState,
    pub(super) context_health: &'a ContextHealth,
    pub(super) session_boundary_action: SessionBoundaryAction,
    pub(super) session_boundary_evidence: &'a [String],
    pub(super) next_step_status: NextStepStatus,
    pub(super) next_step_summary: String,
    pub(super) next_step_evidence: Vec<String>,
    pub(super) execution_gates: &'a session_gates::ExecutionGatesView,
    pub(super) recovery_status: &'a str,
    pub(super) handoff: &'a HandoffState,
    pub(super) candidate_updates_handoff: &'a [CandidateUpdate],
    pub(super) candidate_updates_memory_count: usize,
    pub(super) session_state: &'a RadarSessionStateView,
}

impl<'a> ContextCheckInputs<'a> {
    /// Build inputs from a full handover RadarStateReport.
    pub(super) fn from_handover(report: &'a RadarStateReport) -> Self {
        Self {
            checkout_state: report.checkout_state.as_ref(),
            escalation: &report.escalation,
            behavioral_drift: &report.behavioral_drift,
            context_health: &report.context_health,
            session_boundary_action: report.session_boundary.action,
            session_boundary_evidence: &report.session_boundary.evidence,
            next_step_status: report.evaluation.next_step.status,
            next_step_summary: report.evaluation.next_step.summary.clone(),
            next_step_evidence: report.evaluation.next_step.evidence.clone(),
            execution_gates: &report.execution_gates,
            recovery_status: report.recovery.status,
            handoff: &report.handoff,
            candidate_updates_handoff: &report.candidate_updates.handoff,
            candidate_updates_memory_count: report.candidate_updates.memory.len(),
            session_state: &report.session_state,
        }
    }

    /// Build inputs from a lightweight checkpoint — avoids the expensive
    /// `evaluate()` path. Computes `next_step_status` directly from the
    /// handoff candidates and execution gates already present in the
    /// checkpoint result (#627).
    pub(super) fn from_checkpoint(cp: &'a CheckpointResult) -> Self {
        // Replicate the next_step logic from evaluation::build_next_step_bucket
        // without requiring the full evaluation pipeline.
        let (next_step_status, next_step_summary, next_step_evidence) = if !cp
            .candidate_updates
            .handoff
            .is_empty()
        {
            (
                    NextStepStatus::ReviewRequired,
                    "Repo state changed relative to the recorded handoff; refresh the workspace-local handoff before closing.".to_owned(),
                    cp.candidate_updates
                        .handoff
                        .iter()
                        .map(|c| c.summary.clone())
                        .collect(),
                )
        } else if cp
            .execution_gates
            .attention_anchor
            .as_ref()
            .is_some_and(|a| a.status == session_gates::ExecutionGateStatus::Blocked)
        {
            (
                NextStepStatus::ReviewRequired,
                "The first unfinished execution gate is blocked.".to_owned(),
                Vec::new(),
            )
        } else if cp.execution_gates.attention_anchor.is_some() {
            (
                NextStepStatus::Continue,
                "Execution gates are active.".to_owned(),
                Vec::new(),
            )
        } else {
            (
                NextStepStatus::NoChangeDetected,
                "No deterministic CLI signal says the next step changed.".to_owned(),
                Vec::new(),
            )
        };

        Self {
            checkout_state: cp.checkout_state.as_ref(),
            escalation: &cp.escalation,
            behavioral_drift: &cp.behavioral_drift,
            context_health: &cp.context_health,
            session_boundary_action: cp.session_boundary.inner.action,
            session_boundary_evidence: &cp.session_boundary.inner.evidence,
            next_step_status,
            next_step_summary,
            next_step_evidence,
            execution_gates: &cp.execution_gates,
            recovery_status: cp.recovery.status,
            handoff: &cp.handoff,
            candidate_updates_handoff: &cp.candidate_updates.handoff,
            // Memory candidates are not computed in checkpoint-only path.
            candidate_updates_memory_count: 0,
            session_state: &cp.session_state,
        }
    }
}

pub(super) fn decide(
    trigger: ContextCheckTrigger,
    inputs: &ContextCheckInputs<'_>,
    payload: &ContextCheckPayload,
) -> ContextCheckDecision {
    if let Some(checkout_state) = inputs.checkout_state.filter(|state| state.advisory) {
        return ContextCheckDecision {
            action: ContextCheckAction::Escalate,
            reason: "checkout_advisory",
            recommendation: "seek_operator_attention".to_owned(),
            urgency: ContextCheckUrgency::High,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: vec![checkout_state.summary.clone()],
        };
    }

    if inputs.escalation.blocking_count > 0 {
        return ContextCheckDecision {
            action: ContextCheckAction::Escalate,
            reason: "blocking_escalation",
            recommendation: "seek_operator_attention".to_owned(),
            urgency: ContextCheckUrgency::Critical,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: inputs
                .escalation
                .entries
                .iter()
                .filter(|entry| matches!(entry.kind, escalation_state::EscalationKind::Blocking))
                .map(|entry| format!("{}: {}", entry.id, entry.reason))
                .collect(),
        };
    }

    if inputs.behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
        return ContextCheckDecision {
            action: ContextCheckAction::Escalate,
            reason: "behavioral_drift",
            recommendation: "recalibrate_session".to_owned(),
            urgency: ContextCheckUrgency::High,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: inputs.behavioral_drift.evidence.clone(),
        };
    }

    if matches!(
        trigger,
        ContextCheckTrigger::PreCompaction | ContextCheckTrigger::IdleReset
    ) {
        let reason = match trigger {
            ContextCheckTrigger::PreCompaction => "pre_compaction",
            ContextCheckTrigger::IdleReset => "idle_reset",
            _ => unreachable!("guarded above"),
        };
        let mut evidence = vec![payload.risk_summary.clone()];
        evidence.extend(inputs.session_boundary_evidence.iter().take(2).cloned());
        return ContextCheckDecision {
            action: ContextCheckAction::FlushForCompaction,
            reason,
            recommendation: "capture_before_compaction".to_owned(),
            urgency: ContextCheckUrgency::High,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence,
        };
    }

    if matches!(
        inputs.context_health.recommendation,
        "wrap_up_soon" | "wrap_up_and_clear"
    ) {
        return ContextCheckDecision {
            action: ContextCheckAction::WrapUpRequired,
            reason: "wrap_up_window",
            recommendation: inputs.context_health.recommendation.to_owned(),
            urgency: match inputs.context_health.recommendation {
                "wrap_up_and_clear" => ContextCheckUrgency::Critical,
                _ => ContextCheckUrgency::High,
            },
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: if inputs.session_boundary_evidence.is_empty() {
                vec![payload.risk_summary.clone()]
            } else {
                inputs.session_boundary_evidence.to_vec()
            },
        };
    }

    if inputs.session_boundary_action == SessionBoundaryAction::Refresh
        || inputs.next_step_status == NextStepStatus::ReviewRequired
    {
        return ContextCheckDecision {
            action: ContextCheckAction::CheckpointNow,
            reason: "handoff_refresh_needed",
            recommendation: "checkpoint_now".to_owned(),
            urgency: ContextCheckUrgency::Moderate,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: if inputs.next_step_evidence.is_empty() {
                vec![payload.state_delta.clone()]
            } else {
                inputs.next_step_evidence.clone()
            },
        };
    }

    let manualish_trigger = matches!(
        trigger,
        ContextCheckTrigger::Manual
            | ContextCheckTrigger::Resume
            | ContextCheckTrigger::SupervisorPoll
    );
    let moderate_pressure = matches!(inputs.context_health.band, "moderate" | "risky")
        || inputs.execution_gates.unfinished_count > 0
        || inputs.recovery_status == "loaded";
    if manualish_trigger || moderate_pressure {
        return ContextCheckDecision {
            action: ContextCheckAction::InjectDelta,
            reason: match trigger {
                ContextCheckTrigger::Resume => "resume_refresh",
                ContextCheckTrigger::SupervisorPoll => "supervisor_poll",
                ContextCheckTrigger::Manual => "manual_refresh",
                _ => "band_transition",
            },
            recommendation: match trigger {
                ContextCheckTrigger::Resume => "resume_with_digest".to_owned(),
                _ => inputs.context_health.recommendation.to_owned(),
            },
            urgency: if matches!(inputs.context_health.band, "risky") {
                ContextCheckUrgency::High
            } else {
                ContextCheckUrgency::Moderate
            },
            suppressed: false,
            next_interval_hint_seconds: if matches!(trigger, ContextCheckTrigger::Resume) {
                300
            } else {
                600
            },
            evidence: vec![payload.state_delta.clone(), payload.risk_summary.clone()],
        };
    }

    ContextCheckDecision {
        action: ContextCheckAction::None,
        reason: if trigger == ContextCheckTrigger::Interval {
            "interval_suppressed"
        } else {
            "no_action"
        },
        recommendation: "continue".to_owned(),
        urgency: ContextCheckUrgency::Low,
        suppressed: trigger == ContextCheckTrigger::Interval,
        next_interval_hint_seconds: 900,
        evidence: vec![payload.risk_summary.clone()],
    }
}

/// Backward-compatible wrapper: delegates to `decide()` via
/// `ContextCheckInputs::from_handover`. Retained for reference paths
/// that name the function.
#[allow(dead_code)]
pub(super) fn build_context_check_decision(
    trigger: ContextCheckTrigger,
    report: &RadarStateReport,
    payload: &ContextCheckPayload,
) -> ContextCheckDecision {
    let inputs = ContextCheckInputs::from_handover(report);
    decide(trigger, &inputs, payload)
}

pub(super) fn build_policy_digest(runtime: &runtime_state::LoadedRuntimeState) -> String {
    let guardrails: Vec<String> = runtime
        .state
        .handoff
        .operational_guardrails
        .iter()
        .map(|item| item.text.clone())
        .collect();
    if guardrails.is_empty() {
        return "No compiled operational guardrails are currently recorded.".to_owned();
    }

    summarize_lines(&guardrails, 2)
}

pub(super) fn build_focus_digest_from_inputs(inputs: &ContextCheckInputs<'_>) -> String {
    let mut parts = vec![inputs.handoff.title.clone()];
    if let Some(mode) = inputs
        .session_state
        .mode
        .filter(|mode| *mode != session_state::SessionMode::General)
    {
        parts.push(format!("mode `{}`", mode.as_str()));
    }
    if let Some(anchor) = &inputs.execution_gates.attention_anchor {
        parts.push(format!(
            "gate [{} #{}/{}] {}",
            anchor.status.as_str(),
            anchor.index,
            inputs.execution_gates.total_count,
            anchor.text
        ));
    } else if let Some(next_action) = inputs.handoff.immediate_actions.first() {
        parts.push(format!("next `{next_action}`"));
    }

    parts.join("; ")
}

/// Backward-compatible wrapper for full handover reports.
#[allow(dead_code)]
pub(super) fn build_focus_digest(report: &RadarStateReport) -> String {
    let inputs = ContextCheckInputs::from_handover(report);
    build_focus_digest_from_inputs(&inputs)
}

pub(super) fn build_state_delta_from_inputs(inputs: &ContextCheckInputs<'_>) -> String {
    let mut parts = Vec::new();
    if !inputs.candidate_updates_handoff.is_empty() {
        parts.push(format!(
            "{} handoff update candidate(s)",
            inputs.candidate_updates_handoff.len()
        ));
    }
    if inputs.candidate_updates_memory_count > 0 {
        parts.push(format!(
            "{} memory promotion candidate(s)",
            inputs.candidate_updates_memory_count
        ));
    }
    if let Some(anchor) = &inputs.execution_gates.attention_anchor {
        parts.push(format!(
            "execution gate [{} #{}/{}] {}",
            anchor.status.as_str(),
            anchor.index,
            inputs.execution_gates.total_count,
            anchor.text
        ));
    }
    if parts.is_empty() {
        parts.push(inputs.next_step_summary.clone());
    }
    parts.join("; ")
}

/// Backward-compatible wrapper for full handover reports.
#[allow(dead_code)]
pub(super) fn build_state_delta(report: &RadarStateReport) -> String {
    let inputs = ContextCheckInputs::from_handover(report);
    build_state_delta_from_inputs(&inputs)
}

pub(super) fn build_risk_summary_from_inputs(
    trigger: ContextCheckTrigger,
    inputs: &ContextCheckInputs<'_>,
) -> String {
    let mut parts = vec![format!(
        "trigger `{}` with context band `{}` (`{}`)",
        trigger.as_str(),
        inputs.context_health.band,
        inputs.context_health.recommendation
    )];
    if inputs.behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
        parts.push("behavioral drift needs recalibration".to_owned());
    }
    if inputs.escalation.blocking_count > 0 {
        parts.push(format!(
            "{} blocking escalation(s) active",
            inputs.escalation.blocking_count
        ));
    }
    parts.push(format!(
        "session boundary is `{}`",
        inputs.session_boundary_action.as_str()
    ));
    parts.join("; ")
}

/// Backward-compatible wrapper for full handover reports.
#[allow(dead_code)]
pub(super) fn build_risk_summary(
    trigger: ContextCheckTrigger,
    report: &RadarStateReport,
) -> String {
    let inputs = ContextCheckInputs::from_handover(report);
    build_risk_summary_from_inputs(trigger, &inputs)
}

fn summarize_lines(lines: &[String], limit: usize) -> String {
    let mut parts: Vec<String> = lines.iter().take(limit).cloned().collect();
    if lines.len() > limit {
        parts.push(format!("+{} more", lines.len() - limit));
    }
    parts.join("; ")
}
