use crate::{HostRequirementsRef, LashlangExecutionCallSite, ModuleRef, ProcessRef};

use super::{ExecutionScratch, ProfileReport, ProjectedBindings, Record, RuntimeFailure, Value};
use crate::LashlangExecutionObservation;
use std::future::Future;
use std::sync::Mutex;
use thiserror::Error;

#[derive(Clone, Debug)]
pub enum AbilityOp {
    ResourceOperation(ResourceOperation),
    ResourceOperationBatch(ResourceOperationBatch),
    Await(Value),
    Cancel(Value),
    Print(Value),
    Submit(Value),
    Finish(Value),
    Fail(Value),
    StartProcess(Box<ProcessStart>),
    ProcessEvent(ProcessEvent),
    Sleep(Sleep),
    WaitSignal { name: String },
    SignalRun(ProcessSignal),
}

#[derive(Clone, Debug)]
pub enum AbilityResult {
    Value(Value),
    ResourceOperationBatch(ResourceOperationBatchResult),
    Unit,
}

impl AbilityResult {
    pub fn into_value(self, op: &'static str) -> Result<Value, ExecutionHostError> {
        match self {
            Self::Value(value) => Ok(value),
            Self::ResourceOperationBatch(_) => Err(ExecutionHostError::new(format!(
                "{op} returned a resource operation batch result"
            ))),
            Self::Unit => Err(ExecutionHostError::new(format!("{op} returned no value"))),
        }
    }
}

#[derive(Clone, Debug)]
pub struct ProcessStart {
    pub module_ref: ModuleRef,
    pub process_ref: ProcessRef,
    pub host_requirements_ref: HostRequirementsRef,
    pub start_site: LashlangExecutionCallSite,
    pub process_name: String,
    pub args: Record,
}

#[derive(Clone, Debug)]
pub struct ResourceOperation {
    pub receiver: Value,
    pub operation: String,
    pub args: Vec<Value>,
    pub call_site: Option<crate::LashlangExecutionCallSite>,
}

#[derive(Clone, Debug)]
pub struct ResourceOperationBatch {
    pub operations: Vec<ResourceOperation>,
}

#[derive(Clone, Debug)]
pub struct ResourceOperationBatchResult {
    pub results: Vec<ResourceOperationResult>,
}

#[derive(Clone, Debug)]
pub enum ResourceOperationResult {
    Value(Value),
    Error(ExecutionHostError),
}

