use std::io::Read as _;
use std::process::ExitCode;

use anyhow::{Context, Result};

use crate::commands;
use crate::output::{self, CommandReport, OutputFormat};
use crate::paths;
use crate::state;

struct RadarRenderOptions<'a> {
    format: OutputFormat,
    repo_path: &'a std::path::Path,
    explicit_profile: Option<&'a str>,
    fields: Option<Vec<String>>,
    memory_depth: Option<output::MemoryDepth>,
    commit: bool,
    close_clean: bool,
    since_session: Option<String>,
    contract: output::DefaultJsonContract,
    filter_spec: &'static output::FieldFilterSpec,
}

pub(crate) fn context_check(
    args: crate::ContextCheckArgs,
    format: OutputFormat,
) -> Result<ExitCode> {
    if args.fields.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--fields requires --output json");
    }
    if args.memory_depth.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--memory-depth requires --output json");
    }
    if let Some(fields) = &args.fields {
        output::validate_fields(&output::CONTEXT_CHECK_FIELD_FILTER_SPEC, fields)?;
    }
    let repo_path = paths::cli::resolve(&args.path)?;
    let report = state::radar::run_context_check(
        &repo_path,
        args.profile.as_deref(),
        args.trigger.into_trigger(),
    )?;
    let use_metadata = args.memory_depth == Some(output::MemoryDepth::Metadata);
    match &args.fields {
        Some(fields) => {
            let exit_code = report.exit_code();
            let mut value = serde_json::to_value(&report)?;
            if use_metadata {
                value = output::strip_memory_content(value);
            }
            let filtered = output::try_filter_json_fields(
                value,
                fields,
                &output::CONTEXT_CHECK_FIELD_FILTER_SPEC,
            )?;
            println!("{}", serde_json::to_string_pretty(&filtered)?);
            Ok(exit_code)
        }
        None if use_metadata => {
            let exit_code = report.exit_code();
            let mut value = serde_json::to_value(&report)?;
            value = output::strip_memory_content(value);
            println!("{}", serde_json::to_string_pretty(&value)?);
            Ok(exit_code)
        }
        None => output::render_report(format, &report),
    }
}

pub(crate) fn policy_check(args: crate::PolicyCheckArgs, format: OutputFormat) -> Result<ExitCode> {
    let repo_path = paths::cli::resolve(&args.path)?;
    let report = commands::policy_check::run(
        &repo_path,
        args.profile.as_deref(),
        args.action_family.into_policy_action_family(),
    )?;
    output::render_report(format, &report)
}

pub(crate) fn runtime_state(
    command: crate::RuntimeStateCommand,
    format: OutputFormat,
) -> Result<ExitCode> {
    match command {
        crate::RuntimeStateCommand::Export(args) => {
            let repo_path = paths::cli::resolve(&args.path)?;
            let report = state::runtime_export::run(
                &repo_path,
                args.profile.as_deref(),
                args.projection_target
                    .map(crate::ProjectionTargetArg::into_projection_target),
                args.projection_format
                    .map(crate::ProjectionFormatArg::into_projection_format),
            )?;
            output::render_report(format, &report)
        }
        crate::RuntimeStateCommand::ChildBootstrap(args) => {
            let repo_path = paths::cli::resolve(&args.path)?;
            let report = state::child_bootstrap::run(&repo_path, args.profile.as_deref())?;
            output::render_report(format, &report)
        }
    }
}

pub(crate) fn thread(command: crate::ThreadCommand, format: OutputFormat) -> Result<ExitCode> {
    match command {
        crate::ThreadCommand::Export(args) => {
            let repo_path = paths::cli::resolve(&args.path)?;
            let report = state::thread_transfer::export(&repo_path, args.profile.as_deref())?;
            output::render_report(format, &report)
        }
        crate::ThreadCommand::Import(args) => {
            let repo_path = paths::cli::resolve(&args.path)?;
            let report = state::thread_transfer::import_preview(
                &repo_path,
                args.profile.as_deref(),
                &args.from,
                args.preview,
                args.write,
            )?;
            output::render_report(format, &report)
        }
    }
}

