pub mod error;
pub mod model;

use crate::error::FaultlineComputeError;
use crate::model::{BackendCapability, ComputeBackend, ComputeJob, ComputeStatus, JobResult};

pub trait ExecutionBackend {
    fn backend(&self) -> ComputeBackend;
    fn execute(&self, job: &ComputeJob) -> Result<JobResult, FaultlineComputeError>;
}

#[derive(Debug, Default)]
pub struct CpuBackend;

impl ExecutionBackend for CpuBackend {
    fn backend(&self) -> ComputeBackend {
        ComputeBackend::Cpu
    }

    fn execute(&self, job: &ComputeJob) -> Result<JobResult, FaultlineComputeError> {
        if job.operation.trim().is_empty() {
            return Err(FaultlineComputeError::InvalidJob(
                "operation must not be empty",
            ));
        }

        Ok(JobResult {
            job_id: job.job_id,
            backend: ComputeBackend::Cpu,
            status: ComputeStatus::Completed,
            output: serde_json::json!({
                "dataset_id": job.dataset_id,
                "operation": job.operation,
                "note": "cpu backend stub executed",
            }),
            metrics: std::collections::BTreeMap::from([(String::from("elapsed_ms"), 0.1)]),
            finished_at: Some(chrono::Utc::now()),
        })
    }
}

#[derive(Debug, Default)]
pub struct BackendRegistry {
    capabilities: Vec<BackendCapability>,
}

impl BackendRegistry {
    pub fn new() -> Self {
        Self {
            capabilities: Vec::new(),
        }
    }

    pub fn register(&mut self, capability: BackendCapability) {
        self.capabilities
            .retain(|existing| existing.backend != capability.backend);
        self.capabilities.push(capability);
    }

    pub fn list_available(&self) -> Vec<&BackendCapability> {
        self.capabilities
            .iter()
            .filter(|cap| cap.available)
            .collect()
    }

    pub fn ensure_available(&self, backend: &ComputeBackend) -> Result<(), FaultlineComputeError> {
        let available = self
            .capabilities
            .iter()
            .any(|cap| &cap.backend == backend && cap.available);
        if available {
            Ok(())
        } else {
            Err(FaultlineComputeError::BackendUnavailable(backend.clone()))
        }
    }
}

#[allow(unused_imports)]
pub use error::*;
#[allow(unused_imports)]
pub use model::*;

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

    #[test]
    fn registry_reports_available_backends() {
        let mut registry = BackendRegistry::new();
        registry.register(BackendCapability {
            backend: ComputeBackend::Cpu,
            available: true,
            supports_parallelism: true,
            max_batch_size: Some(1024),
        });
        registry.register(BackendCapability {
            backend: ComputeBackend::GpuExperimental,
            available: false,
            supports_parallelism: true,
            max_batch_size: Some(65535),
        });

        let available = registry.list_available();
        assert_eq!(available.len(), 1);
        assert_eq!(available[0].backend, ComputeBackend::Cpu);
    }

    #[test]
    fn cpu_backend_executes_stub_job() {
        let backend = CpuBackend;
        let job = ComputeJob::new("roads", ComputeBackend::Cpu, "buffer");
        let result = backend.execute(&job).expect("execute cpu job");

        assert_eq!(result.status, ComputeStatus::Completed);
        assert_eq!(result.backend, ComputeBackend::Cpu);
        assert_eq!(result.output["operation"], "buffer");
    }
}
