// Host adapters are selected by feature and platform hooks, so some
// definitions stay dormant in the default kernel-only build.

use std::path::Path;

use anyhow::Result;
use serde::Serialize;
use serde_json::Value;

use crate::commands::start;
use crate::db;
use crate::memory::evidence::{
    self, MemoryEvidenceEnvelope, MemoryEvidenceSubmitReport, SubmitRuntimeHints,
};
use crate::output::CommandReport;
use crate::output::{self, CHECKPOINT_FIELD_FILTER_SPEC};
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo;
use crate::session_boundary::SessionBoundaryRecommendation;
use crate::state::session;
use crate::state::{
    escalation as escalation_state, protected_write::ExclusiveWriteOptions, radar,
    runtime as runtime_state, work_stream_decay,
};
use crate::telemetry::host::HostContextSnapshot;
use crate::telemetry::host_loop::{
    self, ContextDiagnosticsView, ContextSessionSnapshot, ContextTelemetryView,
};

const ON_SESSION_START: &str = "on_session_start";
const BEFORE_PROMPT_BUILD: &str = "before_prompt_build";
const ON_COMPACTION_NOTICE: &str = "on_compaction_notice";
const ON_AGENT_END: &str = "on_agent_end";
const ON_SESSION_END: &str = "on_session_end";
const SUPERVISOR_TICK: &str = "supervisor_tick";
const CHECKPOINT_FIELDS: &[&str] = &[
    "command",
    "mode",
    "session_boundary",
    "session_state",
    "execution_gates",
    "escalation",
    "projection_telemetry",
    "work_stream_decay",
    "context_health",
    "behavioral_drift",
    "candidate_updates",
];

pub(crate) struct HookInput<'a> {
    pub(crate) repo_root: &'a Path,
    pub(crate) profile: &'a str,
    pub(crate) protected_write: &'a ExclusiveWriteOptions,
    pub(crate) session_start: HostSessionStartOptions,
    pub(crate) diagnostics: bool,
    pub(crate) host_total_context_chars: Option<u64>,
    pub(crate) attempt_outcome: Option<work_stream_decay::AttemptOutcome>,
}

#[derive(Clone)]
pub(crate) struct HostSessionStartOptions {
    pub(crate) mode: Option<session::SessionMode>,
    pub(crate) lifecycle: session::SessionLifecycle,
    pub(crate) owner_kind: Option<session::SessionOwnerKind>,
    pub(crate) actor_id: Option<String>,
    pub(crate) supervisor_id: Option<String>,
    pub(crate) lease_ttl_secs: Option<u64>,
    pub(crate) host_session_id: Option<String>,
    pub(crate) host_run_id: Option<String>,
    pub(crate) host_task_id: Option<String>,
}

impl Default for HostSessionStartOptions {
    fn default() -> Self {
        Self {
            mode: None,
            lifecycle: session::SessionLifecycle::Interactive,
            owner_kind: None,
            actor_id: None,
            supervisor_id: None,
            lease_ttl_secs: None,
            host_session_id: None,
            host_run_id: None,
            host_task_id: None,
        }
    }
}

impl HostSessionStartOptions {
    fn into_session_start_options(self) -> session::SessionStartOptions {
        session::SessionStartOptions {
            mode: self.mode,
            lifecycle: self.lifecycle,
            owner_kind: self.owner_kind,
            actor_id: self.actor_id,
            supervisor_id: self.supervisor_id,
            lease_ttl_secs: self.lease_ttl_secs,
            recover_stale: false,
        }
    }

    fn runtime_view(&self) -> Option<HostRuntimeView> {
        if self.host_session_id.is_none()
            && self.host_run_id.is_none()
            && self.host_task_id.is_none()
        {
            return None;
        }

        Some(HostRuntimeView {
            host_session_id: self.host_session_id.clone(),
            host_run_id: self.host_run_id.clone(),
            host_task_id: self.host_task_id.clone(),
        })
    }
}

#[derive(Serialize)]
pub(crate) struct HostRuntimeView {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) host_session_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) host_run_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) host_task_id: Option<String>,
}

pub(crate) trait HostAdapter: Sync {
    fn name(&self) -> &'static str;
    fn snapshot(&self, repo_root: &Path) -> Result<Option<HostContextSnapshot>>;

    fn on_session_start(&self, input: HookInput<'_>) -> Result<HostHookReport> {
        handle_session_start(self.name(), input)
    }

