use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, bail, Context, Result};
use serde::{Deserialize, Serialize};

use crate::db;
use crate::handoff;
use crate::paths::state::StateLayout;
use crate::state::session_gates;

const CHECKPOINT_SCHEMA_VERSION: u32 = 1;
const MAX_CHECKPOINT_SUMMARY_CHARS: usize = 280;
const MAX_RECOVERY_IMMEDIATE_ACTIONS: usize = 8;
const MAX_RECOVERY_KEY_FILES: usize = 8;
const MAX_WORKING_BUFFER_SUMMARY_LINES: usize = 12;
const MAX_WORKING_BUFFER_BYTES: usize = 4096;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RuntimeLifecycle {
    #[serde(rename = "active", alias = "Active")]
    Active,
    #[serde(rename = "inactive", alias = "Inactive")]
    Inactive,
}

impl RuntimeLifecycle {
    pub fn is_active(self) -> bool {
        matches!(self, Self::Active)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeHandoffItem {
    pub text: String,
    pub lifecycle: RuntimeLifecycle,
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeHandoffState {
    pub title: String,
    pub immediate_actions: Vec<RuntimeHandoffItem>,
    pub completed_state: Vec<RuntimeHandoffItem>,
    pub operational_guardrails: Vec<RuntimeHandoffItem>,
    pub key_files: Vec<RuntimeHandoffItem>,
    pub definition_of_done: Vec<RuntimeHandoffItem>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecoveryOrigin {
    Compaction,
    RiskyPause,
    Renewal,
    Manual,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeCheckpointState {
    pub origin: RecoveryOrigin,
    pub captured_at_epoch_s: u64,
    pub session_started_at_epoch_s: u64,
    pub summary: String,
    pub immediate_actions: Vec<String>,
    pub key_files: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeWorkingBufferState {
    pub origin: RecoveryOrigin,
    pub captured_at_epoch_s: u64,
    pub session_started_at_epoch_s: u64,
    pub summary_lines: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeRecoveryState {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub checkpoint: Option<RuntimeCheckpointState>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub working_buffer: Option<RuntimeWorkingBufferState>,
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct RuntimeState {
    pub handoff: RuntimeHandoffState,
}

#[derive(Debug, Clone)]
pub struct RuntimeTextSurface {
    pub kind: &'static str,
    pub path: PathBuf,
    pub status: RuntimeTextSurfaceStatus,
    pub content: String,
    pub migrated_from: Option<String>,
}

impl RuntimeTextSurface {
    pub fn missing(kind: &'static str, path: &Path) -> Self {
        Self {
            kind,
            path: path.to_path_buf(),
            status: RuntimeTextSurfaceStatus::Missing,
            content: String::new(),
            migrated_from: None,
        }
    }

    pub fn is_missing(&self) -> bool {
        self.status.is_missing()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeTextSurfaceStatus {
    Missing,
    #[allow(dead_code)]
    Empty,
    #[allow(dead_code)]
    Loaded,
    LoadedNative,
}

impl RuntimeTextSurfaceStatus {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Missing => "missing",
            Self::Empty => "empty",
            Self::Loaded => "loaded",
            Self::LoadedNative => "loaded_native",
        }
    }

    pub fn is_missing(self) -> bool {
        matches!(self, Self::Missing)
    }

    pub fn is_loaded_native(self) -> bool {
        matches!(self, Self::LoadedNative)
    }
}

#[derive(Debug, Clone)]
pub struct RuntimeSourceSurfaces {
    pub execution_gates: RuntimeTextSurface,
    pub handoff: RuntimeTextSurface,
}

#[derive(Debug, Clone)]
pub struct RuntimeRecoverySurface {
    pub kind: &'static str,
    pub path: PathBuf,
    pub status: &'static str,
}

impl RuntimeRecoverySurface {
    fn missing(kind: &'static str, path: &Path) -> Self {
        Self {
            kind,
            path: path.to_path_buf(),
            status: "missing",
        }
    }
}

#[derive(Debug, Clone)]
pub struct RuntimeRecoverySurfaces {
    pub checkpoint: RuntimeRecoverySurface,
    pub working_buffer: RuntimeRecoverySurface,
}

#[derive(Debug, Clone)]
pub struct LoadedRuntimeState {
    pub state: RuntimeState,
    pub sources: RuntimeSourceSurfaces,
    pub execution_gates: LoadedRuntimeExecutionGates,
    pub recovery: LoadedRuntimeRecoveryState,
}

#[derive(Debug, Clone)]
pub struct LoadedRuntimeExecutionGates {
    pub source: RuntimeTextSurface,
    pub view: session_gates::ExecutionGatesView,
}

#[derive(Debug, Clone)]
pub struct LoadedRuntimeRecoveryState {
    pub state: RuntimeRecoveryState,
    pub sources: RuntimeRecoverySurfaces,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeRecoveryView {
    pub status: String,
    pub checkpoint: RuntimeCheckpointView,
    pub working_buffer: RuntimeWorkingBufferView,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeCheckpointView {
    pub path: String,
    pub status: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub origin: Option<RecoveryOrigin>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub captured_at_epoch_s: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_started_at_epoch_s: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub summary: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub immediate_actions: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub key_files: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuntimeWorkingBufferView {
    pub path: String,
    pub status: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub origin: Option<RecoveryOrigin>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub captured_at_epoch_s: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_started_at_epoch_s: Option<u64>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub summary_lines: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct NativeCloneRuntimeState {
    #[serde(rename = "schema_version")]
    _schema_version: u32,
    handoff: RuntimeHandoffState,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct CheckpointFile {
    schema_version: u32,
    origin: RecoveryOrigin,
    captured_at_epoch_s: u64,
    session_started_at_epoch_s: u64,
    summary: String,
    #[serde(default)]
    immediate_actions: Vec<String>,
    #[serde(default)]
    key_files: Vec<String>,
}

pub fn load_runtime_state(
    _repo_root: &Path,
    layout: &StateLayout,
    _locality_id: &str,
) -> Result<LoadedRuntimeState> {
    normalize_raw_into_loaded(load_raw_start_runtime_sources(
        _repo_root,
        layout,
        _locality_id,
    )?)
}

/// Raw file contents loaded but not yet parsed/normalized.
/// Used by the compiled-state fast path to fingerprint sources
/// before deciding whether full normalization is needed.
pub struct RawRuntimeSources {
    pub execution_gates: LoadedRuntimeExecutionGates,
    pub handoff: RuntimeTextSurface,
    pub(crate) handoff_state: Option<RuntimeHandoffState>,
    pub recovery: LoadedRuntimeRecoveryState,
}

pub fn load_raw_start_runtime_sources(
    _repo_root: &Path,
    layout: &StateLayout,
    _locality_id: &str,
) -> Result<RawRuntimeSources> {
    let execution_gates = load_execution_gates(layout)?;
    let (handoff_state, handoff) = load_runtime_handoff_surface(layout)?;
    let recovery = load_runtime_recovery_state(layout)?;

    Ok(RawRuntimeSources {
        execution_gates,
        handoff,
        handoff_state: Some(handoff_state),
        recovery,
    })
}

pub fn normalize_raw_into_loaded(raw: RawRuntimeSources) -> Result<LoadedRuntimeState> {
    build_loaded_runtime_state(raw)
}

pub fn load_execution_gates(layout: &StateLayout) -> Result<LoadedRuntimeExecutionGates> {
    let path = layout.state_db_path();
    let Some(db) = try_open_handoff_db(layout)? else {
        return Ok(LoadedRuntimeExecutionGates {
            source: RuntimeTextSurface::missing("execution_gates", &path),
            view: session_gates::build_view(layout, None),
        });
    };

    let raw = db::session_gates::read(db.conn())?;
    Ok(LoadedRuntimeExecutionGates {
        source: execution_gates_surface(layout, raw.as_ref())?,
        view: session_gates::build_view(layout, raw.clone()),
    })
}

pub fn load_runtime_recovery_state(layout: &StateLayout) -> Result<LoadedRuntimeRecoveryState> {
    let db_path = layout.state_db_path();
    let Some(db) = try_open_handoff_db(layout)? else {
        return Ok(LoadedRuntimeRecoveryState {
            state: RuntimeRecoveryState {
                checkpoint: None,
                working_buffer: None,
            },
            sources: RuntimeRecoverySurfaces {
                checkpoint: RuntimeRecoverySurface::missing("checkpoint", &db_path),
                working_buffer: RuntimeRecoverySurface::missing("working_buffer", &db_path),
            },
        });
    };

    let checkpoint = db::recovery::read_checkpoint(db.conn())?;
    let working_buffer = db::recovery::read_working_buffer(db.conn())?;

    let checkpoint_source = RuntimeRecoverySurface {
        kind: "checkpoint",
        path: db_path.clone(),
        status: if checkpoint.is_some() {
            "loaded"
        } else {
            "missing"
        },
    };
    let working_buffer_source = RuntimeRecoverySurface {
        kind: "working_buffer",
        path: db_path,
        status: if working_buffer.is_some() {
            "loaded"
        } else {
            "missing"
        },
    };

    Ok(LoadedRuntimeRecoveryState {
        state: RuntimeRecoveryState {
            checkpoint,
            working_buffer,
        },
        sources: RuntimeRecoverySurfaces {
            checkpoint: checkpoint_source,
            working_buffer: working_buffer_source,
        },
    })
}

pub fn recovery_view(recovery: &LoadedRuntimeRecoveryState) -> RuntimeRecoveryView {
    let checkpoint = checkpoint_view(
        &recovery.sources.checkpoint,
        recovery.state.checkpoint.as_ref(),
    );
    let working_buffer = working_buffer_view(
        &recovery.sources.working_buffer,
        recovery.state.working_buffer.as_ref(),
    );

    RuntimeRecoveryView {
        status: if recovery.state.checkpoint.is_some() || recovery.state.working_buffer.is_some() {
            "loaded".to_owned()
        } else {
            "missing".to_owned()
        },
        checkpoint,
        working_buffer,
    }
}

fn checkpoint_view(
    source: &RuntimeRecoverySurface,
    checkpoint: Option<&RuntimeCheckpointState>,
) -> RuntimeCheckpointView {
    if let Some(checkpoint) = checkpoint {
        return RuntimeCheckpointView {
            path: source.path.display().to_string(),
            status: source.status.to_owned(),
            origin: Some(checkpoint.origin),
            captured_at_epoch_s: Some(checkpoint.captured_at_epoch_s),
            session_started_at_epoch_s: Some(checkpoint.session_started_at_epoch_s),
            summary: Some(checkpoint.summary.clone()),
            immediate_actions: checkpoint.immediate_actions.clone(),
            key_files: checkpoint.key_files.clone(),
        };
    }

    RuntimeCheckpointView {
        path: source.path.display().to_string(),
        status: source.status.to_owned(),
        origin: None,
        captured_at_epoch_s: None,
        session_started_at_epoch_s: None,
        summary: None,
        immediate_actions: Vec::new(),
        key_files: Vec::new(),
    }
}

fn working_buffer_view(
    source: &RuntimeRecoverySurface,
    working_buffer: Option<&RuntimeWorkingBufferState>,
) -> RuntimeWorkingBufferView {
    if let Some(working_buffer) = working_buffer {
        return RuntimeWorkingBufferView {
            path: source.path.display().to_string(),
            status: source.status.to_owned(),
            origin: Some(working_buffer.origin),
            captured_at_epoch_s: Some(working_buffer.captured_at_epoch_s),
            session_started_at_epoch_s: Some(working_buffer.session_started_at_epoch_s),
            summary_lines: working_buffer.summary_lines.clone(),
        };
    }

    RuntimeWorkingBufferView {
        path: source.path.display().to_string(),
        status: source.status.to_owned(),
        origin: None,
        captured_at_epoch_s: None,
        session_started_at_epoch_s: None,
        summary_lines: Vec::new(),
    }
}

pub fn native_handoff_exists(layout: &StateLayout) -> Result<bool> {
    let Some(db) = try_open_handoff_db(layout)? else {
        return Ok(false);
    };
    db::handoff::exists(db.conn())
}

// --- DB helpers ---

/// Open the state DB and migrate any legacy handoff/recovery JSON files.
fn open_handoff_db(layout: &StateLayout) -> Result<db::StateDb> {
    let db = db::StateDb::open(&layout.state_db_path())?;
    migrate_handoff_json(&db, layout)?;
    Ok(db)
}

/// Open the state DB only if it already exists or legacy JSON needs migration.
fn try_open_handoff_db(layout: &StateLayout) -> Result<Option<db::StateDb>> {
    db::StateDb::try_open_with_migration(
        layout,
        || {
            layout.clone_runtime_state_path().exists()
                || layout.clone_checkpoint_path().exists()
                || layout.clone_working_buffer_path().exists()
        },
        |db| migrate_handoff_json(db, layout),
    )
}

fn migrate_handoff_json(db: &db::StateDb, layout: &StateLayout) -> Result<()> {
    migrate_handoff_state_json(db, layout)?;
    migrate_checkpoint_json(db, layout)?;
    migrate_working_buffer_md(db, layout)?;
    Ok(())
}

fn migrate_handoff_state_json(db: &db::StateDb, layout: &StateLayout) -> Result<()> {
    let path = layout.clone_runtime_state_path();
    let contents = match fs::read_to_string(&path) {
        Ok(c) => c,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
        Err(e) => return Err(e).with_context(|| format!("failed to read {}", path.display())),
    };

    if db::handoff::read(db.conn())?.is_none() {
        let native: NativeCloneRuntimeState = serde_json::from_str(&contents)
            .with_context(|| format!("failed to parse {}", path.display()))?;
        db::handoff::write(db.conn(), &native.handoff)?;
    }

    let mut migrated = path.as_os_str().to_owned();
    migrated.push(".migrated");
    fs::rename(&path, Path::new(&migrated))
        .with_context(|| format!("failed to rename {} to .migrated", path.display()))?;
    Ok(())
}

fn migrate_checkpoint_json(db: &db::StateDb, layout: &StateLayout) -> Result<()> {
    let path = layout.clone_checkpoint_path();
    let contents = match fs::read_to_string(&path) {
        Ok(c) => c,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
        Err(e) => return Err(e).with_context(|| format!("failed to read {}", path.display())),
    };

    if !contents.trim().is_empty() && db::recovery::read_checkpoint(db.conn())?.is_none() {
        let checkpoint: CheckpointFile = serde_json::from_str(&contents)
            .with_context(|| format!("failed to parse {}", path.display()))?;
        if checkpoint.schema_version != CHECKPOINT_SCHEMA_VERSION {
            bail!(
                "{} has unsupported checkpoint schema version {}; expected {}",
                path.display(),
                checkpoint.schema_version,
                CHECKPOINT_SCHEMA_VERSION
            );
        }
        validate_checkpoint(&checkpoint, &path)?;

        db::recovery::write_checkpoint(
            db.conn(),
            &RuntimeCheckpointState {
                origin: checkpoint.origin,
                captured_at_epoch_s: checkpoint.captured_at_epoch_s,
                session_started_at_epoch_s: checkpoint.session_started_at_epoch_s,
                summary: checkpoint.summary,
                immediate_actions: checkpoint.immediate_actions,
                key_files: checkpoint.key_files,
            },
        )?;
    }

    let mut migrated = path.as_os_str().to_owned();
    migrated.push(".migrated");
    fs::rename(&path, Path::new(&migrated))
        .with_context(|| format!("failed to rename {} to .migrated", path.display()))?;
    Ok(())
}

fn migrate_working_buffer_md(db: &db::StateDb, layout: &StateLayout) -> Result<()> {
    let path = layout.clone_working_buffer_path();
    let contents = match fs::read_to_string(&path) {
        Ok(c) => c,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
        Err(e) => return Err(e).with_context(|| format!("failed to read {}", path.display())),
    };

    if !contents.trim().is_empty() && db::recovery::read_working_buffer(db.conn())?.is_none() {
        if contents.len() > MAX_WORKING_BUFFER_BYTES {
            bail!(
                "{} exceeds the working-buffer size limit of {} bytes",
                path.display(),
                MAX_WORKING_BUFFER_BYTES
            );
        }

        let provenance = parse_recovery_provenance(
            &handoff::extract_bulleted_section(&contents, "Provenance"),
            &path,
        )?;
        let summary_lines = handoff::extract_bulleted_section(&contents, "Recent Exchange Summary");
        if summary_lines.is_empty() {
            bail!(
                "{} is missing `## Recent Exchange Summary` bullet items",
                path.display()
            );
        }
        if summary_lines.len() > MAX_WORKING_BUFFER_SUMMARY_LINES {
            bail!(
                "{} has {} working-buffer summary lines; the limit is {}",
                path.display(),
                summary_lines.len(),
                MAX_WORKING_BUFFER_SUMMARY_LINES
            );
        }

        db::recovery::write_working_buffer(
            db.conn(),
            &RuntimeWorkingBufferState {
                origin: provenance.origin,
                captured_at_epoch_s: provenance.captured_at_epoch_s,
                session_started_at_epoch_s: provenance.session_started_at_epoch_s,
                summary_lines,
            },
        )?;
    }

    let mut migrated = path.as_os_str().to_owned();
    migrated.push(".migrated");
    fs::rename(&path, Path::new(&migrated))
        .with_context(|| format!("failed to rename {} to .migrated", path.display()))?;
    Ok(())
}

pub fn persist_canonical_handoff_state(
    layout: &StateLayout,
    handoff: &RuntimeHandoffState,
) -> Result<u64> {
    let db = open_handoff_db(layout)?;
    db::handoff::write(db.conn(), handoff)
}

pub fn load_canonical_handoff_surface(layout: &StateLayout) -> Result<RuntimeTextSurface> {
    let Some(db) = try_open_handoff_db(layout)? else {
        return Ok(RuntimeTextSurface::missing(
            "handoff",
            &layout.state_db_path(),
        ));
    };
    match db::handoff::read(db.conn())? {
        Some(handoff) => Ok(native_handoff_surface(layout, &handoff)),
        None => Ok(RuntimeTextSurface::missing(
            "handoff",
            &layout.state_db_path(),
        )),
    }
}

fn build_loaded_runtime_state(raw: RawRuntimeSources) -> Result<LoadedRuntimeState> {
    let RawRuntimeSources {
        execution_gates,
        handoff,
        handoff_state,
        recovery,
    } = raw;

    let state = RuntimeState {
        handoff: handoff_state.unwrap_or_else(|| normalize_handoff(&handoff.content)),
    };

    Ok(LoadedRuntimeState {
        state,
        sources: RuntimeSourceSurfaces {
            execution_gates: execution_gates.source.clone(),
            handoff,
        },
        execution_gates,
        recovery,
    })
}

fn load_runtime_handoff_surface(
    layout: &StateLayout,
) -> Result<(RuntimeHandoffState, RuntimeTextSurface)> {
    let Some(db) = try_open_handoff_db(layout)? else {
        return Ok((
            RuntimeHandoffState::default(),
            RuntimeTextSurface::missing("handoff", &layout.state_db_path()),
        ));
    };
    match db::handoff::read(db.conn())? {
        Some(handoff) => {
            let surface = native_handoff_surface(layout, &handoff);
            Ok((handoff, surface))
        }
        None => Ok((
            RuntimeHandoffState::default(),
            RuntimeTextSurface::missing("handoff", &layout.state_db_path()),
        )),
    }
}

fn execution_gates_surface(
    layout: &StateLayout,
    raw: Option<&session_gates::ExecutionGateStateFile>,
) -> Result<RuntimeTextSurface> {
    let Some(raw) = raw else {
        return Ok(RuntimeTextSurface::missing(
            "execution_gates",
            &layout.state_db_path(),
        ));
    };

    Ok(RuntimeTextSurface {
        kind: "execution_gates",
        path: layout.state_db_path(),
        status: RuntimeTextSurfaceStatus::LoadedNative,
        content: serde_json::to_string_pretty(raw)?,
        migrated_from: None,
    })
}

fn native_handoff_surface(
    layout: &StateLayout,
    handoff: &RuntimeHandoffState,
) -> RuntimeTextSurface {
    RuntimeTextSurface {
        kind: "handoff",
        path: layout.state_db_path(),
        status: RuntimeTextSurfaceStatus::LoadedNative,
        content: render_handoff_markdown(handoff),
        migrated_from: None,
    }
}

struct RecoveryProvenance {
    origin: RecoveryOrigin,
    captured_at_epoch_s: u64,
    session_started_at_epoch_s: u64,
}

fn parse_recovery_provenance(items: &[String], path: &Path) -> Result<RecoveryProvenance> {
    let mut values = BTreeMap::new();
    for item in items {
        let (key, value) = item.split_once(':').ok_or_else(|| {
            anyhow!(
                "{} has an invalid recovery provenance item `{item}`; expected `Key: value`",
                path.display()
            )
        })?;
        values.insert(key.trim().to_ascii_lowercase(), value.trim().to_owned());
    }

    let origin = parse_recovery_origin(
        values
            .remove("origin")
            .ok_or_else(|| anyhow!("{} is missing `Origin` in `## Provenance`", path.display()))?,
        path,
    )?;
    let captured_at_epoch_s = parse_required_epoch(
        values.remove("captured at epoch").ok_or_else(|| {
            anyhow!(
                "{} is missing `Captured At Epoch` in `## Provenance`",
                path.display()
            )
        })?,
        path,
        "Captured At Epoch",
    )?;
    let session_started_at_epoch_s = parse_required_epoch(
        values.remove("session started at epoch").ok_or_else(|| {
            anyhow!(
                "{} is missing `Session Started At Epoch` in `## Provenance`",
                path.display()
            )
        })?,
        path,
        "Session Started At Epoch",
    )?;
    if !values.is_empty() {
        let unexpected = values.keys().cloned().collect::<Vec<_>>().join(", ");
        bail!(
            "{} has unsupported recovery provenance keys: {}",
            path.display(),
            unexpected
        );
    }

    Ok(RecoveryProvenance {
        origin,
        captured_at_epoch_s,
        session_started_at_epoch_s,
    })
}

fn parse_recovery_origin(value: String, path: &Path) -> Result<RecoveryOrigin> {
    match value.as_str() {
        "compaction" => Ok(RecoveryOrigin::Compaction),
        "risky_pause" => Ok(RecoveryOrigin::RiskyPause),
        "renewal" => Ok(RecoveryOrigin::Renewal),
        "manual" => Ok(RecoveryOrigin::Manual),
        _ => bail!(
            "{} has unsupported recovery origin `{value}`; expected `compaction`, `risky_pause`, `renewal`, or `manual`",
            path.display()
        ),
    }
}

fn parse_required_epoch(value: String, path: &Path, label: &str) -> Result<u64> {
    let parsed = value.parse::<u64>().with_context(|| {
        format!(
            "{} has an invalid `{label}` value `{value}`; expected an unsigned epoch second",
            path.display()
        )
    })?;
    if parsed == 0 {
        bail!(
            "{} has invalid `{label}` value `0`; expected a positive epoch second",
            path.display()
        );
    }
    Ok(parsed)
}

fn validate_checkpoint(checkpoint: &CheckpointFile, path: &Path) -> Result<()> {
    if checkpoint.captured_at_epoch_s == 0 {
        bail!(
            "{} has invalid `captured_at_epoch_s` value `0`; expected a positive epoch second",
            path.display()
        );
    }
    if checkpoint.session_started_at_epoch_s == 0 {
        bail!(
            "{} has invalid `session_started_at_epoch_s` value `0`; expected a positive epoch second",
            path.display()
        );
    }

    let summary = checkpoint.summary.trim();
    if summary.is_empty() {
        bail!("{} has an empty checkpoint summary", path.display());
    }
    if summary.len() > MAX_CHECKPOINT_SUMMARY_CHARS {
        bail!(
            "{} has a checkpoint summary longer than {} characters",
            path.display(),
            MAX_CHECKPOINT_SUMMARY_CHARS
        );
    }

    validate_recovery_items(
        "immediate_actions",
        &checkpoint.immediate_actions,
        MAX_RECOVERY_IMMEDIATE_ACTIONS,
        path,
    )?;
    validate_recovery_items(
        "key_files",
        &checkpoint.key_files,
        MAX_RECOVERY_KEY_FILES,
        path,
    )?;
    Ok(())
}

fn validate_recovery_items(
    label: &str,
    items: &[String],
    max_items: usize,
    path: &Path,
) -> Result<()> {
    if items.len() > max_items {
        bail!(
            "{} has {} `{label}` entries; the limit is {}",
            path.display(),
            items.len(),
            max_items
        );
    }
    if items.iter().any(|item| item.trim().is_empty()) {
        bail!(
            "{} has an empty `{label}` entry; recovery artifacts must use explicit non-empty items",
            path.display()
        );
    }
    Ok(())
}

pub(crate) fn render_handoff_markdown(state: &RuntimeHandoffState) -> String {
    // The markdown handoff is an active-session export view. Inactive items stay
    // only in native runtime state so the authored/export surface remains a
    // concise next-session handoff instead of a full lifecycle ledger.
    let mut body = String::new();
    if !state.title.is_empty() {
        body.push_str("# ");
        body.push_str(&state.title);
        body.push_str("\n\n");
    }
    append_numbered_section(&mut body, "Immediate Actions", &state.immediate_actions);
    append_bulleted_section(&mut body, "Completed State", &state.completed_state);
    append_bulleted_section(
        &mut body,
        "Operational Guardrails",
        &state.operational_guardrails,
    );
    append_bulleted_section(
        &mut body,
        handoff::KEY_FILES_SECTION_TITLE,
        &state.key_files,
    );
    append_numbered_section(&mut body, "Definition of Done", &state.definition_of_done);
    body
}

fn append_bulleted_section(body: &mut String, title: &str, items: &[RuntimeHandoffItem]) {
    let active_items = items
        .iter()
        .filter(|item| item.lifecycle.is_active())
        .collect::<Vec<_>>();
    if active_items.is_empty() {
        return;
    }
    if !body.is_empty() {
        body.push('\n');
    }
    body.push_str("## ");
    body.push_str(title);
    body.push_str("\n\n");
    for item in active_items {
        body.push_str("- ");
        body.push_str(&item.text);
        body.push('\n');
    }
}

fn append_numbered_section(body: &mut String, title: &str, items: &[RuntimeHandoffItem]) {
    let active_items = items
        .iter()
        .filter(|item| item.lifecycle.is_active())
        .collect::<Vec<_>>();
    if active_items.is_empty() {
        return;
    }
    if !body.is_empty() {
        body.push('\n');
    }
    body.push_str("## ");
    body.push_str(title);
    body.push_str("\n\n");
    for (i, item) in active_items.iter().enumerate() {
        body.push_str(&format!("{}. {}\n", i + 1, item.text));
    }
}

pub(crate) fn normalize_handoff(contents: &str) -> RuntimeHandoffState {
    if contents.is_empty() {
        return RuntimeHandoffState::default();
    }

    let title = contents
        .lines()
        .find_map(|line| line.strip_prefix("# ").map(str::trim))
        .unwrap_or_default()
        .to_owned();

    RuntimeHandoffState {
        title,
        immediate_actions: handoff::extract_numbered_section(contents, "Immediate Actions")
            .into_iter()
            .map(active_handoff_item)
            .collect(),
        completed_state: handoff::extract_bulleted_section(contents, "Completed State")
            .into_iter()
            .filter(|line| !starts_with_inactive_marker(line))
            .map(active_handoff_item)
            .collect(),
        operational_guardrails: handoff::extract_bulleted_section(
            contents,
            "Operational Guardrails",
        )
        .into_iter()
        .map(active_handoff_item)
        .collect(),
        key_files: handoff::extract_bulleted_section(contents, handoff::KEY_FILES_SECTION_TITLE)
            .into_iter()
            .map(active_handoff_item)
            .collect(),
        definition_of_done: handoff::extract_numbered_section(contents, "Definition of Done")
            .into_iter()
            .map(active_handoff_item)
            .collect(),
    }
}

fn active_handoff_item(text: String) -> RuntimeHandoffItem {
    RuntimeHandoffItem {
        text,
        lifecycle: RuntimeLifecycle::Active,
    }
}

fn starts_with_inactive_marker(line: &str) -> bool {
    let lower = line.to_ascii_lowercase();
    lower.starts_with("archived:")
        || lower.starts_with("stale:")
        || lower.starts_with("superseded:")
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::PathBuf;

    use tempfile::tempdir;

    use super::*;
    use crate::profile::ProfileName;

    #[test]
    fn loads_runtime_state_with_handoff_from_native_state() {
        let temp = tempdir().expect("tempdir");
        let repo_root = temp.path().join("repo");
        let layout = StateLayout::new(
            temp.path().join(".ccd"),
            repo_root.join(".git/ccd"),
            ProfileName::new("main").expect("profile"),
        );
        let locality_id = "ccdrepo_runtime";

        fs::create_dir_all(layout.profile_root()).expect("profile root");
        fs::create_dir_all(layout.repo_overlay_root(locality_id).expect("repo overlay"))
            .expect("repo overlay dir");
        fs::create_dir_all(layout.clone_profile_root()).expect("clone profile root");
        fs::create_dir_all(&repo_root).expect("repo root");

        let handoff = RuntimeHandoffState {
            title: "Next Session: Native fallback".to_owned(),
            immediate_actions: vec![RuntimeHandoffItem {
                text: "Use native handoff state.".to_owned(),
                lifecycle: RuntimeLifecycle::Active,
            }],
            ..RuntimeHandoffState::default()
        };
        persist_canonical_handoff_state(&layout, &handoff).expect("persist native handoff");

        let loaded =
            load_runtime_state(&repo_root, &layout, locality_id).expect("load runtime state");

        assert_eq!(
            loaded.sources.handoff.status,
            RuntimeTextSurfaceStatus::LoadedNative
        );
        assert_eq!(loaded.state.handoff.title, "Next Session: Native fallback");
        assert_eq!(
            loaded
                .state
                .handoff
                .immediate_actions
                .iter()
                .map(|item| item.text.clone())
                .collect::<Vec<_>>(),
            vec!["Use native handoff state.".to_owned()]
        );
    }

    #[test]
    fn missing_handoff_surface_becomes_missing_runtime_source() {
        let temp = tempdir().expect("tempdir");
        let layout = StateLayout::new(
            temp.path().join(".ccd"),
            PathBuf::from("/tmp/repo/.git/ccd"),
            ProfileName::new("main").expect("profile"),
        );

        let loaded = load_runtime_state(temp.path(), &layout, "ccdrepo_missing")
            .expect("missing runtime state should load");

        assert_eq!(
            loaded.sources.handoff.status,
            RuntimeTextSurfaceStatus::Missing
        );
        assert!(loaded.state.handoff.title.is_empty());
    }

    #[test]
    fn runtime_load_accepts_lowercase_native_handoff_lifecycle_values() {
        let temp = tempdir().expect("tempdir");
        let repo_root = temp.path().join("repo");
        let layout = StateLayout::new(
            temp.path().join(".ccd"),
            repo_root.join(".git/ccd"),
            ProfileName::new("main").expect("profile"),
        );
        let locality_id = "ccdrepo_runtime";

        fs::create_dir_all(layout.profile_root()).expect("profile root");
        fs::create_dir_all(layout.repo_overlay_root(locality_id).expect("repo overlay"))
            .expect("repo overlay dir");
        fs::create_dir_all(layout.clone_profile_root()).expect("clone profile root");
        fs::create_dir_all(&repo_root).expect("repo root");
        fs::create_dir_all(
            layout
                .clone_runtime_state_path()
                .parent()
                .expect("clone runtime parent"),
        )
        .expect("clone runtime dir");

        fs::write(
            layout.clone_runtime_state_path(),
            serde_json::to_string_pretty(&serde_json::json!({
                "schema_version": 1,
                "handoff_adapter_fingerprint": serde_json::Value::Null,
                "handoff": {
                    "title": "Next Session: Lowercase native lifecycle",
                    "immediate_actions": [
                        { "text": "Accept lowercase lifecycle values.", "lifecycle": "active" }
                    ],
                    "completed_state": [],
                    "operational_guardrails": [],
                    "key_files": [],
                    "definition_of_done": []
                }
            }))
            .expect("serialize native runtime"),
        )
        .expect("native runtime");

        let loaded =
            load_runtime_state(temp.path(), &layout, locality_id).expect("runtime-first load");

        assert_eq!(
            loaded.sources.handoff.status,
            RuntimeTextSurfaceStatus::LoadedNative
        );
        assert_eq!(
            loaded.state.handoff.immediate_actions,
            vec![RuntimeHandoffItem {
                text: "Accept lowercase lifecycle values.".to_owned(),
                lifecycle: RuntimeLifecycle::Active,
            }]
        );
    }

    #[test]
    fn render_handoff_markdown_omits_inactive_items_from_export() {
        let rendered = render_handoff_markdown(&RuntimeHandoffState {
            title: "Next Session: Active export only".to_owned(),
            immediate_actions: vec![
                RuntimeHandoffItem {
                    text: "Do the active thing.".to_owned(),
                    lifecycle: RuntimeLifecycle::Active,
                },
                RuntimeHandoffItem {
                    text: "Do not render this archived action.".to_owned(),
                    lifecycle: RuntimeLifecycle::Inactive,
                },
            ],
            completed_state: vec![RuntimeHandoffItem {
                text: "Keep this visible.".to_owned(),
                lifecycle: RuntimeLifecycle::Active,
            }],
            operational_guardrails: vec![],
            key_files: vec![RuntimeHandoffItem {
                text: "`src/archived.rs`".to_owned(),
                lifecycle: RuntimeLifecycle::Inactive,
            }],
            definition_of_done: vec![],
        });

        assert!(rendered.contains("Do the active thing."));
        assert!(rendered.contains("Keep this visible."));
        assert!(!rendered.contains("Do not render this archived action."));
        assert!(!rendered.contains("`src/archived.rs`"));
        assert!(!rendered.contains("## Key Files for This Session"));
    }
}