pub(crate) fn escalation_state(
    command: crate::EscalationStateCommand,
    format: OutputFormat,
) -> Result<ExitCode> {
    match command {
        crate::EscalationStateCommand::Set(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::escalation::set(
                &repo_path,
                args.base.profile.as_deref(),
                args.id.as_deref(),
                args.protected_write.into_state_options(),
                Some(args.kind.into_kind()),
                &args.reason,
            )?;
            output::render_report(format, &report)
        }
        crate::EscalationStateCommand::Clear(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::escalation::clear(
                &repo_path,
                args.base.profile.as_deref(),
                args.id.as_deref(),
                args.protected_write.into_state_options(),
            )?;
            output::render_report(format, &report)
        }
        crate::EscalationStateCommand::List(args) => {
            let repo_path = paths::cli::resolve(&args.base.path)?;
            let report = state::escalation::list(&repo_path, args.base.profile.as_deref())?;
            output::render_report(format, &report)
        }
    }
}

pub(crate) fn handover(args: crate::HandoverArgs, format: OutputFormat) -> Result<ExitCode> {
    if args.fields.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--fields requires --output json");
    }
    if args.memory_depth.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--memory-depth requires --output json");
    }
    if args.commit && format != OutputFormat::Json {
        anyhow::bail!("--commit requires --output json");
    }
    if args.close_clean && format != OutputFormat::Json {
        anyhow::bail!("--close-clean requires --output json");
    }
    if args.commit && args.close_clean {
        anyhow::bail!("--commit and --close-clean are mutually exclusive");
    }
    if args.close_clean && args.since_session.is_some() {
        anyhow::bail!("--close-clean and --since-session are mutually exclusive");
    }
    if args.since_session.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--since-session requires --output json");
    }
    if let Some(fields) = &args.fields {
        output::validate_fields(&output::HANDOVER_FIELD_FILTER_SPEC, fields)?;
    }
    let repo_path = paths::cli::resolve(&args.path)?;
    let result = state::radar::handover(&repo_path, args.profile.as_deref())?;
    let current_digests = args
        .since_session
        .as_ref()
        .map(|_| result.projection_digests.clone());

    render_radar_like_report(
        RadarRenderOptions {
            format,
            repo_path: &repo_path,
            explicit_profile: args.profile.as_deref(),
            fields: args.fields,
            memory_depth: args.memory_depth,
            commit: args.commit,
            close_clean: args.close_clean,
            since_session: args.since_session,
            contract: output::DefaultJsonContract::Handover,
            filter_spec: &output::HANDOVER_FIELD_FILTER_SPEC,
        },
        result.report,
        current_digests,
    )
}

pub(crate) fn checkpoint(args: crate::CheckpointArgs, format: OutputFormat) -> Result<ExitCode> {
    if args.fields.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--fields requires --output json");
    }
    if args.memory_depth.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--memory-depth requires --output json");
    }
    if args.commit && format != OutputFormat::Json {
        anyhow::bail!("--commit requires --output json");
    }
    if args.since_session.is_some() && format != OutputFormat::Json {
        anyhow::bail!("--since-session requires --output json");
    }
    if let Some(fields) = &args.fields {
        output::validate_fields(&output::CHECKPOINT_FIELD_FILTER_SPEC, fields)?;
    }
    let repo_path = paths::cli::resolve(&args.path)?;
    let result = state::radar::checkpoint(&repo_path, args.profile.as_deref())?;
    let current_digests = args
        .since_session
        .as_ref()
        .map(|_| result.projection_digests.clone());

    render_radar_like_report(
        RadarRenderOptions {
            format,
            repo_path: &repo_path,
            explicit_profile: args.profile.as_deref(),
            fields: args.fields,
            memory_depth: args.memory_depth,
            commit: args.commit,
            close_clean: false,
            since_session: args.since_session,
            contract: output::DefaultJsonContract::Checkpoint,
            filter_spec: &output::CHECKPOINT_FIELD_FILTER_SPEC,
        },
        result,
        current_digests,
    )
}

fn close_clean_result(
    repo_path: &std::path::Path,
    explicit_profile: Option<&str>,
    report: &serde_json::Value,
) -> Result<serde_json::Value> {
    let refusal = close_clean_refusal_reason(report);
    if let Some(reason) = refusal {
        return Ok(serde_json::json!({
            "ok": false,
            "outcome": "refused",
            "reason": reason,
            "needed_human_decision": true,
            "session_cleared": false,
            "handoff_candidates_applied": 0,
            "recovery_written": false,
        }));
    }

    let result = state::radar::commit_writes(
        repo_path,
        explicit_profile,
        state::radar::CommitPayload {
            handoff: None,
            recovery: None,
            clear_session: true,
        },
    );
    Ok(serde_json::json!({
        "ok": result.ok,
        "outcome": if result.ok { "applied" } else { "failed" },
        "reason": result.error,
        "needed_human_decision": false,
        "session_cleared": result.session_cleared.unwrap_or(false),
        "handoff_candidates_applied": 0,
        "recovery_written": false,
    }))
}

