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

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

use crate::db;
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile::{self, ProfileName};
use crate::state::protected_write::{self, ExclusiveWriteOptions};
use crate::state::session;

const EXECUTION_GATES_SCHEMA_VERSION: u32 = 2;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionGateStatus {
    Open,
    Done,
    Blocked,
}

impl ExecutionGateStatus {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Open => "open",
            Self::Done => "done",
            Self::Blocked => "blocked",
        }
    }

    fn is_done(self) -> bool {
        matches!(self, Self::Done)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionGate {
    pub text: String,
    pub status: ExecutionGateStatus,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionGateStateFile {
    pub schema_version: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub seeded_from: Option<String>,
    #[serde(default)]
    pub revision: u64,
    #[serde(default)]
    pub gates: Vec<ExecutionGate>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateSeedSource {
    ImmediateActions,
    DefinitionOfDone,
}

impl GateSeedSource {
    fn as_seed_label(self) -> &'static str {
        match self {
            Self::ImmediateActions => "handoff:immediate_actions",
            Self::DefinitionOfDone => "handoff:definition_of_done",
        }
    }

    fn display_label(self) -> &'static str {
        match self {
            Self::ImmediateActions => "handoff immediate actions",
            Self::DefinitionOfDone => "handoff definition of done",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ExecutionGateAnchor {
    pub index: usize,
    pub text: String,
    pub status: ExecutionGateStatus,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ExecutionGatesView {
    pub path: String,
    pub status: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub seeded_from: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub revision: Option<u64>,
    pub total_count: usize,
    pub unfinished_count: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attention_anchor: Option<ExecutionGateAnchor>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub gates: Vec<ExecutionGate>,
}

#[derive(Serialize)]
pub struct SessionGatesReport {
    command: &'static str,
    ok: bool,
    profile: String,
    path: String,
    action: &'static str,
    view: ExecutionGatesView,
    #[serde(skip_serializing_if = "Option::is_none")]
    message: Option<String>,
}

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

    fn render_text(&self) {
        if let Some(message) = &self.message {
            println!("{message}");
        }
        println!("Execution gates: {} ({})", self.view.path, self.view.status);
        println!(
            "Counts: {} total, {} unfinished",
            self.view.total_count, self.view.unfinished_count
        );
        if let Some(seed) = &self.view.seeded_from {
            println!("Seeded from: {seed}");
        }
        if let Some(anchor) = &self.view.attention_anchor {
            println!(
                "Attention anchor [{} #{}/{}]: {}",
                anchor.status.as_str(),
                anchor.index,
                self.view.total_count,
                anchor.text
            );
        } else if self.view.total_count == 0 {
            println!("No execution gates are active.");
        }
    }
}

pub fn list(repo_root: &Path, explicit_profile: Option<&str>) -> Result<SessionGatesReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = required_layout(repo_root, &profile)?;
    let raw = load_for_layout(&layout)?;
    Ok(report(
        "session-state-gates-list",
        "list",
        &profile,
        &layout,
        raw,
        None,
    ))
}

pub fn replace(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    gates: Vec<String>,
    write_options: ExclusiveWriteOptions,
) -> Result<SessionGatesReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = required_layout(repo_root, &profile)?;
    let db = require_active_session_db(&layout)?;
    let gate_texts = normalize_gate_texts(gates)?;
    let state = ExecutionGateStateFile {
        schema_version: EXECUTION_GATES_SCHEMA_VERSION,
        seeded_from: None,
        revision: 0,
        gates: gate_texts
            .into_iter()
            .map(|text| ExecutionGate {
                text,
                status: ExecutionGateStatus::Open,
            })
            .collect(),
    };
    let state = persist_gate_state(&db, &layout, state, &write_options)?;
    Ok(report(
        "session-state-gates-replace",
        "replace",
        &profile,
        &layout,
        Some(state),
        Some("stored manual execution gates".to_owned()),
    ))
}

pub fn seed(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    source: GateSeedSource,
    write_options: ExclusiveWriteOptions,
) -> Result<SessionGatesReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = required_layout(repo_root, &profile)?;
    let db = require_active_session_db(&layout)?;
    let handoff = db::handoff::read(db.conn())?.ok_or_else(|| {
        anyhow::anyhow!(
            "no workspace-local handoff is available; run `ccd start --path {}` or `ccd handoff write --path {}` first",
            repo_root.display(),
            repo_root.display()
        )
    })?;

    let gate_texts = match source {
        GateSeedSource::ImmediateActions => handoff
            .immediate_actions
            .into_iter()
            .map(|item| item.text)
            .collect(),
        GateSeedSource::DefinitionOfDone => handoff
            .definition_of_done
            .into_iter()
            .map(|item| item.text)
            .collect(),
    };
    let gate_texts = normalize_gate_texts(gate_texts)?;
    let state = ExecutionGateStateFile {
        schema_version: EXECUTION_GATES_SCHEMA_VERSION,
        seeded_from: Some(source.as_seed_label().to_owned()),
        revision: 0,
        gates: gate_texts
            .into_iter()
            .map(|text| ExecutionGate {
                text,
                status: ExecutionGateStatus::Open,
            })
            .collect(),
    };
    let state = persist_gate_state(&db, &layout, state, &write_options)?;
    Ok(report(
        "session-state-gates-seed",
        "seed",
        &profile,
        &layout,
        Some(state),
        Some(format!(
            "seeded execution gates from {}",
            source.display_label()
        )),
    ))
}

pub fn set_status(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    index: usize,
    status: ExecutionGateStatus,
    write_options: ExclusiveWriteOptions,
) -> Result<SessionGatesReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = required_layout(repo_root, &profile)?;
    let db = require_active_session_db(&layout)?;
    let mut state = db::session_gates::read(db.conn())?.ok_or_else(|| {
        anyhow::anyhow!("no execution gates are active in this clone; seed or replace them first")
    })?;
    if index == 0 || index > state.gates.len() {
        bail!(
            "gate index {index} is out of range; current gate count is {}",
            state.gates.len()
        );
    }
    state.gates[index - 1].status = status;
    let state = persist_gate_state(&db, &layout, state, &write_options)?;
    Ok(report(
        "session-state-gates-set-status",
        "set_status",
        &profile,
        &layout,
        Some(state),
        Some(format!("updated gate {index} to {}", status.as_str())),
    ))
}

