use std::path::{Path, PathBuf};
use std::process::ExitCode;

use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sha2::{Digest, Sha256};

use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::state::compiled as compiled_state;
use crate::state::continuity as runtime_state;
use crate::state::escalation as escalation_state;
use crate::state::session as session_state;
use crate::state::session_gates;
use crate::state::thread as thread_state;

const CAPSULE_KIND: &str = "ccd.thread_transfer";
// Phase 7 (slimdown row 11): schema bumped 1 → 2 with the wire-format envelope
// trim. The bump itself is the gate — pre-Phase-7 v1 capsules are rejected by
// `validate_capsule_header` with an actionable message, satisfying the
// "Capsule wire gate" exit criterion in docs/plans/2026-05-09-ccd-slimdown.md.
const CAPSULE_SCHEMA_VERSION: u32 = 2;
const CAPSULE_STABILITY: &str = "experimental";
const KERNEL_CONTINUITY_SCHEMA_VERSION: u32 = 1;

#[derive(Serialize)]
pub struct ThreadExportReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    project_id: String,
    locality_id: String,
    export: ThreadExportView,
}

#[derive(Serialize)]
struct ThreadExportView {
    kind: &'static str,
    stability: &'static str,
    load_mode: &'static str,
    derived: bool,
    portable: bool,
    notice: &'static str,
    capsule: ThreadTransferCapsule,
    diagnostics: Vec<ThreadDiagnostic>,
}

#[derive(Serialize)]
pub struct ThreadImportReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    project_id: String,
    locality_id: String,
    import: ThreadImportView,
}

#[derive(Serialize)]
struct ThreadImportView {
    source: String,
    stability: &'static str,
    status: &'static str,
    capsule: CapsuleSummaryView,
    diagnostics: Vec<ThreadDiagnostic>,
}

#[derive(Serialize)]
struct ThreadTransferCapsule {
    kind: &'static str,
    schema_version: u32,
    stability: &'static str,
    thread: ThreadEnvelopeView,
    kernel_continuity: KernelContinuityView,
}

#[derive(Serialize)]
struct ThreadEnvelopeView {
    thread_id: String,
    source_project_id: String,
    source_locality_id: String,
    source_profile: String,
    source_workspace: String,
    created_at_epoch_s: u64,
    transfer_scope: &'static str,
}

#[derive(Serialize)]
struct KernelContinuityView {
    schema_version: u32,
    handoff: runtime_state::RuntimeHandoffState,
    recovery: runtime_state::RuntimeRecoveryView,
    execution_gates: session_gates::ExecutionGatesView,
    escalation: escalation_state::EscalationView,
}

#[derive(Serialize)]
pub(crate) struct ThreadDiagnostic {
    pub(crate) severity: &'static str,
    pub(crate) code: &'static str,
    pub(crate) message: String,
}

#[derive(Serialize)]
struct CapsuleSummaryView {
    kind: String,
    schema_version: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    thread_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    source_profile: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    source_project_id: Option<String>,
}

#[derive(Deserialize)]
struct ThreadTransferCapsuleInput {
    kind: String,
    schema_version: u32,
    #[serde(default)]
    thread: JsonValue,
}

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

    fn render_text(&self) {
        println!(
            "Exported thread transfer capsule for profile `{}` in linked project `{}`.",
            self.profile, self.project_id
        );
        println!(
            "Capsule `{}` is envelope-only (kernel continuity).",
            self.export.capsule.thread.thread_id,
        );
        println!("{}", self.export.notice);
    }
}

impl CommandReport for ThreadImportReport {
    fn exit_code(&self) -> ExitCode {
        if self.ok {
            ExitCode::SUCCESS
        } else {
            ExitCode::from(1)
        }
    }

    fn render_text(&self) {
        println!(
            "Inspected thread transfer capsule for profile `{}` in linked project `{}`.",
            self.profile, self.project_id
        );
        println!("Import status: {}.", self.import.status);
        for diagnostic in &self.import.diagnostics {
            println!("- {}: {}", diagnostic.code, diagnostic.message);
        }
    }
}

pub fn export(repo_root: &Path, explicit_profile: Option<&str>) -> Result<ThreadExportReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    ensure_profile_exists(&layout, repo_root, "export")?;
    let locality_id = ensure_repo_linked(repo_root)?;
    let runtime = runtime_state::load_runtime_state(repo_root, &layout, &locality_id)?;
    let escalation_entries = escalation_state::load_for_layout(&layout)?;
    let escalation = escalation_state::build_view(&layout, &escalation_entries);
    let created_at_epoch_s = session_state::now_epoch_s()?;
    let thread_id = thread_state::migrated_legacy_thread_id(
        repo_root,
        &layout,
        profile.as_str(),
        &locality_id,
    )?;
    thread_state::record_exported(
        &layout,
        &locality_id,
        &thread_id,
        serde_json::json!({ "transfer_scope": "portable_resume_context" }),
    )?;

    Ok(ThreadExportReport {
        command: "thread-export",
        ok: true,
        path: repo_root.display().to_string(),
        profile: profile.to_string(),
        project_id: locality_id.clone(),
        locality_id: locality_id.clone(),
        export: ThreadExportView {
            kind: "thread_transfer_capsule",
            stability: CAPSULE_STABILITY,
            load_mode: "native_canonical",
            derived: true,
            portable: true,
            notice: "Portable resume context only. CCD owns the transfer envelope; payload-bearing attachments are out of scope.",
            capsule: ThreadTransferCapsule {
                kind: CAPSULE_KIND,
                schema_version: CAPSULE_SCHEMA_VERSION,
                stability: CAPSULE_STABILITY,
                thread: ThreadEnvelopeView {
                    thread_id,
                    source_project_id: locality_id.clone(),
                    source_locality_id: locality_id,
                    source_profile: profile.to_string(),
                    source_workspace: repo_root.display().to_string(),
                    created_at_epoch_s,
                    transfer_scope: "portable_resume_context",
                },
                kernel_continuity: KernelContinuityView {
                    schema_version: KERNEL_CONTINUITY_SCHEMA_VERSION,
                    handoff: runtime.state.handoff,
                    recovery: runtime_state::recovery_view(&runtime.recovery),
                    execution_gates: runtime.execution_gates.view,
                    escalation,
                },
            },
            diagnostics: Vec::new(),
        },
    })
}