    fn before_prompt_build(&self, input: HookInput<'_>) -> Result<HostHookReport> {
        handle_before_prompt_build(self.name(), input)
    }

    fn on_compaction_notice(
        &self,
        input: CompactionNoticeInput<'_>,
    ) -> Result<CompactionNoticeReport> {
        handle_compaction_notice(self.name(), input)
    }

    fn on_agent_end(&self, input: HookInput<'_>) -> Result<HostHookReport> {
        handle_agent_end(self.name(), input)
    }

    fn on_session_end(&self, input: HookInput<'_>) -> Result<HostHookReport> {
        handle_session_end(self.name(), input)
    }

    fn supervisor_tick(&self, input: HookInput<'_>) -> Result<HostHookReport> {
        handle_supervisor_tick(self.name(), input)
    }
}

#[derive(Serialize)]
pub(crate) struct HostHookReport {
    pub(crate) adapter: &'static str,
    pub(crate) hook: &'static str,
    pub(crate) status: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) detail: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) session_boundary: Option<SessionBoundaryRecommendation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) disposition: Option<start::StartupDisposition>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) compact_summary: Option<start::CompactSummary>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) context: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) telemetry: Option<ContextTelemetryView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) diagnostics: Option<ContextDiagnosticsView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) remember_selection: Option<start::RememberSelectionView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) runtime: Option<HostRuntimeView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) checkpoint: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) session_start: Option<session::SessionStateStartReport>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) session_clear: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) session_heartbeat: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) context_check: Option<radar::ContextCheckReport>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) recovery: Option<runtime_state::RuntimeRecoveryView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) escalation: Option<escalation_state::EscalationView>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub(crate) alerts: Vec<start::StartAlert>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub(crate) warnings: Vec<String>,
}

pub(crate) struct CompactionNoticeInput<'a> {
    pub(crate) repo_root: &'a Path,
    pub(crate) profile: Option<&'a str>,
    pub(crate) capture: Option<MemoryEvidenceEnvelope>,
    pub(crate) protected_write: ExclusiveWriteOptions,
    pub(crate) runtime_hints: SubmitRuntimeHints,
}

#[derive(Serialize)]
pub(crate) struct CompactionNoticeReport {
    pub(crate) adapter: &'static str,
    pub(crate) hook: &'static str,
    pub(crate) status: &'static str,
    pub(crate) context_check: radar::ContextCheckReport,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) capture: Option<MemoryEvidenceSubmitReport>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub(crate) warnings: Vec<String>,
}

#[derive(Serialize)]
pub(crate) struct HostHookRunReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    host: String,
    hook: String,
    status: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    detail: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    session_boundary: Option<SessionBoundaryRecommendation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    disposition: Option<start::StartupDisposition>,
    #[serde(skip_serializing_if = "Option::is_none")]
    compact_summary: Option<start::CompactSummary>,
    #[serde(skip_serializing_if = "Option::is_none")]
    context: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    telemetry: Option<ContextTelemetryView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    diagnostics: Option<ContextDiagnosticsView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    remember_selection: Option<start::RememberSelectionView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    runtime: Option<HostRuntimeView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    checkpoint: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    session_start: Option<session::SessionStateStartReport>,
    #[serde(skip_serializing_if = "Option::is_none")]
    session_clear: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    session_heartbeat: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    context_check: Option<radar::ContextCheckReport>,
    #[serde(skip_serializing_if = "Option::is_none")]
    recovery: Option<runtime_state::RuntimeRecoveryView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    escalation: Option<escalation_state::EscalationView>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    alerts: Vec<start::StartAlert>,
    #[serde(skip_serializing_if = "Option::is_none")]
    capture: Option<MemoryEvidenceSubmitReport>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    warnings: Vec<String>,
}

impl HostHookRunReport {
    pub(crate) fn context(&self) -> Option<&serde_json::Value> {
        self.context.as_ref()
    }

    pub(crate) fn session_boundary_action(&self) -> Option<&'static str> {
        self.session_boundary.as_ref().map(|b| b.action.as_str())
    }
}

impl CommandReport for HostHookRunReport {
    fn exit_code(&self) -> std::process::ExitCode {
        std::process::ExitCode::SUCCESS
    }

