use std::path::PathBuf;

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

use crate::commands::{
    attach, check, doctor, drift, gc, link, policy_check, preflight, scaffold, session_open,
    skills, start, unlink,
};
use crate::handoff;
use crate::paths;
use crate::recovery;
use crate::repo;
use crate::state::{
    compiled, escalation, protected_write, radar, session, session_gates, state_export,
};

/// Default field set for MCP start responses — omits heavy source blobs and
/// provider-backed diagnostics.
/// Agents can override by passing an explicit `fields` argument.
const MCP_START_COMPACT_FIELDS: &[&str] = &[
    "command",
    "ok",
    "path",
    "profile",
    "project_id",
    "locality_id",
    "compact_summary",
    "compiled_state",
    "prompt_injection",
    "refresh_actions",
    "session_state",
    "escalation",
    "warnings",
];

/// Dispatch an MCP tool call to the corresponding library function.
///
/// Returns the command report as a `serde_json::Value`.
pub fn dispatch(tool_name: &str, args: &Value) -> Result<Value> {
    let command = get_required_string(args, "command")?;
    let full = get_bool(args, "full");

    let report = match (tool_name, command.as_str()) {
        // ── ccd_repo ──────────────────────────────────────────────
        ("ccd_repo", "attach") => {
            let path = resolve_path(args)?;
            let report = attach::run(
                &path,
                get_opt_str(args, "profile").as_deref(),
                get_opt_str_any(args, &["project_id", "locality_id"]).as_deref(),
                get_opt_str(args, "display_name").as_deref(),
            )?;
            to_value(&report)
        }
        ("ccd_repo", "scaffold") => {
            let path = resolve_path(args)?;
            let report = scaffold::run(&path, get_bool(args, "force"))?;
            to_value(&report)
        }
        ("ccd_repo", "link") => {
            let path = resolve_path(args)?;
            let report = link::run(
                &path,
                get_opt_str(args, "profile").as_deref(),
                get_opt_str_any(args, &["project_id", "locality_id"]).as_deref(),
                get_opt_str(args, "display_name").as_deref(),
            )?;
            to_value(&report)
        }
        ("ccd_repo", "unlink") => {
            let path = resolve_path(args)?;
            let report = unlink::run(&path)?;
            to_value(&report)
        }
        ("ccd_repo", "gc") => {
            let path = resolve_path(args)?;
            let report = gc::run(&path)?;
            to_value(&report)
        }
        ("ccd_repo", "skills-install") => {
            let path = resolve_path(args)?;
            let global = !get_bool(args, "local");
            let harness = args.get("harness").and_then(|value| value.as_str());
            let report = skills::run(&path, global, harness)?;
            to_value(&report)
        }

        // ── ccd_health ────────────────────────────────────────────
        ("ccd_health", "check") => {
            let path = resolve_path(args)?;
            let report = check::run(&path)?;
            to_value(&report)
        }
        ("ccd_health", "preflight") => {
            let path = resolve_path(args)?;
            let report = preflight::run(&path, get_opt_str(args, "profile").as_deref())?;
            to_value(&report)
        }
        ("ccd_health", "doctor") => {
            let path = resolve_path(args)?;
            let severity = get_opt_str(args, "severity");
            let fields = get_opt_str(args, "fields");
            let field_vec: Option<Vec<String>> = fields
                .as_ref()
                .map(|f| f.split(',').map(|s| s.trim().to_owned()).collect());
            if let Some(ref fv) = field_vec {
                crate::output::validate_fields(&crate::output::DOCTOR_FIELD_FILTER_SPEC, fv)?;
            }
            let mut report = doctor::run(&path, None, doctor::RunOptions::default())?;
            if let Some(sev) = severity {
                report.filter_by_status(&sev);
            }
            let value = to_value(&report)?;
            match &field_vec {
                Some(fv) => crate::output::try_filter_json_fields(
                    value,
                    fv,
                    &crate::output::DOCTOR_FIELD_FILTER_SPEC,
                ),
                None => Ok(value),
            }
        }
        ("ccd_health", "drift") => {
            let path = resolve_path(args)?;
            let report = drift::run(&path)?;
            to_value(&report)
        }
        // ── ccd_session ───────────────────────────────────────────
        ("ccd_session", "start") => {
            let path = resolve_path(args)?;
            let profile = get_opt_str(args, "profile");
            let refresh = get_bool(args, "refresh");
            let fields = get_opt_str(args, "fields");
            // Validate fields BEFORE running the command — start with refresh=true
            // mutates clone-local state, so an invalid field must fail before that.
            let field_vec: Option<Vec<String>> = fields
                .as_ref()
                .map(|f| f.split(',').map(|s| s.trim().to_owned()).collect());
            if let Some(ref fv) = field_vec {
                crate::output::validate_start_fields(fv)?;
            }
            let resolution = match &field_vec {
                Some(fv) if !crate::output::needs_full_start_resolution(fv) => {
                    start::StartResolutionMode::CompiledOnly
                }
                Some(_) => start::StartResolutionMode::Full,
                None if full => start::StartResolutionMode::Full,
                None => start::StartResolutionMode::CompiledOnly,
            };
            let report = start::run_with_options(
                &path,
                profile.as_deref(),
                refresh,
                start::StartRunOptions {
                    resolution,
                    activation: None,
                },
            )?;
            let value = with_ccd1_prompt(
                serde_json::to_value(&report)?,
                args,
                crate::ccd1_prompt::JsonSurface::Start,
            )?;
            if let Some(fv) = &field_vec {
                crate::output::try_filter_json_fields(
                    value,
                    fv,
                    &crate::output::START_FIELD_FILTER_SPEC,
                )
            } else if full {
                Ok(value)
            } else {
                // MCP compact default: skip source rendering, return operational fields only.
                // Agents can request full output via explicit `fields` or `full: true`.
                let compact: Vec<String> = MCP_START_COMPACT_FIELDS
                    .iter()
                    .map(|s| s.to_string())
                    .collect();
                crate::output::try_filter_json_fields(
                    value,
                    &compact,
                    &crate::output::START_FIELD_FILTER_SPEC,
                )
            }
        }
        ("ccd_session", "session-open") => {
            let path = resolve_path(args)?;
            let worktree = get_opt_str(args, "worktree").map(PathBuf::from);
            let report = session_open::run(
                &path,
                get_opt_str(args, "profile").as_deref(),
                worktree.as_deref(),
                get_opt_str(args, "branch").as_deref(),
                get_opt_str(args, "from").as_deref(),
            )?;
            to_value(&report)
        }
        ("ccd_session_lifecycle", "start-session") => {
            let path = resolve_path(args)?;
            let owner = parse_session_owner(args)?;
            let mode = get_opt_str(args, "mode")
                .map(|value| parse_session_mode(&value))
                .transpose()?;
            let locality_id = locality_id_for_path(&path);
            let report = session::start(
                &path,
                get_opt_str(args, "profile").as_deref(),
                locality_id.as_deref(),
                session::SessionStartOptions {
                    mode,
                    lifecycle: owner
                        .lifecycle
                        .unwrap_or(session::SessionLifecycle::Interactive),
                    owner_kind: owner.owner_kind,
                    actor_id: owner.actor_id,
                    supervisor_id: owner.supervisor_id,
                    lease_ttl_secs: owner.lease_ttl_secs,
                    recover_stale: owner.recover_stale,
                    continuation_token: None,
                },
            )?;
            to_value(&report)
        }
        ("ccd_session_lifecycle", "heartbeat-session") => {
            let path = resolve_path(args)?;
            let owner = parse_session_owner(args)?;
            let actor_id = owner.actor_id.ok_or_else(|| {
                anyhow::anyhow!("missing required argument: session_owner.actor_id")
            })?;
            let report = session::heartbeat(
                &path,
                get_opt_str(args, "profile").as_deref(),
                session::SessionHeartbeatOptions {
                    actor_id,
                    activity: get_opt_str(args, "activity"),
                },
            )?;
            to_value(&report)
        }
        ("ccd_session_lifecycle", "clear-session") => {
            let path = resolve_path(args)?;
            let owner = parse_session_owner(args)?;
            let locality_id = locality_id_for_path(&path);
            let report = session::clear(
                &path,
                get_opt_str(args, "profile").as_deref(),
                locality_id.as_deref(),
                session::SessionClearOptions {
                    actor_id: owner.actor_id,
                    reason: get_opt_str(args, "reason"),
                },
            )?;
            to_value(&report)
        }
        ("ccd_session_lifecycle", "takeover-session") => {
            let path = resolve_path(args)?;
            let owner = parse_session_owner(args)?;
            let actor_id = owner.actor_id.ok_or_else(|| {
                anyhow::anyhow!("missing required argument: session_owner.actor_id")
            })?;
            let locality_id = locality_id_for_path(&path);
            let reason = get_required_string(args, "reason")?;
            let report = session::takeover(
                &path,
                get_opt_str(args, "profile").as_deref(),
                locality_id.as_deref(),
                session::SessionTakeoverOptions {
                    actor_id,
                    supervisor_id: owner.supervisor_id,
                    reason,
                },
            )?;
            to_value(&report)
        }
        ("ccd_session_gates", "list-gates") => {
            let path = resolve_path(args)?;
            let report = session_gates::list(&path, get_opt_str(args, "profile").as_deref())?;
            to_value(&report)
        }
        ("ccd_session_gates", "replace-gates") => {
            let path = resolve_path(args)?;
            let report = session_gates::replace(
                &path,
                get_opt_str(args, "profile").as_deref(),
                get_string_list(args, "gates")?,
                parse_protected_write(args),
            )?;
            to_value(&report)
        }
        ("ccd_session_gates", "seed-gates") => {
            let path = resolve_path(args)?;
            let source = parse_gate_seed_source(&get_required_string(args, "from")?)?;
            let report = session_gates::seed(
                &path,
                get_opt_str(args, "profile").as_deref(),
                source,
                parse_protected_write(args),
            )?;
            to_value(&report)
        }
        ("ccd_session_gates", "set-gate-status") => {
            let path = resolve_path(args)?;
            let index = get_required_usize(args, "index")?;
            let status = parse_gate_status(&get_required_string(args, "status")?)?;
            let report = session_gates::set_status(
                &path,
                get_opt_str(args, "profile").as_deref(),
                index,
                status,
                parse_protected_write(args),
            )?;
            to_value(&report)
        }
        ("ccd_session_gates", "advance-gates") => {
            let path = resolve_path(args)?;
            let report = session_gates::advance(
                &path,
                get_opt_str(args, "profile").as_deref(),
                parse_protected_write(args),
            )?;
            to_value(&report)
        }
        ("ccd_session_gates", "clear-gates") => {
            let path = resolve_path(args)?;
            let report = session_gates::clear(
                &path,
                get_opt_str(args, "profile").as_deref(),
                parse_protected_write(args),
            )?;
            to_value(&report)
        }
        // ── ccd_state ─────────────────────────────────────────────
        ("ccd_state", "state-export") => {
            let path = resolve_path(args)?;
            let target = get_opt_str(args, "projection_target")
                .map(|s| parse_projection_target(&s))
                .transpose()?;
            let format = get_opt_str(args, "projection_format")
                .map(|s| parse_projection_format(&s))
                .transpose()?;
            let report = state_export::run(
                &path,
                get_opt_str(args, "profile").as_deref(),
                target,
                format,
            )?;
            to_value(&report)
        }
        ("ccd_state", "policy-check") => {
            let path = resolve_path(args)?;
            let action_family = get_required_string(args, "action_family")?;
            let report = policy_check::run(
                &path,
                get_opt_str(args, "profile").as_deref(),
                parse_policy_action_family(&action_family)?,
            )?;
            to_value(&report)
        }
        ("ccd_escalation", "list-escalations") => {
            let path = resolve_path(args)?;
            let report = escalation::list(&path, get_opt_str(args, "profile").as_deref())?;
            to_value(&report)
        }
        ("ccd_escalation", "set-escalation") => {
            let path = resolve_path(args)?;
            let kind = get_opt_str(args, "kind")
                .map(|value| parse_escalation_kind(&value))
                .transpose()?;
            let reason = get_required_string(args, "reason")?;
            let id = get_opt_str(args, "id").or_else(|| get_runtime_request_idempotency_key(args));
            let report = escalation::set(
                &path,
                get_opt_str(args, "profile").as_deref(),
                id.as_deref(),
                parse_protected_write(args),
                kind,
                &reason,
            )?;
            to_value(&report)
        }
        ("ccd_escalation", "clear-escalation") => {
            let path = resolve_path(args)?;
            let report = escalation::clear(
                &path,
                get_opt_str(args, "profile").as_deref(),
                get_opt_str(args, "id").as_deref(),
                parse_protected_write(args),
            )?;
            to_value(&report)
        }
        ("ccd_recovery", "write-recovery") => {
            let path = resolve_path(args)?;
            let report = recovery::write_with_input(
                &path,
                get_opt_str(args, "profile").as_deref(),
                parse_recovery_write_input(args)?,
                parse_protected_write(args),
            )?;
            to_value(&report)
        }
        ("ccd_state", "handover") => {
            let path = resolve_path(args)?;
            let result = radar::handover(&path, get_opt_str(args, "profile").as_deref())?;
            with_ccd1_prompt(
                to_value(&result.report)?,
                args,
                crate::ccd1_prompt::JsonSurface::Handover,
            )
        }
        ("ccd_state", "radar-state") => {
            anyhow::bail!(
                "`ccd_state` command `radar-state` has been renamed to `handover` in the beta \
                 clean-break (#609). Update the caller to invoke `handover` with the same arguments."
            )
        }
        ("ccd_state", "checkpoint") => {
            let path = resolve_path(args)?;
            let result = radar::checkpoint(&path, get_opt_str(args, "profile").as_deref())?;
            let value = with_ccd1_prompt(
                to_value(&result)?,
                args,
                crate::ccd1_prompt::JsonSurface::Checkpoint,
            )?;
            crate::output::apply_default_json_contract(
                value,
                crate::output::DefaultJsonContract::Checkpoint,
            )
        }
        ("ccd_context", "context-check") => {
            let path = resolve_path(args)?;
            let trigger = get_opt_str(args, "trigger")
                .map(|value| parse_context_check_trigger(&value))
                .transpose()?
                .unwrap_or(radar::ContextCheckTrigger::Interval);
            let report =
                radar::run_context_check(&path, get_opt_str(args, "profile").as_deref(), trigger)?;
            with_ccd1_prompt(
                to_value(&report)?,
                args,
                crate::ccd1_prompt::JsonSurface::ContextCheck,
            )
        }
        ("ccd_state", "handoff-refresh") => {
            let path = resolve_path(args)?;
            let report = handoff::refresh::run(
                &path,
                get_opt_str(args, "profile").as_deref(),
                get_bool(args, "write"),
            )?;
            to_value(&report)
        }

        (tool, cmd) => {
            bail!("unknown tool/command: {tool}/{cmd}")
        }
    }?;

    let report = compact_report(tool_name, command.as_str(), report, full, args);
    Ok(report)
}

