use std::collections::BTreeSet;
use std::time::Duration;

use color_eyre::eyre::eyre;
use models::ImageId;
use patch_db::HasModel;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use tracing::instrument;

use self::docker::DockerProcedure;
use crate::context::RpcContext;
use crate::prelude::*;
use crate::s9pk::manifest::PackageId;
use crate::util::Version;
use crate::volume::Volumes;
use crate::{Error, ErrorKind};

pub mod docker;
#[cfg(feature = "js-engine")]
pub mod js_scripts;
pub use models::ProcedureName;

#[derive(Clone, Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
#[serde(tag = "type")]
#[model = "Model<Self>"]
pub enum PackageProcedure {
    Docker(DockerProcedure),

    #[cfg(feature = "js-engine")]
    Script(js_scripts::JsProcedure),
}

impl PackageProcedure {
    pub fn is_script(&self) -> bool {
        match self {
            #[cfg(feature = "js-engine")]
            Self::Script(_) => true,
            _ => false,
        }
    }
    #[instrument(skip_all)]
    pub fn validate(
        &self,
        eos_version: &Version,
        volumes: &Volumes,
        image_ids: &BTreeSet<ImageId>,
        expected_io: bool,
    ) -> Result<(), color_eyre::eyre::Report> {
        match self {
            PackageProcedure::Docker(action) => {
                action.validate(eos_version, volumes, image_ids, expected_io)
            }
            #[cfg(feature = "js-engine")]
            PackageProcedure::Script(action) => action.validate(volumes),
        }
    }

    #[instrument(skip_all)]
    pub async fn execute<I: Serialize, O: DeserializeOwned + 'static>(
        &self,
        ctx: &RpcContext,
        pkg_id: &PackageId,
        pkg_version: &Version,
        name: ProcedureName,
        volumes: &Volumes,
        input: Option<I>,
        timeout: Option<Duration>,
    ) -> Result<Result<O, (i32, String)>, Error> {
        tracing::trace!("Procedure execute {} {} - {:?}", self, pkg_id, name);
        match self {
            PackageProcedure::Docker(procedure) if procedure.inject == true => {
                procedure
                    .inject(ctx, pkg_id, pkg_version, name, volumes, input, timeout)
                    .await
            }
            PackageProcedure::Docker(procedure) => {
                procedure
                    .execute(ctx, pkg_id, pkg_version, name, volumes, input, timeout)
                    .await
            }
            #[cfg(feature = "js-engine")]
            PackageProcedure::Script(procedure) => {
                let man = ctx
                    .managers
                    .get(&(pkg_id.clone(), pkg_version.clone()))
                    .await
                    .ok_or_else(|| {
                        Error::new(
                            eyre!("No manager found for {}", pkg_id),
                            ErrorKind::NotFound,
                        )
                    })?;
                let rpc_client = man.rpc_client();
                let gid = if matches!(name, ProcedureName::Main) {
                    man.gid.new_main_gid()
                } else {
                    man.gid.new_gid()
                };

                procedure
                    .execute(
                        &ctx.datadir,
                        pkg_id,
                        pkg_version,
                        name,
                        volumes,
                        input,
                        timeout,
                        gid,
                        rpc_client,
                    )
                    .await
            }
        }
    }

    #[instrument(skip_all)]
    pub async fn sandboxed<I: Serialize, O: DeserializeOwned>(
        &self,
        ctx: &RpcContext,
        pkg_id: &PackageId,
        pkg_version: &Version,
        volumes: &Volumes,
        input: Option<I>,
        timeout: Option<Duration>,
        name: ProcedureName,
    ) -> Result<Result<O, (i32, String)>, Error> {
        tracing::trace!("Procedure sandboxed {} {} - {:?}", self, pkg_id, name);
        match self {
            PackageProcedure::Docker(procedure) => {
                procedure
                    .sandboxed(ctx, pkg_id, pkg_version, volumes, input, timeout)
                    .await
            }
            #[cfg(feature = "js-engine")]
            PackageProcedure::Script(procedure) => {
                procedure
                    .sandboxed(
                        &ctx.datadir,
                        pkg_id,
                        pkg_version,
                        volumes,
                        input,
                        timeout,
                        name,
                    )
                    .await
            }
        }
    }
}

impl std::fmt::Display for PackageProcedure {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PackageProcedure::Docker(_) => write!(f, "Docker")?,
            #[cfg(feature = "js-engine")]
            PackageProcedure::Script(_) => write!(f, "JS")?,
        }
        Ok(())
    }
}

// TODO: make this not allocate
#[derive(Debug)]
pub struct NoOutput;
impl<'de> Deserialize<'de> for NoOutput {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let _ = Value::deserialize(deserializer);
        Ok(NoOutput)
    }
}

#[test]
fn test_deser_no_output() {
    serde_json::from_str::<NoOutput>("").unwrap();
    serde_json::from_str::<Result<NoOutput, NoOutput>>("{\"Ok\": null}")
        .unwrap()
        .unwrap();
}