    fn render_text(&self) {
        println!("{} {} -> {}", self.host, self.hook, self.status);
        if let Some(detail) = &self.detail {
            println!("{detail}");
        }
        render_section("session boundary", self.session_boundary.as_ref());
        render_section("disposition", self.disposition.as_ref());
        render_section("compact summary", self.compact_summary.as_ref());
        render_section("context", self.context.as_ref());
        render_section("telemetry", self.telemetry.as_ref());
        render_section("diagnostics", self.diagnostics.as_ref());
        render_section("remember selection", self.remember_selection.as_ref());
        render_section("runtime", self.runtime.as_ref());
        render_section("checkpoint", self.checkpoint.as_ref());
        render_section("session start", self.session_start.as_ref());
        render_section("session clear", self.session_clear.as_ref());
        render_section("session heartbeat", self.session_heartbeat.as_ref());
        render_section("recovery", self.recovery.as_ref());
        render_section("escalation", self.escalation.as_ref());
        render_section("alerts", (!self.alerts.is_empty()).then_some(&self.alerts));
        if let Some(context_check) = &self.context_check {
            println!();
            context_check.render_text();
        }
        if let Some(capture) = &self.capture {
            println!();
            capture.render_text();
        }
        for warning in &self.warnings {
            println!("warning: {warning}");
        }
    }
}

pub(crate) fn named(name: &str) -> Option<&'static dyn HostAdapter> {
    let canonical = match name {
        "claude-code" => "claude",
        "open-code" => "opencode",
        other => other,
    };
    all()
        .into_iter()
        .find(|adapter| adapter.name() == canonical)
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn run_hook(
    repo_root: &Path,
    profile: &str,
    adapter_name: &str,
    hook: &str,
    session_start: HostSessionStartOptions,
    diagnostics: bool,
    host_total_context_chars: Option<u64>,
    attempt_outcome: Option<work_stream_decay::AttemptOutcome>,
    capture: Option<MemoryEvidenceEnvelope>,
    protected_write: ExclusiveWriteOptions,
    runtime_hints: SubmitRuntimeHints,
) -> Result<HostHookRunReport> {
    let adapter = named(adapter_name)
        .ok_or_else(|| anyhow::anyhow!("unknown host adapter `{adapter_name}`"))?;
    if attempt_outcome.is_some() && hook != ON_AGENT_END {
        anyhow::bail!("--attempt-outcome is only supported for --hook on-agent-end");
    }

    let mut report = HostHookRunReport {
        command: "host-hook",
        ok: true,
        path: repo_root.display().to_string(),
        profile: profile.to_owned(),
        host: adapter.name().to_owned(),
        hook: hook.to_owned(),
        status: String::new(),
        detail: None,
        session_boundary: None,
        disposition: None,
        compact_summary: None,
        context: None,
        telemetry: None,
        diagnostics: None,
        remember_selection: None,
        runtime: None,
        checkpoint: None,
        session_start: None,
        session_clear: None,
        session_heartbeat: None,
        context_check: None,
        recovery: None,
        escalation: None,
        alerts: Vec::new(),
        capture: None,
        warnings: Vec::new(),
    };

    match hook {
        ON_SESSION_START => {
            let hook_report = adapter.on_session_start(HookInput {
                repo_root,
                profile,
                protected_write: &protected_write,
                session_start,
                diagnostics,
                host_total_context_chars,
                attempt_outcome,
            })?;
            apply_hook_report(&mut report, hook_report);
        }
        BEFORE_PROMPT_BUILD => {
            let hook_report = adapter.before_prompt_build(HookInput {
                repo_root,
                profile,
                protected_write: &protected_write,
                session_start,
                diagnostics,
                host_total_context_chars,
                attempt_outcome,
            })?;
            apply_hook_report(&mut report, hook_report);
        }
        ON_COMPACTION_NOTICE => {
            let hook_report = adapter.on_compaction_notice(CompactionNoticeInput {
                repo_root,
                profile: Some(profile),
                capture,
                protected_write,
                runtime_hints,
            })?;
            report.hook = hook_report.hook.to_owned();
            report.status = hook_report.status.to_owned();
            report.context_check = Some(hook_report.context_check);
            report.capture = hook_report.capture;
            report.warnings = hook_report.warnings;
        }
        ON_AGENT_END => {
            let hook_report = adapter.on_agent_end(HookInput {
                repo_root,
                profile,
                protected_write: &protected_write,
                session_start,
                diagnostics,
                host_total_context_chars,
                attempt_outcome,
            })?;
            apply_hook_report(&mut report, hook_report);
        }
        ON_SESSION_END => {
            let hook_report = adapter.on_session_end(HookInput {
                repo_root,
                profile,
                protected_write: &protected_write,
                session_start,
                diagnostics,
                host_total_context_chars,
                attempt_outcome,
            })?;
            apply_hook_report(&mut report, hook_report);
        }
        SUPERVISOR_TICK => {
            let hook_report = adapter.supervisor_tick(HookInput {
                repo_root,
                profile,
                protected_write: &protected_write,
                session_start,
                diagnostics,
                host_total_context_chars,
                attempt_outcome,
            })?;
            apply_hook_report(&mut report, hook_report);
        }
        _ => anyhow::bail!("unknown host hook `{hook}`"),
    }

    Ok(report)
}