pub fn import_preview(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    from: &Path,
) -> Result<ThreadImportReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    ensure_profile_exists(&layout, repo_root, "import")?;
    let locality_id = ensure_repo_linked(repo_root)?;
    let source_path = display_source_path(from);
    let capsule = read_capsule(from)?;
    validate_capsule_header(&capsule)?;
    if let Some(thread_id) = json_string_at(&capsule.thread, "/thread_id")
        .or_else(|| json_string_at(&capsule.thread, "/id"))
    {
        let _ = thread_state::record_imported(
            &layout,
            &locality_id,
            &thread_id,
            serde_json::json!({ "source": source_path }),
        );
    }

    Ok(ThreadImportReport {
        command: "thread-import",
        ok: true,
        path: repo_root.display().to_string(),
        profile: profile.to_string(),
        project_id: locality_id.clone(),
        locality_id,
        import: ThreadImportView {
            source: source_path,
            stability: CAPSULE_STABILITY,
            status: "envelope_valid",
            capsule: capsule_summary(&capsule),
            diagnostics: Vec::new(),
        },
    })
}

fn read_capsule(from: &Path) -> Result<ThreadTransferCapsuleInput> {
    let contents = std::fs::read_to_string(from).with_context(|| {
        format!(
            "failed to read thread transfer capsule from {}",
            from.display()
        )
    })?;
    let value: JsonValue =
        serde_json::from_str(&contents).context("failed to parse thread transfer JSON")?;
    let capsule_value = if value.get("kind").is_some() {
        value
    } else if let Some(capsule) = value.pointer("/export/capsule") {
        capsule.clone()
    } else {
        bail!(
            "thread transfer input must be a `{CAPSULE_KIND}` capsule or a `thread export` report containing `export.capsule`"
        );
    };

    serde_json::from_value(capsule_value).context("failed to parse thread transfer capsule")
}

fn validate_capsule_header(capsule: &ThreadTransferCapsuleInput) -> Result<()> {
    if capsule.kind != CAPSULE_KIND {
        bail!(
            "unsupported thread transfer kind `{}`; expected `{}`",
            capsule.kind,
            CAPSULE_KIND
        );
    }
    if capsule.schema_version != CAPSULE_SCHEMA_VERSION {
        bail!(
            "unsupported thread transfer schema_version {}; expected {}",
            capsule.schema_version,
            CAPSULE_SCHEMA_VERSION
        );
    }
    Ok(())
}

fn capsule_summary(capsule: &ThreadTransferCapsuleInput) -> CapsuleSummaryView {
    CapsuleSummaryView {
        kind: capsule.kind.clone(),
        schema_version: capsule.schema_version,
        thread_id: json_string_at(&capsule.thread, "/thread_id")
            .or_else(|| json_string_at(&capsule.thread, "/id")),
        source_profile: json_string_at(&capsule.thread, "/source_profile"),
        source_project_id: json_string_at(&capsule.thread, "/source_project_id")
            .or_else(|| json_string_at(&capsule.thread, "/source_locality_id")),
    }
}

pub(crate) fn sha256_hex(value: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(value.as_bytes());
    format!("{:x}", hasher.finalize())
}

fn json_string_at(value: &JsonValue, pointer: &str) -> Option<String> {
    value
        .pointer(pointer)
        .and_then(JsonValue::as_str)
        .map(str::to_owned)
}

pub(crate) fn stable_thread_id(profile: &str, locality_id: &str) -> String {
    let seed = format!("{profile}\0{locality_id}");
    let digest = compiled_state::sha256_string(&seed);
    format!("thread_{}", &digest[..26])
}

fn display_source_path(path: &Path) -> String {
    absolutize_path(path)
        .unwrap_or_else(|| path.to_path_buf())
        .display()
        .to_string()
}

fn absolutize_path(path: &Path) -> Option<PathBuf> {
    if path.is_absolute() {
        return Some(path.to_path_buf());
    }
    Some(std::env::current_dir().ok()?.join(path))
}

pub(crate) fn ensure_profile_exists(
    layout: &StateLayout,
    repo_root: &Path,
    command: &str,
) -> Result<()> {
    let profile_root = layout.profile_root();
    if profile_root.is_dir() {
        return Ok(());
    }

    bail!(
        "profile `{}` does not exist at {}; bootstrap it with `ccd attach --path {}` before using `ccd thread {}`",
        layout.profile(),
        profile_root.display(),
        repo_root.display(),
        command
    )
}

pub(crate) fn ensure_repo_linked(repo_root: &Path) -> Result<String> {
    let Some(marker) = repo_marker::load(repo_root)? else {
        bail!(
            "repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
            repo_root.join(repo_marker::MARKER_FILE).display(),
            repo_root.display(),
            repo_root.display()
        )
    };

    Ok(marker.locality_id)
}