// ---------------------------------------------------------------------------
// Arg extraction helpers
// ---------------------------------------------------------------------------

fn resolve_path(args: &Value) -> Result<PathBuf> {
    let raw = args.get("path").and_then(Value::as_str).unwrap_or(".");
    paths::cli::resolve(raw.as_ref())
}

fn locality_id_for_path(path: &std::path::Path) -> Option<String> {
    repo::marker::load(path)
        .ok()
        .flatten()
        .map(|marker| marker.locality_id)
}

fn get_required_string(args: &Value, key: &str) -> Result<String> {
    args.get(key)
        .and_then(Value::as_str)
        .map(String::from)
        .ok_or_else(|| anyhow::anyhow!("missing required argument: {key}"))
}

fn get_opt_str(args: &Value, key: &str) -> Option<String> {
    args.get(key).and_then(Value::as_str).map(String::from)
}

fn get_opt_str_any(args: &Value, keys: &[&str]) -> Option<String> {
    keys.iter().find_map(|key| get_opt_str(args, key))
}

fn get_bool(args: &Value, key: &str) -> bool {
    args.get(key).and_then(Value::as_bool).unwrap_or(false)
}

fn get_opt_u64(args: &Value, key: &str) -> Option<u64> {
    args.get(key).and_then(Value::as_u64)
}