pub(crate) fn all() -> [&'static dyn HostAdapter; 6] {
    [
        &CODEX_ADAPTER,
        &CLAUDE_ADAPTER,
        &GEMINI_ADAPTER,
        &OPENCODE_ADAPTER,
        &OPENCLAW_ADAPTER,
        &HERMES_ADAPTER,
    ]
}

fn handle_compaction_notice(
    adapter: &'static str,
    input: CompactionNoticeInput<'_>,
) -> Result<CompactionNoticeReport> {
    let context_check = radar::run_context_check(
        input.repo_root,
        input.profile,
        radar::ContextCheckTrigger::PreCompaction,
    )?;
    let mut warnings = Vec::new();
    let capture = match input.capture {
        Some(envelope) => match evidence::submit(
            input.repo_root,
            input.profile,
            envelope,
            input.protected_write,
            &input.runtime_hints,
        ) {
            Ok(report) => Some(report),
            Err(error) => {
                warnings.push(format!(
                    "best-effort compaction capture failed after one submit attempt: {error}"
                ));
                None
            }
        },
        None => None,
    };

    Ok(CompactionNoticeReport {
        adapter,
        hook: ON_COMPACTION_NOTICE,
        status: "bounded",
        context_check,
        capture,
        warnings,
    })
}

fn apply_hook_report(report: &mut HostHookRunReport, hook_report: HostHookReport) {
    report.hook = hook_report.hook.to_owned();
    report.status = hook_report.status.to_owned();
    report.detail = hook_report.detail;
    report.session_boundary = hook_report.session_boundary;
    report.disposition = hook_report.disposition;
    report.compact_summary = hook_report.compact_summary;
    report.context = hook_report.context;
    report.telemetry = hook_report.telemetry;
    report.diagnostics = hook_report.diagnostics;
    report.remember_selection = hook_report.remember_selection;
    report.runtime = hook_report.runtime;
    report.checkpoint = hook_report.checkpoint;
    report.session_start = hook_report.session_start;
    report.session_clear = hook_report.session_clear;
    report.session_heartbeat = hook_report.session_heartbeat;
    report.context_check = hook_report.context_check;
    report.recovery = hook_report.recovery;
    report.escalation = hook_report.escalation;
    report.alerts = hook_report.alerts;
    report.warnings.extend(hook_report.warnings);
}

fn session_snapshot_from_start_report(
    report: &session::SessionStateStartReport,
) -> ContextSessionSnapshot {
    ContextSessionSnapshot {
        session_id: report.session_id().map(str::to_owned),
        started_at_epoch_s: Some(report.started_at_epoch_s()),
        last_started_at_epoch_s: Some(report.last_started_at_epoch_s()),
        start_count: Some(report.start_count()),
    }
}

#[allow(clippy::too_many_arguments)]
fn observe_context(
    repo_root: &Path,
    profile_name: &str,
    host: &str,
    hook: &str,
    status: &str,
    session_boundary: &SessionBoundaryRecommendation,
    source_fingerprint: &str,
    context: &Value,
    sections: &[host_loop::ContextSectionSpec],
    session: &ContextSessionSnapshot,
    host_total_context_chars: Option<u64>,
    include_diagnostics: bool,
) -> Result<(
    ContextTelemetryView,
    Option<ContextDiagnosticsView>,
    Option<String>,
)> {
    let profile = profile::resolve(Some(profile_name))?;
    let layout = StateLayout::resolve(repo_root, profile)?;
    let previous = db::host_loop::latest_for_host_hook(&layout.state_db_path(), host, hook)
        .ok()
        .flatten();
    let observation = host_loop::build_context_observation(
        session::now_epoch_s()?,
        host,
        hook,
        status,
        Some(session_boundary.action.as_str()),
        source_fingerprint,
        context,
        sections,
        previous.as_ref(),
        session,
        host_total_context_chars,
    )?;

    let mut warning = None;
    match db::StateDb::open_for_layout(&layout) {
        Ok(state_db) => {
            if let Err(error) = db::host_loop::insert(state_db.conn(), &observation.record) {
                warning = Some(format!("failed to persist host-loop telemetry: {error}"));
            }
        }
        Err(error) => {
            warning = Some(format!(
                "failed to open state DB for host-loop telemetry: {error}"
            ));
        }
    }

    Ok((
        observation.telemetry,
        include_diagnostics.then_some(observation.diagnostics),
        warning,
    ))
}

