use std::path::Path;
use std::process::ExitCode;

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

use crate::db;
use crate::display_safe::display_safe;
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::state::continuity::{RecoveryOrigin, RuntimeCheckpointState};
use crate::state::escalation::EscalationKind;
use crate::state::session::{self, SessionMode};
use crate::state::session_gates::ExecutionGateStatus;

pub(crate) const DEFAULT_RENEWAL_LEASE_TTL_SECS: u64 = 10 * 60;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum RenewalLeaseStatus {
    PendingReset,
    Fulfilled,
    Expired,
    Abandoned,
}

impl RenewalLeaseStatus {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::PendingReset => "pending_reset",
            Self::Fulfilled => "fulfilled",
            Self::Expired => "expired",
            Self::Abandoned => "abandoned",
        }
    }

    pub(crate) fn from_str(value: &str) -> Option<Self> {
        match value {
            "pending_reset" => Some(Self::PendingReset),
            "fulfilled" => Some(Self::Fulfilled),
            "expired" => Some(Self::Expired),
            "abandoned" => Some(Self::Abandoned),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum RenewalResetPath {
    OperatorManual,
    LifeloopNative,
    LifeloopWrapper,
    LifeloopManual,
}

impl RenewalResetPath {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::OperatorManual => "operator_manual",
            Self::LifeloopNative => "lifeloop_native",
            Self::LifeloopWrapper => "lifeloop_wrapper",
            Self::LifeloopManual => "lifeloop_manual",
        }
    }

    pub(crate) fn from_str(value: &str) -> Option<Self> {
        match value {
            "operator_manual" => Some(Self::OperatorManual),
            "lifeloop_native" => Some(Self::LifeloopNative),
            "lifeloop_wrapper" => Some(Self::LifeloopWrapper),
            "lifeloop_manual" => Some(Self::LifeloopManual),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LifeloopResetPath {
    Native,
    Wrapper,
    Manual,
}

impl LifeloopResetPath {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Native => "native",
            Self::Wrapper => "wrapper",
            Self::Manual => "manual",
        }
    }

    fn reset_capability_path(self) -> &'static [&'static str] {
        match self {
            Self::Native => &["renewal", "reset", "native"],
            Self::Wrapper => &["renewal", "reset", "wrapper_mediated"],
            Self::Manual => &["renewal", "reset", "manual"],
        }
    }

    fn into_lease_path(self) -> RenewalResetPath {
        match self {
            Self::Native => RenewalResetPath::LifeloopNative,
            Self::Wrapper => RenewalResetPath::LifeloopWrapper,
            Self::Manual => RenewalResetPath::LifeloopManual,
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct RenewalLease {
    pub(crate) renewal_lease_id: String,
    pub(crate) thread_id: String,
    pub(crate) previous_session_id: String,
    pub(crate) previous_session_mode: SessionMode,
    pub(crate) continuation_token: String,
    pub(crate) issued_at_epoch_s: u64,
    pub(crate) expires_at_epoch_s: u64,
    pub(crate) status: RenewalLeaseStatus,
    pub(crate) fulfilled_session_id: Option<String>,
    pub(crate) fulfilled_at_epoch_s: Option<u64>,
    pub(crate) reset_path: RenewalResetPath,
    pub(crate) lifeloop_adapter_id: Option<String>,
    pub(crate) lifeloop_receipt_id: Option<String>,
    pub(crate) lifecycle_evidence_json: String,
}

#[derive(Debug, Clone)]
pub(crate) struct RenewalPrepareOptions {
    pub(crate) lifeloop_adapter_id: Option<String>,
    pub(crate) lifeloop_reset_path: Option<LifeloopResetPath>,
    pub(crate) lifeloop_receipt_path: Option<std::path::PathBuf>,
}

#[derive(Serialize)]
pub(crate) struct RenewalPrepareReport {
    command: &'static str,
    ok: bool,
    profile: String,
    path: String,
    state_db: String,
    renewal: RenewalLeaseView,
    continuation: RenewalContinuationView,
    checkpoint: RenewalCheckpointView,
    instruction: String,
}

#[derive(Serialize)]
struct RenewalLeaseView {
    renewal_lease_id: String,
    status: RenewalLeaseStatus,
    thread_id: String,
    previous_session_id: String,
    previous_session_mode: SessionMode,
    issued_at_epoch_s: u64,
    expires_at_epoch_s: u64,
    reset_path: RenewalResetPath,
    #[serde(skip_serializing_if = "Option::is_none")]
    lifeloop_adapter_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    lifeloop_receipt_id: Option<String>,
}

#[derive(Serialize)]
struct RenewalContinuationView {
    token: String,
    path: String,
    command: String,
}

#[derive(Serialize)]
struct RenewalCheckpointView {
    status: &'static str,
    summary: String,
}

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

    fn render_text(&self) {
        println!(
            "Renewal lease prepared for thread `{}`; it expires at {}.",
            self.renewal.thread_id, self.renewal.expires_at_epoch_s
        );
        println!("{}", self.instruction);
        println!("{}", self.continuation.command);
    }
}

pub(crate) fn prepare(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    options: RenewalPrepareOptions,
) -> Result<RenewalPrepareReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    ensure_profile_exists(repo_root, &layout)?;
    let locality_id = repo_marker::load(repo_root)?
        .ok_or_else(|| {
            anyhow::anyhow!(
                "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()
            )
        })?
        .locality_id;

    let lifecycle_evidence = resolve_lifecycle_evidence(&options)?;
    let reset_path = lifecycle_evidence.reset_path;
    let lifeloop_adapter_id = lifecycle_evidence.adapter_id;
    let lifeloop_receipt_id = lifecycle_evidence.receipt_id;
    let lifecycle_evidence_json = lifecycle_evidence.evidence.to_string();

    let checkpoint = crate::state::radar::checkpoint(repo_root, explicit_profile)
        .context("failed to build renewal checkpoint before preparing the lease")?;
    let checkpoint_summary = format!(
        "Renewal checkpoint for `{}`. {}",
        checkpoint.handoff.title, checkpoint.session_boundary.inner.summary
    );
    let checkpoint_immediate_actions = checkpoint.handoff.immediate_actions.clone();
    let checkpoint_key_files = checkpoint.handoff.key_files.clone();

    let db = db::StateDb::open_for_layout(&layout)?;
    let mut prepared: Option<RenewalLease> = None;
    run_renewal_transaction(&db, |db| {
        let now = session::now_epoch_s()?;
        db::renewal::expire_pending_if_needed(db.conn(), now)?;
        if let Some(active) = db::renewal::read_pending(db.conn())? {
            bail!(
                "renewal lease `{}` is already pending for thread `{}`; complete it with its continuation token or wait until it expires",
                active.renewal_lease_id,
                active.thread_id
            );
        }

        let Some(active_session) = db::session::read(db.conn())? else {
            bail!("renewal prepare requires an active session; run `ccd start --activate --path {}` first", repo_root.display());
        };
        let previous_session_id = active_session.session_id.clone().ok_or_else(|| {
            anyhow::anyhow!("renewal prepare refused: active session is missing session_id")
        })?;
        if session::is_stale(&active_session, now) {
            bail!(
                "renewal prepare refused: session `{}` is stale; start or clear the session deliberately before renewal",
                display_safe(&previous_session_id)
            );
        }
        let thread_id = active_session.thread_id.clone().ok_or_else(|| {
            anyhow::anyhow!(
                "renewal prepare refused: active session `{}` is not bound to a first-class thread",
                display_safe(&previous_session_id)
            )
        })?;
        ensure_no_blocking_escalation_or_gate(db)?;
        db::recovery::write_checkpoint(
            db.conn(),
            &RuntimeCheckpointState {
                origin: RecoveryOrigin::Renewal,
                captured_at_epoch_s: now,
                session_started_at_epoch_s: active_session.started_at_epoch_s,
                summary: checkpoint_summary.clone(),
                immediate_actions: checkpoint_immediate_actions,
                key_files: checkpoint_key_files,
            },
        )
        .context("failed to persist renewal checkpoint before preparing the lease")?;

        let lease = RenewalLease {
            renewal_lease_id: mint_renewal_lease_id(),
            thread_id,
            previous_session_id,
            previous_session_mode: active_session.mode,
            continuation_token: mint_continuation_token(),
            issued_at_epoch_s: now,
            expires_at_epoch_s: now.saturating_add(DEFAULT_RENEWAL_LEASE_TTL_SECS),
            status: RenewalLeaseStatus::PendingReset,
            fulfilled_session_id: None,
            fulfilled_at_epoch_s: None,
            reset_path,
            lifeloop_adapter_id,
            lifeloop_receipt_id,
            lifecycle_evidence_json,
        };
        db::renewal::insert(db.conn(), &lease)?;
        crate::state::curl::record_renewal_prepared_on_db(
            db.conn(),
            &layout,
            profile.as_str(),
            &locality_id,
            &lease.thread_id,
            &lease.previous_session_id,
            &lease.renewal_lease_id,
            lease.lifeloop_receipt_id.as_deref(),
            now,
        )
        .context("failed to record curl renewal preparation")?;
        db::session_activity::delete(db.conn())?;
        db::work_stream_decay::delete(db.conn())?;
        db::session::delete(db.conn())?;
        prepared = Some(lease);
        Ok(())
    })?;

    let lease = prepared.context("renewal transaction committed without a lease")?;
    crate::state::thread::record_session_closed(
        &layout,
        &locality_id,
        &lease.thread_id,
        &lease.previous_session_id,
        json!({ "reason": "renewal_prepare", "renewal_lease_id": lease.renewal_lease_id }),
    )
    .context("failed to record renewal session-closed thread event")?;

    let continuation_command = format!(
        "ccd start --refresh --continuation {} --path {}",
        lease.continuation_token,
        shell_quote_path(repo_root)
    );
    let instruction =
        "Reset the runtime session, then run the continuation command before resuming work."
            .to_owned();

    Ok(RenewalPrepareReport {
        command: "session-renew-prepare",
        ok: true,
        profile: profile.to_string(),
        path: repo_root.display().to_string(),
        state_db: layout.state_db_path().display().to_string(),
        renewal: RenewalLeaseView::from_lease(&lease),
        continuation: RenewalContinuationView {
            token: lease.continuation_token,
            path: repo_root.display().to_string(),
            command: continuation_command,
        },
        checkpoint: RenewalCheckpointView {
            status: "written",
            summary: checkpoint_summary,
        },
        instruction,
    })
}

fn ensure_profile_exists(repo_root: &Path, layout: &StateLayout) -> Result<()> {
    if layout.profile_root().is_dir() {
        return Ok(());
    }
    bail!(
        "profile `{}` does not exist at {}; bootstrap it with `ccd attach --path {}` before preparing renewal",
        layout.profile(),
        layout.profile_root().display(),
        repo_root.display()
    )
}

fn ensure_no_blocking_escalation_or_gate(db: &db::StateDb) -> Result<()> {
    let blocking_escalations = db::escalation::list(db.conn())?
        .into_iter()
        .filter(|entry| matches!(entry.kind, EscalationKind::Blocking))
        .collect::<Vec<_>>();
    if !blocking_escalations.is_empty() {
        let reasons = blocking_escalations
            .iter()
            .map(|entry| format!("{}: {}", entry.id, entry.reason))
            .collect::<Vec<_>>()
            .join("; ");
        bail!("renewal prepare refused because blocking escalation state is active: {reasons}");
    }

    if let Some(gates) = db::session_gates::read(db.conn())? {
        let blocked = gates
            .gates
            .iter()
            .enumerate()
            .filter(|(_, gate)| gate.status == ExecutionGateStatus::Blocked)
            .map(|(idx, gate)| format!("#{} {}", idx + 1, gate.text))
            .collect::<Vec<_>>();
        if !blocked.is_empty() {
            bail!(
                "renewal prepare refused because blocked execution gates require operator action: {}",
                blocked.join("; ")
            );
        }
    }

    Ok(())
}

struct LifecycleEvidence {
    reset_path: RenewalResetPath,
    adapter_id: Option<String>,
    receipt_id: Option<String>,
    evidence: serde_json::Value,
}

fn resolve_lifecycle_evidence(options: &RenewalPrepareOptions) -> Result<LifecycleEvidence> {
    let receipt = options
        .lifeloop_receipt_path
        .as_ref()
        .map(|path| read_lifeloop_receipt(path))
        .transpose()?;

    let Some(adapter_id) = options.lifeloop_adapter_id.as_deref() else {
        if options.lifeloop_reset_path.is_some() {
            bail!("--reset-path requires --adapter because reset-path names Lifeloop adapter capability evidence");
        }
        return Ok(LifecycleEvidence {
            reset_path: RenewalResetPath::OperatorManual,
            adapter_id: None,
            receipt_id: receipt.as_ref().and_then(receipt_id),
            evidence: json!({
                "source": "operator_manual",
                "lifeloop_receipt": receipt,
            }),
        });
    };

    let reset_path = options
        .lifeloop_reset_path
        .unwrap_or(LifeloopResetPath::Wrapper);
    let manifest = lifeloop_manifest_value(adapter_id)?;
    require_support(
        &manifest,
        reset_path.reset_capability_path(),
        &format!("renewal.reset.{}", reset_path.as_str()),
        adapter_id,
    )?;
    require_support(
        &manifest,
        &["renewal", "continuation", "payload_delivery"],
        "renewal.continuation.payload_delivery",
        adapter_id,
    )?;
    let renewal_profiles = require_renewal_profile(&manifest, "ccd-renewal", adapter_id)?;

    Ok(LifecycleEvidence {
        reset_path: reset_path.into_lease_path(),
        adapter_id: Some(adapter_id.to_owned()),
        receipt_id: receipt.as_ref().and_then(receipt_id),
        evidence: json!({
            "source": "lifeloop_manifest",
            "adapter_id": adapter_id,
            "reset_path": reset_path.as_str(),
            "manifest_contract_version": manifest.get("contract_version").cloned(),
            "renewal_profiles": renewal_profiles,
            "lifeloop_receipt": receipt,
        }),
    })
}

fn read_lifeloop_receipt(path: &Path) -> Result<serde_json::Value> {
    let contents = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read Lifeloop receipt {}", path.display()))?;
    serde_json::from_str(&contents)
        .with_context(|| format!("failed to parse Lifeloop receipt JSON {}", path.display()))
}

fn receipt_id(value: &serde_json::Value) -> Option<String> {
    value
        .get("receipt_id")
        .and_then(serde_json::Value::as_str)
        .map(str::to_owned)
}

fn lifeloop_manifest_value(adapter_id: &str) -> Result<serde_json::Value> {
    lifeloop::manifest_registry()
        .into_iter()
        .find(|registered| registered.manifest.adapter_id == adapter_id)
        .map(|registered| serde_json::to_value(registered.manifest))
        .transpose()?
        .ok_or_else(|| {
            anyhow::anyhow!(
                "Lifeloop adapter `{}` is not registered",
                display_safe(adapter_id)
            )
        })
}

fn require_support(
    manifest: &serde_json::Value,
    path: &[&str],
    capability: &str,
    adapter_id: &str,
) -> Result<()> {
    let support = path
        .iter()
        .try_fold(manifest, |value, key| value.get(*key))
        .and_then(serde_json::Value::as_str)
        .unwrap_or("unavailable");
    if !matches!(support, "native" | "synthesized" | "manual") {
        bail!(
            "Lifeloop adapter `{}` does not prove required renewal capability `{capability}` (support=`{support}`)",
            display_safe(adapter_id)
        );
    }
    Ok(())
}

fn require_renewal_profile(
    manifest: &serde_json::Value,
    profile: &str,
    adapter_id: &str,
) -> Result<serde_json::Value> {
    let profiles = manifest
        .pointer("/renewal/profiles")
        .and_then(serde_json::Value::as_array)
        .ok_or_else(|| {
            anyhow::anyhow!(
                "Lifeloop adapter `{}` does not prove required renewal profile `{}`",
                display_safe(adapter_id),
                display_safe(profile)
            )
        })?;
    if !profiles
        .iter()
        .any(|candidate| candidate.as_str() == Some(profile))
    {
        bail!(
            "Lifeloop adapter `{}` does not prove required renewal profile `{}`",
            display_safe(adapter_id),
            display_safe(profile)
        );
    }
    Ok(serde_json::Value::Array(profiles.clone()))
}

fn shell_quote_path(path: &Path) -> String {
    let value = path.display().to_string();
    format!("'{}'", value.replace('\'', "'\\''"))
}

fn mint_renewal_lease_id() -> String {
    format!("rnl_{}", crate::ids::new_ulid())
}

fn mint_continuation_token() -> String {
    format!("cnt_{}_{}", crate::ids::new_ulid(), crate::ids::new_ulid())
}

fn run_renewal_transaction<F>(db: &db::StateDb, body: F) -> Result<()>
where
    F: FnOnce(&db::StateDb) -> Result<()>,
{
    db.conn().execute_batch("BEGIN IMMEDIATE")?;
    match body(db) {
        Ok(()) => {
            db.conn().execute_batch("COMMIT")?;
            Ok(())
        }
        Err(error) => {
            let _ = db.conn().execute_batch("ROLLBACK");
            Err(error)
        }
    }
}

impl RenewalLeaseView {
    fn from_lease(lease: &RenewalLease) -> Self {
        Self {
            renewal_lease_id: lease.renewal_lease_id.clone(),
            status: lease.status,
            thread_id: lease.thread_id.clone(),
            previous_session_id: lease.previous_session_id.clone(),
            previous_session_mode: lease.previous_session_mode,
            issued_at_epoch_s: lease.issued_at_epoch_s,
            expires_at_epoch_s: lease.expires_at_epoch_s,
            reset_path: lease.reset_path,
            lifeloop_adapter_id: lease.lifeloop_adapter_id.clone(),
            lifeloop_receipt_id: lease.lifeloop_receipt_id.clone(),
        }
    }
}

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

    #[test]
    fn require_support_rejects_partial_or_unknown_values() {
        let manifest = json!({
            "renewal": {
                "reset": { "wrapper_mediated": "partial" },
                "continuation": { "payload_delivery": "experimental" }
            }
        });

        assert!(require_support(
            &manifest,
            &["renewal", "reset", "wrapper_mediated"],
            "renewal.reset.wrapper",
            "test"
        )
        .is_err());
        assert!(require_support(
            &manifest,
            &["renewal", "continuation", "payload_delivery"],
            "renewal.continuation.payload_delivery",
            "test"
        )
        .is_err());
    }

    #[test]
    fn require_support_escapes_adapter_id_in_error_text() {
        let manifest = json!({ "renewal": { "reset": { "wrapper_mediated": "partial" } } });
        let err = require_support(
            &manifest,
            &["renewal", "reset", "wrapper_mediated"],
            "renewal.reset.wrapper",
            "bad\nadapter",
        )
        .unwrap_err()
        .to_string();

        assert!(err.contains(r"bad\nadapter"));
        assert!(!err.contains("bad\nadapter"));
    }

    #[test]
    fn require_support_accepts_positive_lifeloop_support_states() {
        for support in ["native", "synthesized", "manual"] {
            let manifest =
                json!({ "renewal": { "continuation": { "payload_delivery": support } } });
            require_support(
                &manifest,
                &["renewal", "continuation", "payload_delivery"],
                "renewal.continuation.payload_delivery",
                "test",
            )
            .unwrap();
        }
    }

    #[test]
    fn require_renewal_profile_rejects_missing_ccd_profile() {
        let manifest = json!({ "renewal": { "profiles": ["manual-renewal"] } });
        let err = require_renewal_profile(&manifest, "ccd-renewal", "codex")
            .unwrap_err()
            .to_string();

        assert!(err.contains("does not prove required renewal profile"));
    }

    #[test]
    fn require_renewal_profile_accepts_membership_without_requiring_order() {
        let manifest = json!({ "renewal": { "profiles": ["manual-renewal", "ccd-renewal"] } });
        let profiles = require_renewal_profile(&manifest, "ccd-renewal", "codex").unwrap();

        assert_eq!(profiles[1], "ccd-renewal");
    }

    #[test]
    fn shell_quote_path_handles_spaces_and_quotes() {
        assert_eq!(
            shell_quote_path(Path::new("/tmp/repo with 'quote'")),
            "'/tmp/repo with '\\''quote'\\'''"
        );
    }
}