pub fn advance(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    write_options: ExclusiveWriteOptions,
) -> Result<SessionGatesReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = required_layout(repo_root, &profile)?;
    let db = require_active_session_db(&layout)?;
    let mut state = db::session_gates::read(db.conn())?.ok_or_else(|| {
        anyhow::anyhow!("no execution gates are active in this clone; seed or replace them first")
    })?;
    let Some((index, gate)) = state
        .gates
        .iter_mut()
        .enumerate()
        .find(|(_, gate)| !gate.status.is_done())
    else {
        return Ok(report(
            "session-state-gates-advance",
            "advance",
            &profile,
            &layout,
            Some(state),
            Some("all execution gates are already done".to_owned()),
        ));
    };
    gate.status = ExecutionGateStatus::Done;
    let state = persist_gate_state(&db, &layout, state, &write_options)?;
    Ok(report(
        "session-state-gates-advance",
        "advance",
        &profile,
        &layout,
        Some(state),
        Some(format!("marked gate {} done", index + 1)),
    ))
}

pub fn clear(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    write_options: ExclusiveWriteOptions,
) -> Result<SessionGatesReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = required_layout(repo_root, &profile)?;
    let existed = clear_for_layout(&layout, &write_options)?;
    Ok(report(
        "session-state-gates-clear",
        "clear",
        &profile,
        &layout,
        None,
        Some(if existed {
            "cleared execution gates".to_owned()
        } else {
            "no execution gates were active".to_owned()
        }),
    ))
}