#[allow(dead_code)]
fn get_opt_usize(args: &Value, key: &str) -> Result<Option<usize>> {
    let Some(value) = args.get(key) else {
        return Ok(None);
    };

    match value {
        Value::Null => Ok(None),
        Value::Number(_) => {
            let raw = value.as_u64().ok_or_else(|| {
                anyhow::anyhow!("invalid argument {key}: expected a positive integer")
            })?;
            usize::try_from(raw)
                .map(Some)
                .map_err(|_| anyhow::anyhow!("invalid argument {key}: value is too large"))
        }
        Value::String(raw) => {
            let parsed = raw.parse::<u128>().map_err(|_| {
                anyhow::anyhow!("invalid argument {key}: expected a positive integer")
            })?;
            usize::try_from(parsed)
                .map(Some)
                .map_err(|_| anyhow::anyhow!("invalid argument {key}: value is too large"))
        }
        _ => bail!("invalid argument {key}: expected a positive integer"),
    }
}

fn get_required_usize(args: &Value, key: &str) -> Result<usize> {
    if let Some(value) = args.get(key).and_then(Value::as_u64) {
        return usize::try_from(value)
            .map_err(|_| anyhow::anyhow!("invalid argument {key}: value is too large"));
    }

    let raw = get_required_string(args, key)?;
    raw.parse::<usize>()
        .map_err(|_| anyhow::anyhow!("invalid argument {key}: expected a positive integer"))
}