fn close_clean_refusal_reason(report: &serde_json::Value) -> Option<String> {
    let next_step = report
        .pointer("/evaluation/next_step/status")
        .and_then(|value| value.as_str())
        .unwrap_or("unknown");
    if next_step != "no_change_detected" {
        return Some(format!(
            "clean close-out requires no-change evaluation; found `{next_step}` and needs a human decision"
        ));
    }

    let boundary = report
        .pointer("/session_boundary/action")
        .and_then(|value| value.as_str())
        .unwrap_or("unknown");
    if boundary != "wrap_up" {
        return Some(format!(
            "clean close-out requires wrap_up session boundary; found `{boundary}` and needs a human decision"
        ));
    }

    let handoff_candidates = report
        .pointer("/candidate_updates/handoff")
        .and_then(|value| value.as_array())
        .map_or(0, |items| items.len());
    let memory_candidates = report
        .pointer("/candidate_updates/memory")
        .and_then(|value| value.as_array())
        .map_or(0, |items| items.len());
    if handoff_candidates + memory_candidates > 0 {
        return Some(format!(
            "clean close-out refuses {handoff_candidates} handoff candidate(s) and {memory_candidates} memory candidate(s); candidate application needs a human decision"
        ));
    }

    let unfinished_gates = report
        .pointer("/execution_gates/unfinished_count")
        .and_then(|value| value.as_u64())
        .unwrap_or(0);
    if unfinished_gates > 0 {
        return Some(format!(
            "clean close-out refuses {unfinished_gates} unfinished execution gate(s); gate disposition needs a human decision"
        ));
    }

    let drift_status = report
        .pointer("/behavioral_drift/status")
        .and_then(|value| value.as_str())
        .unwrap_or("unknown");
    if !matches!(drift_status, "aligned" | "no_signal") {
        return Some(format!(
            "clean close-out requires aligned drift state; found `{drift_status}` and needs a human decision"
        ));
    }

    None
}

fn render_radar_like_report<R>(
    options: RadarRenderOptions<'_>,
    report: R,
    current_digests: Option<state::compiled::ProjectionDigests>,
) -> Result<ExitCode>
where
    R: serde::Serialize + CommandReport,
{
    let use_metadata = options.memory_depth == Some(output::MemoryDepth::Metadata);
    let needs_json = options.fields.is_some()
        || options.commit
        || options.close_clean
        || use_metadata
        || options.since_session.is_some();

    if needs_json {
        let mut value = serde_json::to_value(&report)?;

        if let Some(ref since) = options.since_session {
            value = crate::apply_since_session_delta(
                value,
                options.repo_path,
                options.explicit_profile,
                since,
                current_digests
                    .as_ref()
                    .expect("digests available when --since-session is set"),
            )?;
        }

        if options.commit {
            let mut buf = String::new();
            std::io::stdin()
                .read_to_string(&mut buf)
                .context("failed to read commit payload from stdin")?;
            let payload: state::radar::CommitPayload =
                serde_json::from_str(&buf).context("failed to parse commit payload JSON")?;
            let commit_result =
                state::radar::commit_writes(options.repo_path, options.explicit_profile, payload);
            value["commit"] = serde_json::to_value(&commit_result)?;
        }

        if options.close_clean {
            value["close_clean"] =
                close_clean_result(options.repo_path, options.explicit_profile, &value)?;
        }

        if use_metadata {
            value = output::strip_memory_content(value);
        }
        if let Some(ref fields) = options.fields {
            value = output::try_filter_json_fields(value, fields, options.filter_spec)?;
        } else {
            value = output::apply_default_json_contract(value, options.contract)?;
        }

        println!("{}", serde_json::to_string_pretty(&value)?);
        Ok(ExitCode::SUCCESS)
    } else if options.format == OutputFormat::Json {
        let mut value = serde_json::to_value(&report)?;
        value = output::apply_default_json_contract(value, options.contract)?;
        println!("{}", serde_json::to_string_pretty(&value)?);
        Ok(ExitCode::SUCCESS)
    } else {
        output::render_report(options.format, &report)
    }
}
