use std::process::ExitCode;

use anyhow::{Context, Result};
use clap::ValueEnum;
use serde::Serialize;

use crate::handoff::GitState;

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum OutputFormat {
    Text,
    Json,
    #[value(name = "hook-protocol")]
    HookProtocol,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum MemoryDepth {
    Content,
    Metadata,
}

pub trait CommandReport: Serialize {
    fn exit_code(&self) -> ExitCode;
    fn render_text(&self);
}

#[derive(Serialize)]
struct ErrorReport {
    ok: bool,
    error: String,
}

pub struct FieldFilterSpec {
    valid_fields: &'static [&'static str],
    expensive_fields: &'static [&'static str],
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DefaultJsonContract {
    Start,
    Status,
    Handover,
    Checkpoint,
}

#[derive(Serialize)]
pub struct WorkStreamView {
    status: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    name: Option<String>,
}

pub fn work_stream_view(git: Option<&GitState>) -> WorkStreamView {
    match git {
        Some(git) if git.branch != "HEAD" => WorkStreamView {
            status: "available",
            name: Some(git.branch.clone()),
        },
        Some(_) => WorkStreamView {
            status: "detached",
            name: None,
        },
        None => WorkStreamView {
            status: "missing",
            name: None,
        },
    }
}

pub fn render_report<T: CommandReport>(format: OutputFormat, report: &T) -> Result<ExitCode> {
    match format {
        OutputFormat::Text => report.render_text(),
        OutputFormat::Json => println!("{}", serde_json::to_string_pretty(report)?),
        OutputFormat::HookProtocol => {
            anyhow::bail!(
                "--output hook-protocol is only supported for `ccd host-hook`; other commands must use --output text or --output json"
            )
        }
    }

    Ok(report.exit_code())
}

pub const START_FIELD_FILTER_SPEC: FieldFilterSpec = FieldFilterSpec {
    valid_fields: &[
        "command",
        "ok",
        "decisions",
        "agent_readiness",
        "disposition",
        "session_boundary",
        "path",
        "profile",
        "project_id",
        "workspace_path",
        "work_stream",
        "locality_id",
        "session_id",
        "compact_summary",
        "source_order",
        "manifest",
        "machine_identity",
        "machine_presence",
        "execution_context",
        "takeover_preconditions",
        "coordination_scope",
        "sources",
        "effective_policy",
        "policy_projection",
        "effective_memory",
        "memory_recall",
        "memory_provider",
        "startup_recall",
        "execution_gates",
        "handoff",
        "continuity",
        "recovery",
        "session_state",
        "escalation",
        "compiled_state",
        "alerts",
        "refresh_actions",
        "warnings",
        "activation",
    ],
    // These fields require the full start resolver to preserve their canonical
    // JSON meaning. Some need rendered source surfaces, and some depend on
    // provider diagnostics or recall work that the provider-light path skips.
    expensive_fields: &[
        "source_order",
        "manifest",
        "sources",
        "effective_policy",
        "policy_projection",
        "effective_memory",
        "memory_recall",
        "memory_provider",
        "startup_recall",
        "alerts",
        "warnings",
        "execution_gates",
        "handoff",
    ],
};

pub const STATUS_FIELD_FILTER_SPEC: FieldFilterSpec = FieldFilterSpec {
    valid_fields: &[
        "command",
        "ok",
        "path",
        "profile",
        "project_id",
        "locality_id",
        "workspace_path",
        "work_stream",
        "phase",
        "phase_reason",
        "blocked",
        "recommended_next_command",
        "bootstrap",
        "machine_identity",
        "machine_presence",
        "execution_context",
        "takeover_preconditions",
        "coordination_scope",
        "git",
        "checkout_state",
        "continuity",
        "session",
        "execution_gates",
        "escalation",
        "telemetry",
        "review",
        "warnings",
    ],
    expensive_fields: &[],
};

pub const DOCTOR_FIELD_FILTER_SPEC: FieldFilterSpec = FieldFilterSpec {
    valid_fields: &[
        "command",
        "ok",
        "repo_root",
        "failures",
        "warnings",
        "host_integrations",
        "agent_readiness",
        "agent_diagnostics",
        "checks",
    ],
    expensive_fields: &[],
};