fn handle_session_start(adapter: &'static str, input: HookInput<'_>) -> Result<HostHookReport> {
    let locality_id = repo::marker::load(input.repo_root)?.map(|marker| marker.locality_id);
    let prompt_context_payload = start::build_host_context(
        input.repo_root,
        Some(input.profile),
        true,
        start::HostContextMode::Startup,
    )?;
    let runtime = input.session_start.runtime_view();
    let session_start = session::start(
        input.repo_root,
        Some(input.profile),
        locality_id.as_deref(),
        input.session_start.clone().into_session_start_options(),
    )?;
    let mut warnings = Vec::new();
    if let Some(warning) = session_start.warning_message() {
        warnings.push(warning.to_owned());
    }
    let telemetry_session_boundary = prompt_context_payload.session_boundary.clone();
    let (telemetry, diagnostics) = match observe_context(
        input.repo_root,
        input.profile,
        adapter,
        ON_SESSION_START,
        "session_bootstrapped",
        &telemetry_session_boundary,
        &prompt_context_payload.source_fingerprint,
        &prompt_context_payload.context,
        &prompt_context_payload.sections,
        &session_snapshot_from_start_report(&session_start),
        input.host_total_context_chars,
        input.diagnostics,
    ) {
        Ok((telemetry, diagnostics, warning)) => {
            if let Some(warning) = warning {
                warnings.push(warning);
            }
            (Some(telemetry), diagnostics)
        }
        Err(error) => {
            warnings.push(format!(
                "best-effort context telemetry unavailable: {error}"
            ));
            (None, None)
        }
    };
    let operator_summary = start::build_host_operator_summary(
        input.repo_root,
        Some(input.profile),
        true,
        &prompt_context_payload.alerts,
    )?;
    let context_check = match radar::run_context_check(
        input.repo_root,
        Some(input.profile),
        radar::ContextCheckTrigger::Resume,
    ) {
        Ok(report) => Some(report),
        Err(error) => {
            warnings.push(format!(
                "best-effort resume context check failed after bootstrap: {error}"
            ));
            None
        }
    };

    Ok(HostHookReport {
        adapter,
        hook: ON_SESSION_START,
        status: "session_bootstrapped",
        detail: Some(
            "bounded startup context is ready and CCD session bootstrap has been recorded"
                .to_owned(),
        ),
        session_boundary: Some(operator_summary.session_boundary),
        disposition: Some(operator_summary.disposition),
        compact_summary: Some(operator_summary.compact_summary),
        context: Some(prompt_context_payload.context),
        telemetry,
        diagnostics,
        remember_selection: input
            .diagnostics
            .then_some(prompt_context_payload.remember_selection)
            .flatten(),
        runtime,
        checkpoint: None,
        session_start: Some(session_start),
        session_clear: None,
        session_heartbeat: None,
        context_check,
        recovery: Some(operator_summary.recovery),
        escalation: Some(operator_summary.escalation),
        alerts: operator_summary.alerts,
        warnings,
    })
}