fn get_string_list(args: &Value, key: &str) -> Result<Vec<String>> {
    if let Some(values) = args.get(key).and_then(Value::as_array) {
        return values
            .iter()
            .map(|value| {
                value.as_str().map(str::to_owned).ok_or_else(|| {
                    anyhow::anyhow!("invalid argument {key}: array entries must be strings")
                })
            })
            .collect();
    }

    Ok(vec![get_required_string(args, key)?])
}

#[derive(Default)]
struct McpSessionOwnerArgs {
    lifecycle: Option<session::SessionLifecycle>,
    owner_kind: Option<session::SessionOwnerKind>,
    actor_id: Option<String>,
    supervisor_id: Option<String>,
    lease_ttl_secs: Option<u64>,
    recover_stale: bool,
}

fn parse_session_owner(args: &Value) -> Result<McpSessionOwnerArgs> {
    let owner = args.get("session_owner").and_then(Value::as_object);
    let lifecycle = owner
        .and_then(|obj| obj.get("lifecycle"))
        .and_then(Value::as_str)
        .or_else(|| args.get("lifecycle").and_then(Value::as_str))
        .map(parse_session_lifecycle)
        .transpose()?;
    let owner_kind = owner
        .and_then(|obj| obj.get("owner_kind"))
        .and_then(Value::as_str)
        .or_else(|| args.get("owner_kind").and_then(Value::as_str))
        .map(parse_session_owner_kind)
        .transpose()?;
    let actor_id = owner
        .and_then(|obj| obj.get("actor_id"))
        .and_then(Value::as_str)
        .map(String::from)
        .or_else(|| get_opt_str(args, "actor_id"));
    let supervisor_id = owner
        .and_then(|obj| obj.get("supervisor_id"))
        .and_then(Value::as_str)
        .map(String::from)
        .or_else(|| get_opt_str(args, "supervisor_id"));
    let lease_ttl_secs = owner
        .and_then(|obj| obj.get("lease_seconds"))
        .and_then(Value::as_u64)
        .or_else(|| get_opt_u64(args, "lease_seconds"));
    let recover_stale = owner
        .and_then(|obj| obj.get("recover_stale"))
        .and_then(Value::as_bool)
        .or_else(|| args.get("recover_stale").and_then(Value::as_bool))
        .unwrap_or(false);

    Ok(McpSessionOwnerArgs {
        lifecycle,
        owner_kind,
        actor_id,
        supervisor_id,
        lease_ttl_secs,
        recover_stale,
    })
}

