use std::sync::Arc;

use tracing::{info, warn};

use crate::{
    audit_emit,
    error::ControllerError,
    handlers,
    orchestrator::OrchestratorClient,
    persistence::Persistence,
    types::{AckResult, OpState},
};
use async_nats::jetstream::Context;
use ops_keeper::types::PendingOp;

/// On startup, scan SurrealDB for ops with state=Pending and attempt to re-apply them.
///
/// This handles the crash-recovery path: if ops-controller dies after INSERT but
/// before the orchestrator call, the row stays Pending. On restart we reconcile.
pub async fn reconcile_pending(
    persistence: Arc<Persistence>,
    orc: Arc<OrchestratorClient>,
    js: Arc<Context>,
    workspace: &str,
) {
    let pending = match persistence.list_pending().await {
        Ok(rows) => rows,
        Err(e) => {
            warn!(error = %e, "recovery: failed to list pending ops — skipping reconciliation");
            return;
        }
    };

    if pending.is_empty() {
        info!("recovery: no pending ops found");
        return;
    }

    info!(count = pending.len(), "recovery: reconciling pending ops");

    for row in &pending {
        let op = PendingOp {
            op_type: row.op_type.clone(),
            target: row.target.clone(),
            sub: "recovery".to_string(),
            expected_state_version: "unknown".to_string(),
            image: String::new(),
            params: serde_json::Value::Null,
        };

        match handlers::dispatch(&op, &orc).await {
            Ok(()) => {
                info!(jti = %row.jti, "recovery: re-applied pending op");
                if let Err(e) = persistence.mark_applied(&row.jti).await {
                    warn!(jti = %row.jti, error = %e, "recovery: failed to mark as applied");
                }
                if let Err(e) = audit_emit::emit_audit(&js, workspace, &op, &row.jti, "applied-on-recovery").await {
                    warn!(jti = %row.jti, error = %e, "recovery: audit emit failed");
                }
                if let Err(e) = audit_emit::emit_ack(&js, workspace, &op, &row.jti, AckResult::Applied, None).await {
                    warn!(jti = %row.jti, error = %e, "recovery: ack emit failed");
                }
                let _ = persistence.increment_version().await;
            }
            Err(e) => {
                warn!(jti = %row.jti, error = %e, "recovery: op re-apply failed — marking rejected");
                let reason = e.to_string();
                if let Err(db_err) = persistence.mark_rejected(&row.jti, &reason).await {
                    warn!(jti = %row.jti, error = %db_err, "recovery: failed to mark as rejected");
                }
                if let Err(e) = audit_emit::emit_ack(&js, workspace, &op, &row.jti, AckResult::Rejected, Some(reason)).await {
                    warn!(jti = %row.jti, error = %e, "recovery: ack emit failed for rejection");
                }
            }
        }
    }
}

/// Check if a jti has already been applied, returning true if we should skip this message.
/// If the row exists with state=Applied → skip (idempotent).
/// If the row exists with state=Pending → was mid-apply crash, reconcile handles it.
pub async fn check_idempotency(
    persistence: &Persistence,
    jti: &str,
) -> Result<bool, ControllerError> {
    if let Some(row) = persistence.get(jti).await? {
        match row.state {
            OpState::Applied => return Ok(true),
            OpState::Rejected => return Ok(true),
            OpState::Pending => {
                // In-flight crash: will be picked up by recovery on next restart.
                // For now, leave it and let the message be NACKed for retry.
                return Err(ControllerError::Db(format!(
                    "jti {jti} is in state=Pending from a previous attempt — recovery will handle it"
                )));
            }
        }
    }
    Ok(false)
}