fn handle_before_prompt_build(
    adapter: &'static str,
    input: HookInput<'_>,
) -> Result<HostHookReport> {
    // --- cache-hit pre-check: compare quick source fingerprint against the
    //     last observed fingerprint for this adapter+hook.  When unchanged,
    //     return a compact no-op response keyed by source_fingerprint and
    //     skip the expensive build_host_context pipeline (#627). ---
    if let Some(previous) =
        try_previous_prompt_build_fingerprint(input.repo_root, input.profile, adapter)
    {
        let profile = profile::resolve(Some(input.profile))?;
        let layout = StateLayout::resolve(input.repo_root, profile)?;
        let marker = repo::marker::load(input.repo_root)?
            .ok_or_else(|| anyhow::anyhow!("repo not linked"))?;
        let pod_active =
            crate::state::machine_registry::resolve_active_identity(&layout)?.is_some();
        let raw = runtime_state::load_raw_start_runtime_sources(
            input.repo_root,
            &layout,
            &marker.locality_id,
            pod_active,
        )?;
        let surfaces = runtime_state::RuntimeSourceSurfaces {
            profile_memory: raw.profile_memory,
            locality_memory: raw.locality_memory,
            branch_memory: raw.branch_memory,
            clone_memory: raw.clone_memory,
            execution_gates: raw.execution_gates.source,
            handoff: raw.handoff,
        };
        let current_fp = crate::state::compiled::fingerprint_for_inputs(&surfaces, pod_active)?;
        if current_fp == previous {
            let warnings = record_prompt_build_cache_hit(
                &layout,
                adapter,
                &current_fp,
                input.host_total_context_chars,
            )
            .into_iter()
            .collect();
            return Ok(HostHookReport {
                adapter,
                hook: BEFORE_PROMPT_BUILD,
                status: "unchanged",
                detail: Some(format!(
                    "source fingerprint unchanged ({current_fp}); no fresh context needed"
                )),
                session_boundary: None,
                disposition: None,
                compact_summary: None,
                context: None,
                telemetry: None,
                diagnostics: None,
                remember_selection: None,
                runtime: None,
                checkpoint: None,
                session_start: None,
                session_clear: None,
                session_heartbeat: None,
                context_check: None,
                recovery: None,
                escalation: None,
                alerts: Vec::new(),
                warnings,
            });
        }
    }

    // --- full path: build context + observe ---
    let context_payload = start::build_host_context(
        input.repo_root,
        Some(input.profile),
        false,
        start::HostContextMode::PromptBuild,
    )?;
    let mut warnings = Vec::new();
    let (telemetry, diagnostics) = match observe_context(
        input.repo_root,
        input.profile,
        adapter,
        BEFORE_PROMPT_BUILD,
        "bounded_context",
        &context_payload.session_boundary,
        &context_payload.source_fingerprint,
        &context_payload.context,
        &context_payload.sections,
        &context_payload.session,
        input.host_total_context_chars,
        input.diagnostics,
    ) {
        Ok((telemetry, diagnostics, warning)) => {
            if let Some(warning) = warning {
                warnings.push(warning);
            }
            (Some(telemetry), diagnostics)
        }
        Err(error) => {
            warnings.push(format!(
                "best-effort context telemetry unavailable: {error}"
            ));
            (None, None)
        }
    };
    Ok(HostHookReport {
        adapter,
        hook: BEFORE_PROMPT_BUILD,
        status: "bounded_context",
        detail: Some("bounded CCD prompt-build context is ready for host injection".to_owned()),
        session_boundary: Some(context_payload.session_boundary),
        disposition: None,
        compact_summary: None,
        context: Some(context_payload.context),
        telemetry,
        diagnostics,
        remember_selection: input
            .diagnostics
            .then_some(context_payload.remember_selection)
            .flatten(),
        runtime: None,
        checkpoint: None,
        session_start: None,
        session_clear: None,
        session_heartbeat: None,
        context_check: None,
        recovery: None,
        escalation: None,
        alerts: Vec::new(),
        warnings,
    })
}

/// Read the last-observed source_fingerprint for this adapter's
/// before_prompt_build hook from host_loop_events.  Returns None when
/// the DB is missing, the table doesn't exist, or no record matches.
fn try_previous_prompt_build_fingerprint(
    repo_root: &Path,
    profile_name: &str,
    adapter: &str,
) -> Option<String> {
    let profile = profile::resolve(Some(profile_name)).ok()?;
    let layout = StateLayout::resolve(repo_root, profile).ok()?;
    let record =
        db::host_loop::latest_for_host_hook(&layout.state_db_path(), adapter, BEFORE_PROMPT_BUILD)
            .ok()
            .flatten()?;
    record.source_fingerprint
}

/// Record a lightweight host-loop event for a before_prompt_build cache hit.
/// This preserves observability of prompt-build hook invocations while keeping
/// the expensive bounded-context construction and payload emission skipped.
fn record_prompt_build_cache_hit(
    layout: &StateLayout,
    adapter: &str,
    source_fingerprint: &str,
    host_total_context_chars: Option<u64>,
) -> Option<String> {
    let record = db::host_loop::HostLoopEventRecord {
        _id: None,
        observed_at_epoch_s: session::now_epoch_s().ok()?,
        session_id: session::load_session_id(layout).ok().flatten(),
        host: adapter.to_owned(),
        hook: BEFORE_PROMPT_BUILD.to_owned(),
        status: "unchanged".to_owned(),
        session_boundary_action: None,
        source_fingerprint: Some(source_fingerprint.to_owned()),
        normalized_payload_hash: None,
        payload_chars: None,
        payload_estimated_tokens: None,
        host_total_context_chars,
        overhead_ratio: None,
        session_started_at_epoch_s: None,
        session_last_started_at_epoch_s: None,
        session_start_count: None,
        section_metrics_json: None,
    };
    match db::StateDb::open_for_layout(layout) {
        Ok(state_db) => db::host_loop::insert(state_db.conn(), &record)
            .err()
            .map(|error| format!("failed to persist host-loop cache-hit telemetry: {error}")),
        Err(error) => Some(format!(
            "failed to open state DB for host-loop cache-hit telemetry: {error}"
        )),
    }
}

