//! Governance-native obligation graph for cross-session coordination.
//!
//! Obligations are the formal, dependency-aware units of work in Decapod.
//! Unlike TODOs, obligations are proof-gated and strictly integrated with
//! governance and promotion cycles.
//!
//! KEY PRINCIPLE: Completion is DERIVED, never asserted.
//! - Status is computed from: dependencies satisfied, proofs verified, state_commit present
//! - No user-settable status field - status is always derived

use crate::core::broker::DbBroker;
use crate::core::error;
use crate::core::schemas;
use crate::core::store::Store;
use clap::{Parser, Subcommand, ValueEnum};
use rusqlite::{OptionalExtension, params};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum ObligationStatus {
    Open,
    Met,
    Failed,
}

impl ObligationStatus {
    pub fn as_str(&self) -> &str {
        match self {
            ObligationStatus::Open => "open",
            ObligationStatus::Met => "met",
            ObligationStatus::Failed => "failed",
        }
    }

    pub fn from_status_str(s: &str) -> Self {
        match s {
            "met" => ObligationStatus::Met,
            "failed" => ObligationStatus::Failed,
            _ => ObligationStatus::Open,
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ObligationNode {
    pub id: String,
    pub intent_ref: String,
    pub risk_tier: String,
    pub required_proofs: Vec<String>,
    pub state_commit_root: Option<String>,
    pub status: ObligationStatus,
    pub created_at: String,
    pub updated_at: String,
    pub metadata: Option<serde_json::Value>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ObligationValidationResult {
    pub obligation_id: String,
    pub derived_status: ObligationStatus,
    pub dependencies_satisfied: bool,
    pub proofs_satisfied: bool,
    pub commit_present: bool,
    pub validation_errors: Vec<String>,
    pub timestamp: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GraphValidationResult {
    pub is_valid: bool,
    pub has_cycles: bool,
    pub cycle_errors: Vec<String>,
    pub unsatisfied_obligations: Vec<String>,
    pub missing_proofs: Vec<String>,
    pub missing_commits: Vec<String>,
    pub total_nodes: usize,
    pub total_edges: usize,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ObligationEdge {
    pub edge_id: String,
    pub from_id: String,
    pub to_id: String,
    pub kind: String,
    pub created_at: String,
}

pub fn obligation_db_path(root: &Path) -> PathBuf {
    root.join(schemas::GOVERNANCE_DB_NAME)
}

pub fn initialize_obligation_db(root: &Path) -> Result<(), error::DecapodError> {
    let broker = DbBroker::new(root);
    let db_path = obligation_db_path(root);

    broker.with_conn(&db_path, "decapod", None, "obligation.init", |conn| {
        conn.execute(schemas::GOVERNANCE_DB_SCHEMA_OBLIGATIONS, [])?;
        conn.execute(schemas::GOVERNANCE_DB_SCHEMA_OBLIGATION_EDGES, [])?;
        Ok(())
    })
}

#[derive(Parser, Debug)]
#[clap(name = "obligation", about = "Manage the Obligation Engine")]
pub struct ObligationCli {
    #[clap(subcommand)]
    pub command: ObligationCommand,
}

#[derive(Subcommand, Debug)]
pub enum ObligationCommand {
    /// Add a new obligation node.
    Add {
        #[clap(long)]
        intent: String,
        #[clap(long, default_value = "medium")]
        risk: String,
        #[clap(long, default_value = "")]
        depends_on: String, // comma-separated IDs
        #[clap(long, default_value = "")]
        proofs: String, // comma-separated claim IDs or labels
    },
    /// List all obligations.
    List,
    /// Get an obligation by ID.
    Get {
        #[clap(long)]
        id: String,
    },
    /// Compute and update the status of an obligation (DERIVED, never asserted).
    Verify {
        #[clap(long)]
        id: String,
    },
    /// Mark an obligation as complete by providing a state commit root.
    Complete {
        #[clap(long)]
        id: String,
        #[clap(long)]
        commit: String,
    },
    /// Validate the entire obligation graph (cycles, dependencies, proofs, commits).
    ValidateGraph,
}

pub fn run_obligation_cli(store: &Store, cli: ObligationCli) -> Result<(), error::DecapodError> {
    initialize_obligation_db(&store.root)?;
    match cli.command {
        ObligationCommand::Add {
            intent,
            risk,
            depends_on,
            proofs,
        } => {
            let id = add_obligation(store, &intent, &risk, &depends_on, &proofs)?;
            println!("Obligation added: {}", id);
        }
        ObligationCommand::List => {
            let obligations = list_obligations(store)?;
            println!("{}", serde_json::to_string_pretty(&obligations).unwrap());
        }
        ObligationCommand::Get { id } => {
            let obligation = get_obligation(store, &id)?;
            println!("{}", serde_json::to_string_pretty(&obligation).unwrap());
        }
        ObligationCommand::Verify { id } => {
            let result = derive_obligation_status(store, &id)?;
            println!("{}", serde_json::to_string_pretty(&result).unwrap());
        }
        ObligationCommand::ValidateGraph => {
            let result = validate_obligation_graph(store)?;
            println!("{}", serde_json::to_string_pretty(&result).unwrap());
        }
        ObligationCommand::Complete { id, commit } => {
            complete_obligation(store, &id, &commit)?;
            let (status, reason) = verify_obligation(store, &id)?;
            println!("Obligation {} updated with commit {}.", id, commit);
            println!("Status: {:?}\nReason: {}", status, reason);
        }
    }
    Ok(())
}

pub fn add_obligation(
    store: &Store,
    intent: &str,
    risk: &str,
    depends_on: &str,
    proofs: &str,
) -> Result<String, error::DecapodError> {
    let broker = DbBroker::new(&store.root);
    let db_path = obligation_db_path(&store.root);
    let id = crate::core::ulid::new_ulid();
    let now = crate::core::time::now_epoch_z();

    let depends_on_ids: Vec<String> = depends_on
        .split(',')
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .collect();

    let proof_list: Vec<String> = proofs
        .split(',')
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .collect();

    let proof_json = serde_json::to_string(&proof_list).unwrap();

    broker.with_conn(&db_path, "decapod", None, "obligation.add", |conn| {
        // Check for cycles before adding edges
        for dep_id in &depends_on_ids {
            if detect_cycle(conn, dep_id, &id)? {
                return Err(error::DecapodError::ValidationError(format!(
                    "Circular dependency detected: {} -> {}",
                    id, dep_id
                )));
            }
        }

        conn.execute(
            "INSERT INTO obligations (id, intent_ref, risk_tier, required_proofs, status, created_at, updated_at)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
            params![id, intent, risk, proof_json, ObligationStatus::Open.as_str(), now, now],
        )?;

        for dep_id in depends_on_ids {
            let edge_id = crate::core::ulid::new_ulid();
            conn.execute(
                "INSERT INTO obligation_edges (edge_id, from_id, to_id, kind, created_at)
                 VALUES (?1, ?2, ?3, ?4, ?5)",
                params![edge_id, id, dep_id, "depends_on", now],
            )?;
        }

        Ok(())
    })?;

    Ok(id)
}

pub fn list_obligations(store: &Store) -> Result<Vec<ObligationNode>, error::DecapodError> {
    let broker = DbBroker::new(&store.root);
    let db_path = obligation_db_path(&store.root);

    broker.with_conn(&db_path, "decapod", None, "obligation.list", |conn| {
        let mut stmt = conn.prepare("SELECT id, intent_ref, risk_tier, required_proofs, state_commit_root, status, created_at, updated_at, metadata FROM obligations")?;
        let rows = stmt.query_map([], |row| {
            let proofs_json: String = row.get(3)?;
            let proofs: Vec<String> = serde_json::from_str(&proofs_json).unwrap_or_default();
            let metadata_json: Option<String> = row.get(8)?;
            let metadata: Option<serde_json::Value> = metadata_json.and_then(|s| serde_json::from_str(&s).ok());

            Ok(ObligationNode {
                id: row.get(0)?,
                intent_ref: row.get(1)?,
                risk_tier: row.get(2)?,
                required_proofs: proofs,
                state_commit_root: row.get(4)?,
                status: ObligationStatus::from_status_str(&row.get::<_, String>(5)?),
                created_at: row.get(6)?,
                updated_at: row.get(7)?,
                metadata,
            })
        })?;

        let mut results = Vec::new();
        for row in rows {
            results.push(row?);
        }
        Ok(results)
    })
}

pub fn get_obligation(store: &Store, id: &str) -> Result<ObligationNode, error::DecapodError> {
    let broker = DbBroker::new(&store.root);
    let db_path = obligation_db_path(&store.root);

    broker.with_conn(&db_path, "decapod", None, "obligation.get", |conn| {
        conn.query_row(
            "SELECT id, intent_ref, risk_tier, required_proofs, state_commit_root, status, created_at, updated_at, metadata FROM obligations WHERE id = ?1",
            params![id],
            |row| {
                let proofs_json: String = row.get(3)?;
                let proofs: Vec<String> = serde_json::from_str(&proofs_json).unwrap_or_default();
                let metadata_json: Option<String> = row.get(8)?;
                let metadata: Option<serde_json::Value> = metadata_json.and_then(|s| serde_json::from_str(&s).ok());

                Ok(ObligationNode {
                    id: row.get(0)?,
                    intent_ref: row.get(1)?,
                    risk_tier: row.get(2)?,
                    required_proofs: proofs,
                    state_commit_root: row.get(4)?,
                    status: ObligationStatus::from_status_str(&row.get::<_, String>(5)?),
                    created_at: row.get(6)?,
                    updated_at: row.get(7)?,
                    metadata,
                })
            },
        ).map_err(error::DecapodError::RusqliteError)
    })
}

pub fn detect_cycle(
    conn: &rusqlite::Connection,
    from_id: &str,
    to_id: &str,
) -> Result<bool, error::DecapodError> {
    if from_id == to_id {
        return Ok(true);
    }

    let mut stmt = conn.prepare("SELECT to_id FROM obligation_edges WHERE from_id = ?1")?;
    let mut visited = std::collections::HashSet::new();
    let mut stack = vec![from_id.to_string()];

    while let Some(current) = stack.pop() {
        if current == to_id {
            return Ok(true);
        }
        if visited.contains(&current) {
            continue;
        }
        visited.insert(current.clone());

        let rows = stmt.query_map(params![current], |row| row.get::<_, String>(0))?;
        for row in rows {
            stack.push(row?);
        }
    }

    Ok(false)
}

pub fn verify_obligation(
    store: &Store,
    id: &str,
) -> Result<(ObligationStatus, String), error::DecapodError> {
    let result = derive_obligation_status(store, id)?;
    let reason = if result.validation_errors.is_empty() {
        "All conditions satisfied".to_string()
    } else {
        result.validation_errors.join("; ")
    };
    Ok((result.derived_status, reason))
}

pub fn derive_obligation_status(
    store: &Store,
    id: &str,
) -> Result<ObligationValidationResult, error::DecapodError> {
    let obligation = get_obligation(store, id)?;
    let mut validation_errors = Vec::new();

    let dependencies = get_dependencies(store, id)?;
    let dependencies_satisfied = dependencies
        .iter()
        .all(|dep| dep.status == ObligationStatus::Met);

    if !dependencies_satisfied {
        let unsatisfied: Vec<String> = dependencies
            .iter()
            .filter(|d| d.status != ObligationStatus::Met)
            .map(|d| d.id.clone())
            .collect();
        validation_errors.push(format!("Dependencies not met: {:?}", unsatisfied));
    }

    let mut proofs_satisfied = true;
    for proof_label in &obligation.required_proofs {
        if !check_proof_satisfied(store, proof_label)? {
            proofs_satisfied = false;
            validation_errors.push(format!("Proof not satisfied: {}", proof_label));
        }
    }

    let commit_present = obligation.state_commit_root.is_some();
    if !commit_present {
        validation_errors.push("STATE_COMMIT root missing".to_string());
    }

    let derived_status = if dependencies_satisfied && proofs_satisfied && commit_present {
        ObligationStatus::Met
    } else {
        ObligationStatus::Open
    };

    Ok(ObligationValidationResult {
        obligation_id: id.to_string(),
        derived_status,
        dependencies_satisfied,
        proofs_satisfied,
        commit_present,
        validation_errors,
        timestamp: crate::core::time::now_epoch_z(),
    })
}

pub fn validate_obligation_graph(
    store: &Store,
) -> Result<GraphValidationResult, error::DecapodError> {
    let obligations = list_obligations(store)?;
    let mut cycle_errors = Vec::new();
    let mut unsatisfied_obligations = Vec::new();
    let mut missing_proofs = Vec::new();
    let mut missing_commits = Vec::new();

    let mut edge_count = 0;
    for obligation in &obligations {
        let deps = get_dependencies(store, &obligation.id)?;
        edge_count += deps.len();

        let validation = derive_obligation_status(store, &obligation.id)?;

        if validation.derived_status != ObligationStatus::Met {
            unsatisfied_obligations.push(obligation.id.clone());
        }

        if !validation.proofs_satisfied {
            missing_proofs.push(obligation.id.clone());
        }

        if !validation.commit_present && !obligation.required_proofs.is_empty() {
            missing_commits.push(obligation.id.clone());
        }

        for dep in deps {
            if detect_cycle_in_path(&obligations, store, &dep.id, &obligation.id)? {
                cycle_errors.push(format!("Cycle: {} depends on {}", obligation.id, dep.id));
            }
        }
    }

    Ok(GraphValidationResult {
        is_valid: cycle_errors.is_empty() && unsatisfied_obligations.is_empty(),
        has_cycles: !cycle_errors.is_empty(),
        cycle_errors,
        unsatisfied_obligations,
        missing_proofs,
        missing_commits,
        total_nodes: obligations.len(),
        total_edges: edge_count,
    })
}

fn detect_cycle_in_path(
    _obligations: &[ObligationNode],
    store: &Store,
    start_id: &str,
    target_id: &str,
) -> Result<bool, error::DecapodError> {
    let mut visited = std::collections::HashSet::new();
    let mut stack = vec![start_id.to_string()];

    while let Some(current) = stack.pop() {
        if current == target_id {
            return Ok(true);
        }
        if visited.contains(&current) {
            continue;
        }
        visited.insert(current.clone());

        let deps = get_dependencies(store, &current)?;
        for dep in deps {
            stack.push(dep.id);
        }
    }

    Ok(false)
}

pub fn get_dependencies(
    store: &Store,
    id: &str,
) -> Result<Vec<ObligationNode>, error::DecapodError> {
    let broker = DbBroker::new(&store.root);
    let db_path = obligation_db_path(&store.root);

    broker.with_conn(&db_path, "decapod", None, "obligation.get_deps", |conn| {
        let mut stmt = conn.prepare(
            "SELECT o.id, o.intent_ref, o.risk_tier, o.required_proofs, o.state_commit_root, o.status, o.created_at, o.updated_at, o.metadata 
             FROM obligations o
             JOIN obligation_edges e ON o.id = e.to_id
             WHERE e.from_id = ?1"
        )?;
        let rows = stmt.query_map(params![id], |row| {
            let proofs_json: String = row.get(3)?;
            let proofs: Vec<String> = serde_json::from_str(&proofs_json).unwrap_or_default();
            let metadata_json: Option<String> = row.get(8)?;
            let metadata: Option<serde_json::Value> = metadata_json.and_then(|s| serde_json::from_str(&s).ok());

            Ok(ObligationNode {
                id: row.get(0)?,
                intent_ref: row.get(1)?,
                risk_tier: row.get(2)?,
                required_proofs: proofs,
                state_commit_root: row.get(4)?,
                status: ObligationStatus::from_status_str(&row.get::<_, String>(5)?),
                created_at: row.get(6)?,
                updated_at: row.get(7)?,
                metadata,
            })
        })?;

        let mut results = Vec::new();
        for row in rows {
            results.push(row?);
        }
        Ok(results)
    })
}

fn check_proof_satisfied(store: &Store, proof_label: &str) -> Result<bool, error::DecapodError> {
    let broker = DbBroker::new(&store.root);
    let health_db = store.root.join(schemas::GOVERNANCE_DB_NAME);

    broker.with_conn(
        &health_db,
        "decapod",
        None,
        "obligation.check_proof",
        |conn| {
            let status: Option<String> = conn
                .query_row(
                    "SELECT computed_state FROM health_cache WHERE claim_id = ?1",
                    params![proof_label],
                    |row| row.get(0),
                )
                .optional()?;

            Ok(status == Some("VERIFIED".to_string()))
        },
    )
}

pub fn complete_obligation(
    store: &Store,
    id: &str,
    commit: &str,
) -> Result<(), error::DecapodError> {
    let broker = DbBroker::new(&store.root);
    let db_path = obligation_db_path(&store.root);
    let now = crate::core::time::now_epoch_z();

    broker.with_conn(&db_path, "decapod", None, "obligation.complete", |conn| {
        conn.execute(
            "UPDATE obligations SET state_commit_root = ?1, updated_at = ?2 WHERE id = ?3",
            params![commit, now, id],
        )?;
        Ok(())
    })
}