pub const HANDOVER_FIELD_FILTER_SPEC: FieldFilterSpec = FieldFilterSpec {
    valid_fields: &[
        "command",
        "mode",
        "schema_version",
        "ok",
        "decisions",
        "path",
        "profile",
        "project_id",
        "locality_id",
        "delta_warning",
        "session_delta",
        "handoff",
        "execution_gates",
        "effective_memory",
        "repo_native_checks",
        "git",
        "checkout_state",
        "session_state",
        "recovery",
        "escalation",
        "projection_telemetry",
        "work_stream_decay",
        "context_health",
        "session_boundary",
        "behavioral_drift",
        "evaluation",
        "candidate_updates",
        "approval_intents",
        "approval_steps",
        "commit",
        "close_clean",
    ],
    expensive_fields: &["effective_memory", "repo_native_checks"],
};

pub const CHECKPOINT_FIELD_FILTER_SPEC: FieldFilterSpec = FieldFilterSpec {
    valid_fields: &[
        "command",
        "mode",
        "schema_version",
        "ok",
        "decisions",
        "path",
        "profile",
        "project_id",
        "locality_id",
        "delta_warning",
        "session_delta",
        "handoff",
        "execution_gates",
        "effective_memory",
        "git",
        "checkout_state",
        "session_state",
        "recovery",
        "escalation",
        "projection_telemetry",
        "work_stream_decay",
        "context_health",
        "session_boundary",
        "behavioral_drift",
        "candidate_updates",
        "commit",
    ],
    expensive_fields: &["effective_memory"],
};

pub const CONTEXT_CHECK_FIELD_FILTER_SPEC: FieldFilterSpec = FieldFilterSpec {
    valid_fields: &[
        "command",
        "schema_version",
        "ok",
        "decisions",
        "path",
        "profile",
        "project_id",
        "locality_id",
        "trigger",
        "action",
        "reason",
        "recommendation",
        "urgency",
        "throttle",
        "payload",
        "evidence",
        "session_state",
        "execution_gates",
        "recovery",
        "escalation",
        "context_health",
        "session_boundary",
        "behavioral_drift",
    ],
    expensive_fields: &[],
};

pub fn validate_fields(spec: &FieldFilterSpec, fields: &[String]) -> Result<()> {
    for field in fields {
        if !spec.valid_fields.contains(&field.as_str()) {
            anyhow::bail!(
                "unknown field: `{field}`; valid fields: {}",
                spec.valid_fields.join(", ")
            );
        }
    }

    Ok(())
}

pub fn try_filter_json_fields(
    mut value: serde_json::Value,
    fields: &[String],
    spec: &FieldFilterSpec,
) -> Result<serde_json::Value> {
    let obj = value
        .as_object_mut()
        .context("report is not a JSON object")?;
    validate_fields(spec, fields)?;
    let keys_to_remove: Vec<String> = obj
        .keys()
        .filter(|key| !fields.iter().any(|f| f == *key))
        .cloned()
        .collect();
    for key in keys_to_remove {
        obj.remove(&key);
    }
    Ok(value)
}

pub fn render_report_with_fields<T: CommandReport>(
    report: T,
    fields: &[String],
    spec: &FieldFilterSpec,
) -> Result<ExitCode> {
    let exit_code = report.exit_code();
    let value = serde_json::to_value(&report)?;
    let filtered = try_filter_json_fields(value, fields, spec)?;
    println!("{}", serde_json::to_string_pretty(&filtered)?);
    Ok(exit_code)
}

pub fn apply_default_json_contract(
    value: serde_json::Value,
    contract: DefaultJsonContract,
) -> Result<serde_json::Value> {
    let spec = match contract {
        DefaultJsonContract::Start => &START_FIELD_FILTER_SPEC,
        DefaultJsonContract::Status => &STATUS_FIELD_FILTER_SPEC,
        DefaultJsonContract::Handover => &HANDOVER_FIELD_FILTER_SPEC,
        DefaultJsonContract::Checkpoint => &CHECKPOINT_FIELD_FILTER_SPEC,
    };
    let fields = spec
        .valid_fields
        .iter()
        .map(|field| (*field).to_owned())
        .collect::<Vec<_>>();
    try_filter_json_fields(value, &fields, spec)
}