fn parse_protected_write(args: &Value) -> protected_write::ExclusiveWriteOptions {
    let protected = args.get("protected_write").and_then(Value::as_object);
    protected_write::ExclusiveWriteOptions {
        actor_id: protected
            .and_then(|obj| obj.get("actor_id"))
            .and_then(Value::as_str)
            .map(str::to_owned)
            .or_else(|| get_opt_str(args, "actor_id")),
        session_id: protected
            .and_then(|obj| obj.get("session_id"))
            .and_then(Value::as_str)
            .map(str::to_owned)
            .or_else(|| get_opt_str(args, "session_id")),
        expected_revision: protected
            .and_then(|obj| obj.get("expected_revision"))
            .and_then(Value::as_u64)
            .or_else(|| get_opt_u64(args, "expected_revision")),
    }
}

fn to_value(report: &impl Serialize) -> Result<Value> {
    Ok(serde_json::to_value(report)?)
}

fn with_ccd1_prompt(
    mut value: Value,
    args: &Value,
    surface: crate::ccd1_prompt::JsonSurface,
) -> Result<Value> {
    if !get_bool(args, "no_ccd1_prompt") {
        crate::ccd1_prompt::insert_json_surface(&mut value, surface)?;
    }
    Ok(value)
}

fn get_runtime_request_idempotency_key(args: &Value) -> Option<String> {
    args.get("runtime_request")
        .and_then(Value::as_object)
        .and_then(|request| request.get("idempotency_key"))
        .and_then(Value::as_str)
        .map(str::to_owned)
}