pub(crate) fn load_for_layout(layout: &StateLayout) -> Result<Option<ExecutionGateStateFile>> {
    let Some(db) = try_open_state_db(layout)? else {
        return Ok(None);
    };
    db::session_gates::read(db.conn())
}

pub(crate) fn clear_for_session_boundary(layout: &StateLayout) -> Result<bool> {
    let Some(db) = try_open_state_db(layout)? else {
        return Ok(false);
    };
    clear_on_shared_db(&db)
}

/// Clear gate state against a caller-provided shared `StateDb` handle. Used
/// by `session::clear` and `session::start` to fold the gate mutation into
/// their shared lifecycle transaction. No legacy-JSON migration is needed:
/// `session_gates` has no pre-state.db predecessor.
pub(crate) fn clear_on_shared_db(shared_db: &db::StateDb) -> Result<bool> {
    let existed = db::session_gates::read(shared_db.conn())?.is_some();
    if existed {
        db::session_gates::delete(shared_db.conn())?;
    }
    Ok(existed)
}

fn persist_gate_state(
    db: &db::StateDb,
    layout: &StateLayout,
    mut state: ExecutionGateStateFile,
    write_options: &ExclusiveWriteOptions,
) -> Result<ExecutionGateStateFile> {
    let guard =
        protected_write::authorize_owner_surface_write(layout, "execution_gates", write_options)?;
    state.revision = match guard.expected_revision {
        Some(expected_revision) => {
            match db::session_gates::write_if_revision_matches(
                db.conn(),
                &state,
                expected_revision,
            )? {
                db::session_gates::ExclusiveWriteResult::Applied { revision } => revision,
                db::session_gates::ExclusiveWriteResult::RevisionConflict { current_revision } => {
                    bail!(
                        "`execution_gates` revision conflict: expected {expected_revision}, current is {current_revision}"
                    );
                }
            }
        }
        None => db::session_gates::write(db.conn(), &state)?,
    };
    Ok(state)
}

fn clear_for_layout(layout: &StateLayout, write_options: &ExclusiveWriteOptions) -> Result<bool> {
    let Some(db) = try_open_state_db(layout)? else {
        return Ok(false);
    };
    let guard =
        protected_write::authorize_owner_surface_write(layout, "execution_gates", write_options)?;
    match guard.expected_revision {
        Some(expected_revision) => {
            match db::session_gates::delete_if_revision_matches(db.conn(), expected_revision)? {
                db::session_gates::ExclusiveDeleteResult::Applied => Ok(true),
                db::session_gates::ExclusiveDeleteResult::IdempotentNoop => Ok(false),
                db::session_gates::ExclusiveDeleteResult::RevisionConflict { current_revision } => {
                    bail!(
                        "`execution_gates` revision conflict: expected {expected_revision}, current is {current_revision}"
                    );
                }
            }
        }
        None => clear_on_shared_db(&db),
    }
}

fn report(
    command: &'static str,
    action: &'static str,
    profile: &ProfileName,
    layout: &StateLayout,
    raw: Option<ExecutionGateStateFile>,
    message: Option<String>,
) -> SessionGatesReport {
    SessionGatesReport {
        command,
        ok: true,
        profile: profile.to_string(),
        path: layout.state_db_path().display().to_string(),
        action,
        view: build_view(layout, raw),
        message,
    }
}

pub(crate) fn build_view(
    layout: &StateLayout,
    raw: Option<ExecutionGateStateFile>,
) -> ExecutionGatesView {
    let path = layout.state_db_path().display().to_string();
    let Some(raw) = raw else {
        return ExecutionGatesView {
            path,
            status: "missing",
            seeded_from: None,
            revision: None,
            total_count: 0,
            unfinished_count: 0,
            attention_anchor: None,
            gates: Vec::new(),
        };
    };

    let unfinished_count = raw
        .gates
        .iter()
        .filter(|gate| !gate.status.is_done())
        .count();
    let attention_anchor = raw.gates.iter().enumerate().find_map(|(index, gate)| {
        (!gate.status.is_done()).then(|| ExecutionGateAnchor {
            index: index + 1,
            text: gate.text.clone(),
            status: gate.status,
        })
    });

    ExecutionGatesView {
        path,
        status: if raw.gates.is_empty() {
            "empty"
        } else {
            "loaded"
        },
        seeded_from: raw.seeded_from,
        revision: Some(raw.revision),
        total_count: raw.gates.len(),
        unfinished_count,
        attention_anchor,
        gates: raw.gates,
    }
}

