use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};

pub use helpers::script_dir;
pub use models::VolumeId;
use serde::{Deserialize, Serialize};
use tracing::instrument;

use crate::context::RpcContext;
use crate::net::interface::{InterfaceId, Interfaces};
use crate::net::PACKAGE_CERT_PATH;
use crate::prelude::*;
use crate::s9pk::manifest::PackageId;
use crate::util::Version;
use crate::{Error, ResultExt};

pub const PKG_VOLUME_DIR: &str = "package-data/volumes";
pub const BACKUP_DIR: &str = "/media/embassy/backups";

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Volumes(BTreeMap<VolumeId, Volume>);
impl Volumes {
    #[instrument(skip_all)]
    pub fn validate(&self, interfaces: &Interfaces) -> Result<(), Error> {
        for (id, volume) in &self.0 {
            volume
                .validate(interfaces)
                .with_ctx(|_| (crate::ErrorKind::ValidateS9pk, format!("Volume {}", id)))?;
            if let Volume::Backup { .. } = volume {
                return Err(Error::new(
                    eyre!("Invalid volume type \"backup\""),
                    ErrorKind::ParseS9pk,
                )); // Volume::Backup is for internal use and shouldn't be declared in manifest
            }
        }
        Ok(())
    }
    #[instrument(skip_all)]
    pub async fn install(
        &self,
        ctx: &RpcContext,
        pkg_id: &PackageId,
        version: &Version,
    ) -> Result<(), Error> {
        for (volume_id, volume) in &self.0 {
            volume
                .install(&ctx.datadir, pkg_id, version, volume_id)
                .await?; // TODO: concurrent?
        }
        Ok(())
    }
    pub fn get_path_for(
        &self,
        path: &PathBuf,
        pkg_id: &PackageId,
        version: &Version,
        volume_id: &VolumeId,
    ) -> Option<PathBuf> {
        self.0
            .get(volume_id)
            .map(|volume| volume.path_for(path, pkg_id, version, volume_id))
    }
    pub fn to_readonly(&self) -> Self {
        Volumes(
            self.0
                .iter()
                .map(|(id, volume)| {
                    let mut volume = volume.clone();
                    volume.set_readonly();
                    (id.clone(), volume)
                })
                .collect(),
        )
    }
}
impl Deref for Volumes {
    type Target = BTreeMap<VolumeId, Volume>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl DerefMut for Volumes {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}
impl Map for Volumes {
    type Key = VolumeId;
    type Value = Volume;
}

pub fn data_dir<P: AsRef<Path>>(datadir: P, pkg_id: &PackageId, volume_id: &VolumeId) -> PathBuf {
    datadir
        .as_ref()
        .join(PKG_VOLUME_DIR)
        .join(pkg_id)
        .join("data")
        .join(volume_id)
}

pub fn asset_dir<P: AsRef<Path>>(datadir: P, pkg_id: &PackageId, version: &Version) -> PathBuf {
    datadir
        .as_ref()
        .join(PKG_VOLUME_DIR)
        .join(pkg_id)
        .join("assets")
        .join(version.as_str())
}

pub fn backup_dir(pkg_id: &PackageId) -> PathBuf {
    Path::new(BACKUP_DIR).join(pkg_id).join("data")
}

pub fn cert_dir(pkg_id: &PackageId, interface_id: &InterfaceId) -> PathBuf {
    Path::new(PACKAGE_CERT_PATH).join(pkg_id).join(interface_id)
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
#[serde(rename_all = "kebab-case")]
pub enum Volume {
    #[serde(rename_all = "kebab-case")]
    Data {
        #[serde(skip)]
        readonly: bool,
    },
    #[serde(rename_all = "kebab-case")]
    Assets {},
    #[serde(rename_all = "kebab-case")]
    Pointer {
        package_id: PackageId,
        volume_id: VolumeId,
        path: PathBuf,
        readonly: bool,
    },
    #[serde(rename_all = "kebab-case")]
    Certificate { interface_id: InterfaceId },
    #[serde(rename_all = "kebab-case")]
    Backup { readonly: bool },
}
impl Volume {
    #[instrument(skip_all)]
    pub fn validate(&self, interfaces: &Interfaces) -> Result<(), color_eyre::eyre::Report> {
        match self {
            Volume::Certificate { interface_id } => {
                if !interfaces.0.contains_key(interface_id) {
                    color_eyre::eyre::bail!("unknown interface: {}", interface_id);
                }
            }
            _ => (),
        }
        Ok(())
    }
    pub async fn install(
        &self,
        path: &PathBuf,
        pkg_id: &PackageId,
        version: &Version,
        volume_id: &VolumeId,
    ) -> Result<(), Error> {
        match self {
            Volume::Data { .. } => {
                tokio::fs::create_dir_all(self.path_for(path, pkg_id, version, volume_id)).await?;
            }
            _ => (),
        }
        Ok(())
    }
    pub fn path_for(
        &self,
        data_dir_path: impl AsRef<Path>,
        pkg_id: &PackageId,
        version: &Version,
        volume_id: &VolumeId,
    ) -> PathBuf {
        match self {
            Volume::Data { .. } => data_dir(&data_dir_path, pkg_id, volume_id),
            Volume::Assets {} => asset_dir(&data_dir_path, pkg_id, version).join(volume_id),
            Volume::Pointer {
                package_id,
                volume_id,
                path,
                ..
            } => data_dir(&data_dir_path, package_id, volume_id).join(if path.is_absolute() {
                path.strip_prefix("/").unwrap()
            } else {
                path.as_ref()
            }),
            Volume::Certificate { interface_id } => cert_dir(pkg_id, &interface_id),
            Volume::Backup { .. } => backup_dir(pkg_id),
        }
    }

    pub fn pointer_path(&self, data_dir_path: impl AsRef<Path>) -> Option<PathBuf> {
        if let Volume::Pointer {
            path,
            package_id,
            volume_id,
            ..
        } = self
        {
            Some(
                data_dir(data_dir_path.as_ref(), package_id, volume_id).join(
                    if path.is_absolute() {
                        path.strip_prefix("/").unwrap()
                    } else {
                        path.as_ref()
                    },
                ),
            )
        } else {
            None
        }
    }

    pub fn set_readonly(&mut self) {
        match self {
            Volume::Data { readonly } => {
                *readonly = true;
            }
            Volume::Pointer { readonly, .. } => {
                *readonly = true;
            }
            Volume::Backup { readonly } => {
                *readonly = true;
            }
            _ => (),
        }
    }
    pub fn readonly(&self) -> bool {
        match self {
            Volume::Data { readonly } => *readonly,
            Volume::Assets {} => true,
            Volume::Pointer { readonly, .. } => *readonly,
            Volume::Certificate { .. } => true,
            Volume::Backup { readonly } => *readonly,
        }
    }
}