fn handle_agent_end(adapter: &'static str, input: HookInput<'_>) -> Result<HostHookReport> {
    // Record the attempt outcome BEFORE building the checkpoint payload so
    // the returned checkpoint reflects the just-recorded decay state. For
    // retry safety against corrupt local state, `radar::checkpoint()` relies
    // on `runtime_state::load_*_memory_automation_ledger` treating malformed
    // ledgers as empty rather than erroring out — see those loaders'
    // tolerance contract. Other classes of checkpoint failure would also
    // break normal (non-retry) hook responses, so this order is consistent
    // with overall error semantics.
    let mut warnings = Vec::new();
    if let Some(attempt_outcome) = input.attempt_outcome {
        let profile = profile::resolve(Some(input.profile))?;
        let layout = StateLayout::resolve(input.repo_root, profile)?;
        match session::load_session_id(&layout)? {
            Some(session_id) => {
                if let Err(error) = work_stream_decay::record_attempt_outcome(
                    &layout,
                    &session_id,
                    attempt_outcome,
                    session::now_epoch_s()?,
                ) {
                    warnings.push(format!(
                        "best-effort work-stream decay update unavailable: {error}"
                    ));
                }
            }
            None => warnings.push(
                "attempt outcome was ignored because no active CCD session is tracked".to_owned(),
            ),
        }
    }

    Ok(HostHookReport {
        adapter,
        hook: ON_AGENT_END,
        status: "checkpoint_evaluated",
        detail: Some(
            "lightweight checkpoint evaluation is ready; the session lease remains active"
                .to_owned(),
        ),
        session_boundary: None,
        disposition: None,
        compact_summary: None,
        context: None,
        telemetry: None,
        diagnostics: None,
        remember_selection: None,
        runtime: None,
        checkpoint: Some(build_checkpoint_payload(
            input.repo_root,
            Some(input.profile),
        )?),
        session_start: None,
        session_clear: None,
        session_heartbeat: None,
        context_check: None,
        recovery: None,
        escalation: None,
        alerts: Vec::new(),
        warnings,
    })
}

fn handle_session_end(adapter: &'static str, input: HookInput<'_>) -> Result<HostHookReport> {
    let locality_id = repo::marker::load(input.repo_root)?
        .map(|marker| marker.locality_id)
        .unwrap_or_default();
    let session_clear = session::clear(
        input.repo_root,
        Some(input.profile),
        (!locality_id.is_empty()).then_some(locality_id.as_str()),
        session::SessionClearOptions {
            actor_id: input.protected_write.actor_id.clone(),
            reason: Some("host-hook session_end".to_owned()),
        },
    )?;
    let session_clear_value = serde_json::to_value(&session_clear)?;
    let status = if session_clear_value
        .get("cleared")
        .and_then(Value::as_bool)
        .unwrap_or(false)
    {
        "session_cleared"
    } else {
        "already_clear"
    };

    Ok(HostHookReport {
        adapter,
        hook: ON_SESSION_END,
        status,
        detail: Some("session boundary close-out completed through CCD session clear".to_owned()),
        session_boundary: None,
        disposition: None,
        compact_summary: None,
        context: None,
        telemetry: None,
        diagnostics: None,
        remember_selection: None,
        runtime: None,
        checkpoint: None,
        session_start: None,
        session_clear: Some(session_clear_value),
        session_heartbeat: None,
        context_check: None,
        recovery: None,
        escalation: None,
        alerts: Vec::new(),
        warnings: Vec::new(),
    })
}