fn parse_recovery_write_input(args: &Value) -> Result<recovery::RecoveryWriteInput> {
    let checkpoint = args
        .get("checkpoint")
        .map(|checkpoint| {
            serde_json::from_value::<recovery::CheckpointInput>(checkpoint.clone())
                .context("invalid argument checkpoint")
        })
        .transpose()?;
    let working_buffer = args
        .get("working_buffer")
        .map(|working_buffer| {
            serde_json::from_value::<recovery::WorkingBufferInput>(working_buffer.clone())
                .context("invalid argument working_buffer")
        })
        .transpose()?;

    Ok(recovery::RecoveryWriteInput {
        origin: get_opt_str(args, "origin"),
        checkpoint,
        working_buffer,
    })
}

// ---------------------------------------------------------------------------
// Compact report post-processing
// ---------------------------------------------------------------------------

fn compact_report(
    tool_name: &str,
    command: &str,
    mut report: Value,
    full: bool,
    args: &Value,
) -> Value {
    if full {
        return report;
    }

    match (tool_name, command) {
        ("ccd_health", "doctor") => compact_doctor_report(&mut report),
        ("ccd_state", "state-export") => {
            // Don't strip projection fields when the caller explicitly requested them.
            let has_projection_args =
                args.get("projection_target").is_some() || args.get("projection_format").is_some();
            if !has_projection_args {
                compact_state_export(&mut report);
            }
        }
        _ => {}
    }

    report
}