impl ResourceOperationResult {
    pub fn from_result(result: Result<Value, ExecutionHostError>) -> Self {
        match result {
            Ok(value) => Self::Value(value),
            Err(error) => Self::Error(error),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProcessEventKind {
    Yield,
    Wake,
}

#[derive(Clone, Debug)]
pub struct ProcessEvent {
    pub kind: ProcessEventKind,
    pub value: Value,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SleepKind {
    For,
    Until,
}

#[derive(Clone, Debug)]
pub struct Sleep {
    pub kind: SleepKind,
    pub value: Value,
}

#[derive(Clone, Debug)]
pub struct ProcessSignal {
    pub run: Value,
    pub name: String,
    pub payload: Value,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ExecutionMode {
    #[default]
    Foreground,
    Process,
}

pub trait ExecutionHost: Sync {
    fn perform(
        &self,
        op: AbilityOp,
    ) -> impl Future<Output = Result<AbilityResult, ExecutionHostError>> + Send;

    fn yield_now(&self) -> impl Future<Output = ()> + Send {
        async {}
    }

    fn execution_mode(&self) -> ExecutionMode {
        ExecutionMode::Foreground
    }

    fn projected_bindings(&self) -> ProjectedBindings {
        ProjectedBindings::default()
    }

    fn trace_runtime_errors(&self) -> bool {
        false
    }

    fn profile_execution(&self) -> bool {
        false
    }

    fn take_scratch(&self) -> Option<ExecutionScratch> {
        None
    }

    fn store_scratch(&self, _scratch: ExecutionScratch) {}

    fn observe_runtime_failure(&self, _failure: RuntimeFailure) {}

    fn observe_profile(&self, _profile: ProfileReport) {}

    fn observe_lashlang_execution(&self, _observation: LashlangExecutionObservation) {}
}

pub struct ExecutionEnvironment<'host, H: ExecutionHost> {
    host: &'host H,
    mode: ExecutionMode,
    projected: ProjectedBindings,
    scratch: Mutex<Option<ExecutionScratch>>,
    trace_runtime_errors: bool,
    profile_execution: bool,
    runtime_failure: Mutex<Option<RuntimeFailure>>,
    profile: Mutex<Option<ProfileReport>>,
}

impl<'host, H: ExecutionHost> ExecutionEnvironment<'host, H> {
    pub fn new(host: &'host H) -> Self {
        Self {
            host,
            mode: host.execution_mode(),
            projected: host.projected_bindings(),
            scratch: Mutex::new(host.take_scratch()),
            trace_runtime_errors: host.trace_runtime_errors(),
            profile_execution: host.profile_execution(),
            runtime_failure: Mutex::new(None),
            profile: Mutex::new(None),
        }
    }

    pub fn with_mode(mut self, mode: ExecutionMode) -> Self {
        self.mode = mode;
        self
    }

    pub fn process(self) -> Self {
        self.with_mode(ExecutionMode::Process)
    }

    pub fn foreground(self) -> Self {
        self.with_mode(ExecutionMode::Foreground)
    }

    pub fn with_projected_bindings(mut self, projected: ProjectedBindings) -> Self {
        self.projected = projected;
        self
    }

    pub fn with_scratch(mut self, scratch: ExecutionScratch) -> Self {
        self.scratch = Mutex::new(Some(scratch));
        self
    }

    pub fn traced(mut self) -> Self {
        self.trace_runtime_errors = true;
        self
    }

    pub fn profiled(mut self) -> Self {
        self.profile_execution = true;
        self
    }

    pub fn take_runtime_failure(&self) -> Option<RuntimeFailure> {
        self.runtime_failure.lock().ok()?.take()
    }

    pub fn take_profile(&self) -> Option<ProfileReport> {
        self.profile.lock().ok()?.take()
    }

    pub fn take_recycled_scratch(&self) -> Option<ExecutionScratch> {
        self.scratch.lock().ok()?.take()
    }
}

impl<H: ExecutionHost> ExecutionHost for ExecutionEnvironment<'_, H> {
    async fn perform(&self, op: AbilityOp) -> Result<AbilityResult, ExecutionHostError> {
        self.host.perform(op).await
    }

    async fn yield_now(&self) {
        self.host.yield_now().await;
    }

    fn execution_mode(&self) -> ExecutionMode {
        self.mode
    }

    fn projected_bindings(&self) -> ProjectedBindings {
        self.projected.clone()
    }

    fn trace_runtime_errors(&self) -> bool {
        self.trace_runtime_errors
    }

    fn profile_execution(&self) -> bool {
        self.profile_execution
    }

    fn take_scratch(&self) -> Option<ExecutionScratch> {
        self.scratch.lock().ok()?.take()
    }

    fn store_scratch(&self, scratch: ExecutionScratch) {
        if let Ok(mut guard) = self.scratch.lock() {
            *guard = Some(scratch);
        }
    }

    fn observe_runtime_failure(&self, failure: RuntimeFailure) {
        self.host.observe_runtime_failure(failure.clone());
        if let Ok(mut guard) = self.runtime_failure.lock() {
            *guard = Some(failure);
        }
    }

    fn observe_profile(&self, profile: ProfileReport) {
        self.host.observe_profile(profile.clone());
        if let Ok(mut guard) = self.profile.lock() {
            *guard = Some(profile);
        }
    }

    fn observe_lashlang_execution(&self, observation: LashlangExecutionObservation) {
        self.host.observe_lashlang_execution(observation);
    }
}

#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("{message}")]
pub struct ExecutionHostError {
    message: String,
}

impl ExecutionHostError {
    pub fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}