fn handle_supervisor_tick(adapter: &'static str, input: HookInput<'_>) -> Result<HostHookReport> {
    let context_check = radar::run_context_check(
        input.repo_root,
        Some(input.profile),
        radar::ContextCheckTrigger::SupervisorPoll,
    )?;
    let session_heartbeat = input
        .protected_write
        .actor_id
        .clone()
        .map(|actor_id| {
            session::heartbeat(
                input.repo_root,
                Some(input.profile),
                session::SessionHeartbeatOptions {
                    actor_id,
                    activity: Some("host-hook supervisor_tick".to_owned()),
                },
            )
        })
        .transpose()?
        .map(|report| serde_json::to_value(&report))
        .transpose()?;
    let (status, detail) = if session_heartbeat.is_some() {
        (
            "heartbeat_recorded",
            "supervisor poll recorded a fresh autonomous-session heartbeat".to_owned(),
        )
    } else {
        (
            "supervisor_reviewed",
            "supervisor poll evaluated refresh state; pass --actor-id to refresh an autonomous lease"
                .to_owned(),
        )
    };

    Ok(HostHookReport {
        adapter,
        hook: SUPERVISOR_TICK,
        status,
        detail: Some(detail),
        session_boundary: None,
        disposition: None,
        compact_summary: None,
        context: None,
        telemetry: None,
        diagnostics: None,
        remember_selection: None,
        runtime: None,
        checkpoint: None,
        session_start: None,
        session_clear: None,
        session_heartbeat,
        context_check: Some(context_check),
        recovery: None,
        escalation: None,
        alerts: Vec::new(),
        warnings: Vec::new(),
    })
}

fn build_checkpoint_payload(repo_root: &Path, explicit_profile: Option<&str>) -> Result<Value> {
    let result = radar::checkpoint(repo_root, explicit_profile)?;
    let value = serde_json::to_value(&result)?;
    output::try_filter_json_fields(
        value,
        &CHECKPOINT_FIELDS
            .iter()
            .map(|field| field.to_string())
            .collect::<Vec<_>>(),
        &CHECKPOINT_FIELD_FILTER_SPEC,
    )
}

fn render_section<T: Serialize>(title: &str, value: Option<&T>) {
    let Some(value) = value else {
        return;
    };

    println!();
    println!("{title}:");
    match serde_json::to_string_pretty(value) {
        Ok(rendered) => println!("{rendered}"),
        Err(_) => println!("<unrenderable {title}>"),
    }
}

struct CodexAdapter;
struct ClaudeAdapter;
struct GeminiAdapter;
struct OpenCodeAdapter;
struct OpenClawAdapter;
struct HermesAdapter;

static CODEX_ADAPTER: CodexAdapter = CodexAdapter;
static CLAUDE_ADAPTER: ClaudeAdapter = ClaudeAdapter;
static GEMINI_ADAPTER: GeminiAdapter = GeminiAdapter;
static OPENCODE_ADAPTER: OpenCodeAdapter = OpenCodeAdapter;
static OPENCLAW_ADAPTER: OpenClawAdapter = OpenClawAdapter;
static HERMES_ADAPTER: HermesAdapter = HermesAdapter;

impl HostAdapter for CodexAdapter {
    fn name(&self) -> &'static str {
        "codex"
    }

    fn snapshot(&self, _repo_root: &Path) -> Result<Option<HostContextSnapshot>> {
        crate::telemetry::codex::current()
    }
}

impl HostAdapter for ClaudeAdapter {
    fn name(&self) -> &'static str {
        "claude"
    }

    fn snapshot(&self, repo_root: &Path) -> Result<Option<HostContextSnapshot>> {
        crate::telemetry::claude::current(repo_root)
    }
}

impl HostAdapter for GeminiAdapter {
    fn name(&self) -> &'static str {
        "gemini"
    }

    fn snapshot(&self, repo_root: &Path) -> Result<Option<HostContextSnapshot>> {
        crate::telemetry::gemini::current(repo_root)
    }
}

impl HostAdapter for OpenCodeAdapter {
    fn name(&self) -> &'static str {
        "opencode"
    }

    fn snapshot(&self, repo_root: &Path) -> Result<Option<HostContextSnapshot>> {
        crate::telemetry::opencode::current(repo_root)
    }
}

impl HostAdapter for OpenClawAdapter {
    fn name(&self) -> &'static str {
        "openclaw"
    }

    fn snapshot(&self, _repo_root: &Path) -> Result<Option<HostContextSnapshot>> {
        Ok(None)
    }
}

impl HostAdapter for HermesAdapter {
    fn name(&self) -> &'static str {
        "hermes"
    }

    fn snapshot(&self, _repo_root: &Path) -> Result<Option<HostContextSnapshot>> {
        Ok(None)
    }
}