fn compact_doctor_report(report: &mut Value) {
    let Some(checks) = report.get_mut("checks").and_then(Value::as_array_mut) else {
        return;
    };

    checks.retain(|check| check.get("status").and_then(Value::as_str) != Some("pass"));
}

fn compact_state_export(report: &mut Value) {
    let Some(export) = report.get_mut("export").and_then(Value::as_object_mut) else {
        return;
    };

    export.remove("projection");
    export.remove("symbolic_projection");
    export.remove("bundle_projection");
    export.remove("state");
}

// ---------------------------------------------------------------------------
// Enum parsers
// ---------------------------------------------------------------------------

fn parse_projection_target(s: &str) -> Result<compiled::ProjectionTarget> {
    match s {
        "default" => Ok(compiled::ProjectionTarget::Default),
        "planning" => Ok(compiled::ProjectionTarget::Planning),
        "session" => Ok(compiled::ProjectionTarget::Session),
        other => {
            bail!("invalid projection_target: {other}; expected default, planning, or session")
        }
    }
}

fn parse_context_check_trigger(s: &str) -> Result<radar::ContextCheckTrigger> {
    match s {
        "interval" => Ok(radar::ContextCheckTrigger::Interval),
        "manual" => Ok(radar::ContextCheckTrigger::Manual),
        "pre-compaction" | "pre_compaction" => Ok(radar::ContextCheckTrigger::PreCompaction),
        "idle-reset" | "idle_reset" => Ok(radar::ContextCheckTrigger::IdleReset),
        "resume" => Ok(radar::ContextCheckTrigger::Resume),
        "supervisor-poll" | "supervisor_poll" => Ok(radar::ContextCheckTrigger::SupervisorPoll),
        _ => bail!("invalid trigger `{s}`: expected interval, manual, pre-compaction, idle-reset, resume, or supervisor-poll"),
    }
}

fn parse_projection_format(s: &str) -> Result<compiled::ProjectionFormat> {
    match s {
        "narrative" => Ok(compiled::ProjectionFormat::Narrative),
        "symbolic" => Ok(compiled::ProjectionFormat::Symbolic),
        "bundle" => Ok(compiled::ProjectionFormat::Bundle),
        other => {
            bail!("invalid projection_format: {other}; expected narrative, symbolic, or bundle")
        }
    }
}

fn parse_session_mode(s: &str) -> Result<session::SessionMode> {
    session::SessionMode::from_str(s).ok_or_else(|| {
        anyhow::anyhow!("invalid mode: {s}; expected general, research, or implement")
    })
}

fn parse_session_lifecycle(s: &str) -> Result<session::SessionLifecycle> {
    match s {
        "interactive" => Ok(session::SessionLifecycle::Interactive),
        "autonomous" => Ok(session::SessionLifecycle::Autonomous),
        other => bail!("invalid lifecycle: {other}; expected interactive or autonomous"),
    }
}

fn parse_session_owner_kind(s: &str) -> Result<session::SessionOwnerKind> {
    match s {
        "runtime_worker" => Ok(session::SessionOwnerKind::RuntimeWorker),
        "runtime_supervisor" => Ok(session::SessionOwnerKind::RuntimeSupervisor),
        other => {
            bail!("invalid owner_kind: {other}; expected runtime_worker or runtime_supervisor")
        }
    }
}

fn parse_escalation_kind(s: &str) -> Result<escalation::EscalationKind> {
    match s {
        "blocking" => Ok(escalation::EscalationKind::Blocking),
        "non-blocking" | "non_blocking" => Ok(escalation::EscalationKind::NonBlocking),
        other => bail!("invalid kind: {other}; expected blocking or non-blocking"),
    }
}