fn normalize_gate_texts(gates: Vec<String>) -> Result<Vec<String>> {
    let gates: Vec<String> = gates
        .into_iter()
        .map(|gate| gate.trim().to_owned())
        .filter(|gate| !gate.is_empty())
        .collect();
    if gates.is_empty() {
        bail!("at least one non-empty `--gate` entry is required");
    }
    Ok(gates)
}

fn require_active_session_db(layout: &StateLayout) -> Result<db::StateDb> {
    let Some(db) = try_open_state_db(layout)? else {
        bail!("{}", active_session_error());
    };
    let Some(state) = db::session::read(db.conn())? else {
        bail!("{}", active_session_error());
    };
    let now = session::now_epoch_s()?;
    if state.session_id.is_none() {
        bail!("{}", active_session_error());
    }
    if session::is_stale(&state, now) && state.lifecycle() == session::SessionLifecycle::Interactive
    {
        bail!("session telemetry is stale; run `ccd session-state start --path .` before mutating execution gates");
    }
    Ok(db)
}

fn active_session_error() -> &'static str {
    "no active session telemetry is available; run `ccd session-state start --path .` first"
}

fn required_layout(repo_root: &Path, profile: &ProfileName) -> Result<StateLayout> {
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    ensure_profile_exists(&layout)?;
    Ok(layout)
}

fn ensure_profile_exists(layout: &StateLayout) -> Result<()> {
    let profile_root = layout.profile_root();
    if profile_root.is_dir() {
        return Ok(());
    }

    bail!(
        "profile `{}` does not exist at {}; bootstrap it before using session-state gates",
        layout.profile(),
        profile_root.display()
    )
}

fn try_open_state_db(layout: &StateLayout) -> Result<Option<db::StateDb>> {
    // session_gates has no legacy-JSON predecessor, so a bare probe on the
    // shared `state.db` is sufficient — the consolidated helper on StateDb.
    db::StateDb::try_open_for_layout(layout)
}

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

    fn test_layout(temp: &std::path::Path) -> StateLayout {
        StateLayout::new(
            temp.join(".ccd"),
            temp.join("repo/.git/ccd"),
            ProfileName::new("main").expect("profile"),
        )
    }

    #[test]
    fn build_view_uses_first_unfinished_gate_as_anchor() {
        let dir = tempfile::tempdir().unwrap();
        let layout = test_layout(dir.path());
        let view = build_view(
            &layout,
            Some(ExecutionGateStateFile {
                schema_version: 2,
                seeded_from: Some("handoff:immediate_actions".to_owned()),
                revision: 3,
                gates: vec![
                    ExecutionGate {
                        text: "Done".to_owned(),
                        status: ExecutionGateStatus::Done,
                    },
                    ExecutionGate {
                        text: "Blocked".to_owned(),
                        status: ExecutionGateStatus::Blocked,
                    },
                    ExecutionGate {
                        text: "Open".to_owned(),
                        status: ExecutionGateStatus::Open,
                    },
                ],
            }),
        );

        assert_eq!(view.status, "loaded");
        assert_eq!(view.total_count, 3);
        assert_eq!(view.unfinished_count, 2);
        let anchor = view.attention_anchor.expect("anchor");
        assert_eq!(anchor.index, 2);
        assert_eq!(anchor.status, ExecutionGateStatus::Blocked);
        assert_eq!(anchor.text, "Blocked");
    }

    #[test]
    fn normalize_gate_texts_rejects_empty_input() {
        assert!(normalize_gate_texts(vec!["  ".to_owned()]).is_err());
    }
}