pub fn needs_expensive_rendering(spec: &FieldFilterSpec, fields: &[String]) -> bool {
    fields
        .iter()
        .any(|f| spec.expensive_fields.contains(&f.as_str()))
}

pub fn validate_start_fields(fields: &[String]) -> Result<()> {
    validate_fields(&START_FIELD_FILTER_SPEC, fields)
}

pub fn needs_full_start_resolution(fields: &[String]) -> bool {
    needs_expensive_rendering(&START_FIELD_FILTER_SPEC, fields)
}

pub fn render_error(format: OutputFormat, error: &anyhow::Error) {
    match format {
        OutputFormat::Text => eprintln!("error: {error:#}"),
        OutputFormat::Json | OutputFormat::HookProtocol => {
            let report = ErrorReport {
                ok: false,
                error: format!("{error:#}"),
            };
            println!(
                "{}",
                serde_json::to_string_pretty(&report).expect("error report serialization")
            );
        }
    }
}

/// When memory depth is `metadata`, replace all bulk content strings inside
/// `effective_memory` with empty strings while preserving structural metadata
/// (paths, counts, entry IDs, types, decay classes, etc.).
pub fn strip_memory_content(mut value: serde_json::Value) -> serde_json::Value {
    if let Some(mem) = value.pointer_mut("/effective_memory/content") {
        *mem = serde_json::Value::String(String::new());
    }
    for layer in &[
        "profile_entries",
        "repo_entries",
        "pod_entries",
        "branch_entries",
        "clone_entries",
    ] {
        let pointer = format!("/effective_memory/structured/{layer}");
        if let Some(serde_json::Value::Array(entries)) = value.pointer_mut(&pointer) {
            for entry in entries.iter_mut() {
                if let Some(c) = entry.get_mut("content") {
                    *c = serde_json::Value::String(String::new());
                }
            }
        }
    }
    value
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filter_fields_retains_only_requested_keys() {
        let value = serde_json::json!({
            "command": "start",
            "ok": true,
            "path": "/tmp/repo",
            "compact_summary": {"current_state": {"title": "Next Session"}},
            "compiled_state": {"schema_version": 3},
            "effective_memory": {"content": "big payload"},
            "sources": [{"kind": "project_truth"}]
        });

        let filtered = try_filter_json_fields(
            value,
            &[
                "command".to_owned(),
                "compact_summary".to_owned(),
                "compiled_state".to_owned(),
            ],
            &START_FIELD_FILTER_SPEC,
        )
        .unwrap();
        assert_eq!(filtered["command"], "start");
        assert!(filtered["compact_summary"].is_object());
        assert!(filtered["compiled_state"].is_object());
        assert!(filtered.get("effective_memory").is_none());
        assert!(filtered.get("sources").is_none());
        assert!(filtered.get("ok").is_none());
    }

    #[test]
    fn filter_fields_rejects_unknown_field() {
        let value = serde_json::json!({"command": "start", "ok": true});
        let err = try_filter_json_fields(
            value,
            &["command".to_owned(), "nonexistent".to_owned()],
            &START_FIELD_FILTER_SPEC,
        );
        assert!(err.is_err());
        assert!(err.unwrap_err().to_string().contains("nonexistent"));
    }

    #[test]
    fn handover_filter_fields_retains_only_requested_keys() {
        let value = serde_json::json!({
            "command": "handover",
            "ok": true,
            "schema_version": 20,
            "path": "/tmp/repo",
            "profile": "main",
            "project_id": "ccdrepo_abc",
            "locality_id": "ccdrepo_abc",
            "session_boundary": {"action": "continue"},
            "effective_memory": {"content": "big payload"},
            "evaluation": {"summary": "all good"},
        });

        let filtered = try_filter_json_fields(
            value,
            &[
                "command".to_owned(),
                "ok".to_owned(),
                "session_boundary".to_owned(),
            ],
            &HANDOVER_FIELD_FILTER_SPEC,
        )
        .unwrap();
        assert_eq!(filtered["command"], "handover");
        assert!(filtered["session_boundary"].is_object());
        assert!(filtered.get("effective_memory").is_none());
        assert!(filtered.get("evaluation").is_none());
    }

    #[test]
    fn handover_filter_fields_rejects_unknown_field() {
        let value = serde_json::json!({"command": "handover", "ok": true});
        let err = try_filter_json_fields(
            value,
            &["command".to_owned(), "bogus_field".to_owned()],
            &HANDOVER_FIELD_FILTER_SPEC,
        );
        assert!(err.is_err());
        assert!(err.unwrap_err().to_string().contains("bogus_field"));
    }

    #[test]
    fn strip_memory_content_removes_content_fields() {
        let value = serde_json::json!({
            "command": "start",
            "effective_memory": {
                "status": "loaded",
                "content": "profile memory block\nlocality memory block",
                "parts": [{"kind": "profile", "path": "/p", "status": "loaded"}],
                "structured": {
                    "status": "loaded",
                    "profile_entries": [
                        {"id": "mem_1", "type": "rule", "content": "some rule text"}
                    ],
                    "repo_entries": [],
                    "pod_entries": [],
                    "branch_entries": [],
                    "clone_entries": [],
                    "diagnostics": []
                }
            }
        });

        let stripped = strip_memory_content(value.clone());
        assert_eq!(stripped["effective_memory"]["content"], "");
        assert_eq!(
            stripped["effective_memory"]["structured"]["profile_entries"][0]["content"],
            ""
        );
        // Non-memory fields are untouched
        assert_eq!(stripped["command"], "start");
        // Structural metadata preserved
        assert_eq!(stripped["effective_memory"]["status"], "loaded");
        assert_eq!(
            stripped["effective_memory"]["structured"]["profile_entries"][0]["id"],
            "mem_1"
        );
    }

    #[test]
    fn strip_memory_content_is_idempotent_without_memory() {
        let value = serde_json::json!({"command": "start", "ok": true});
        let stripped = strip_memory_content(value.clone());
        assert_eq!(stripped, value);
    }

    #[test]
    fn default_start_json_contract_preserves_kernel_shape() {
        let value = serde_json::json!({
            "command": "start",
            "session_boundary": {"action": "continue"},
        });

        let filtered = apply_default_json_contract(value, DefaultJsonContract::Start).unwrap();

        assert_eq!(filtered["command"], "start");
        assert_eq!(filtered["session_boundary"]["action"], "continue");
    }

    #[test]
    fn default_status_json_contract_preserves_kernel_shape() {
        let value = serde_json::json!({
            "command": "status",
            "review": {"status": "idle"},
        });

        let filtered = apply_default_json_contract(value, DefaultJsonContract::Status).unwrap();

        assert_eq!(filtered["command"], "status");
        assert_eq!(filtered["review"]["status"], "idle");
    }

    #[test]
    fn default_handover_json_contract_preserves_kernel_shape() {
        let value = serde_json::json!({
            "command": "handover",
            "evaluation": {"next_step": {"status": "continue"}},
            "candidate_updates": {"handoff": [], "memory": []}
        });

        let filtered = apply_default_json_contract(value, DefaultJsonContract::Handover).unwrap();

        assert!(filtered["evaluation"].get("next_step").is_some());
        assert!(filtered["candidate_updates"].get("memory").is_some());
    }

    #[test]
    fn needs_expensive_rendering_detects_expensive_fields() {
        assert!(needs_expensive_rendering(
            &START_FIELD_FILTER_SPEC,
            &["effective_memory".to_owned()]
        ));
        assert!(needs_expensive_rendering(
            &START_FIELD_FILTER_SPEC,
            &["compiled_state".to_owned(), "handoff".to_owned()]
        ));
        assert!(needs_expensive_rendering(
            &START_FIELD_FILTER_SPEC,
            &["memory_recall".to_owned()]
        ));
        assert!(needs_expensive_rendering(
            &START_FIELD_FILTER_SPEC,
            &["alerts".to_owned()]
        ));
        assert!(!needs_expensive_rendering(
            &START_FIELD_FILTER_SPEC,
            &[
                "compact_summary".to_owned(),
                "compiled_state".to_owned(),
                "session_state".to_owned()
            ]
        ));
        assert!(!needs_expensive_rendering(
            &STATUS_FIELD_FILTER_SPEC,
            &["review".to_owned()]
        ));
    }
}