fn parse_gate_seed_source(s: &str) -> Result<session_gates::GateSeedSource> {
    match s {
        "immediate-actions" | "immediate_actions" => {
            Ok(session_gates::GateSeedSource::ImmediateActions)
        }
        "definition-of-done" | "definition_of_done" => {
            Ok(session_gates::GateSeedSource::DefinitionOfDone)
        }
        other => bail!("invalid from: {other}; expected immediate-actions or definition-of-done"),
    }
}

fn parse_gate_status(s: &str) -> Result<session_gates::ExecutionGateStatus> {
    match s {
        "open" => Ok(session_gates::ExecutionGateStatus::Open),
        "done" => Ok(session_gates::ExecutionGateStatus::Done),
        "blocked" => Ok(session_gates::ExecutionGateStatus::Blocked),
        other => bail!("invalid status: {other}; expected open, done, or blocked"),
    }
}

fn parse_policy_action_family(
    s: &str,
) -> Result<crate::state::policy_projection::PolicyActionFamily> {
    use crate::state::policy_projection::PolicyActionFamily;

    match s {
        "read_only" => Ok(PolicyActionFamily::ReadOnly),
        "local_execution" => Ok(PolicyActionFamily::LocalExecution),
        "local_isolation" => Ok(PolicyActionFamily::LocalIsolation),
        "local_finalize" => Ok(PolicyActionFamily::LocalFinalize),
        "publish_external" => Ok(PolicyActionFamily::PublishExternal),
        "shared_queue_mutation" => Ok(PolicyActionFamily::SharedQueueMutation),
        "repair_destructive" => Ok(PolicyActionFamily::RepairDestructive),
        "protected_surface_mutation" => Ok(PolicyActionFamily::ProtectedSurfaceMutation),
        other => bail!(
            "invalid action_family: {other}; expected read_only, local_execution, local_isolation, local_finalize, publish_external, shared_queue_mutation, repair_destructive, or protected_surface_mutation"
        ),
    }
}

#[cfg(test)]
mod tests {
    use std::env;
    use std::fs;
    use std::sync::Mutex;

    use serde_json::json;
    use tempfile::tempdir;

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    struct HomeOverride {
        previous: Option<std::ffi::OsString>,
    }

    impl HomeOverride {
        fn set(path: &std::path::Path) -> Self {
            let previous = env::var_os("HOME");
            env::set_var("HOME", path);
            Self { previous }
        }
    }

    impl Drop for HomeOverride {
        fn drop(&mut self) {
            match self.previous.take() {
                Some(home) => env::set_var("HOME", home),
                None => env::remove_var("HOME"),
            }
        }
    }

    #[test]
    fn mcp_start_compact_default_keeps_warnings_and_full_keeps_rendered_surfaces() {
        let _guard = ENV_LOCK.lock().unwrap();
        let temp = tempdir().unwrap();
        let repo = temp.path().join("repo");
        let home = temp.path().join("home");
        fs::create_dir_all(&repo).unwrap();
        fs::create_dir_all(&home).unwrap();
        let _home = HomeOverride::set(&home);

        crate::commands::scaffold::run(&repo, false).unwrap();
        crate::commands::attach::run(&repo, None, None, Some("MCP UX fixture")).unwrap();

        let compact = super::dispatch(
            "ccd_session",
            &json!({
                "command": "start",
                "path": repo,
            }),
        )
        .unwrap();
        assert!(compact.get("compact_summary").is_some());
        assert!(compact.get("warnings").is_some());
        assert!(compact.get("manifest").is_none());
        assert!(compact.get("handoff").is_none());

        let full = super::dispatch(
            "ccd_session",
            &json!({
                "command": "start",
                "path": repo,
                "full": true,
            }),
        )
        .unwrap();
        assert!(full.get("warnings").is_some());
        assert!(full.get("manifest").is_some());
        assert!(full.get("handoff").is_some());
    }
}
