use std::path::{Path, PathBuf};

use crank_core::{DescriptorId, OperationId, SampleId};
use crank_registry::{DescriptorKind, SampleKind};
use serde_json::Value;
use thiserror::Error;
use tokio::fs;

#[derive(Clone)]
pub struct LocalArtifactStorage {
    root: PathBuf,
}

#[derive(Debug, Error)]
pub enum StorageError {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Serialization(#[from] serde_json::Error),
    #[error("invalid storage reference: {details}")]
    InvalidStorageRef { details: String },
}

impl LocalArtifactStorage {
    pub fn new(root: PathBuf) -> Self {
        Self { root }
    }

    pub async fn write_json_sample(
        &self,
        operation_id: &OperationId,
        version: u32,
        sample_kind: SampleKind,
        sample_id: &SampleId,
        payload: &Value,
    ) -> Result<String, StorageError> {
        let file_name = match sample_kind {
            SampleKind::InputJson => "input.json",
            SampleKind::OutputJson => "output.json",
            SampleKind::YamlImportSource => "source.json",
        };
        let path = self
            .root
            .join("samples")
            .join(operation_id.as_str())
            .join(format!("v{version}"))
            .join(format!("{}_{}", sample_id.as_str(), file_name));

        write_json_file(&path, payload).await?;

        Ok(to_storage_ref(&path))
    }

    pub async fn read_json(&self, storage_ref: &str) -> Result<Value, StorageError> {
        let path = from_storage_ref(storage_ref)?;
        let bytes = fs::read(path).await?;

        Ok(serde_json::from_slice(&bytes)?)
    }

    pub async fn write_descriptor(
        &self,
        operation_id: &OperationId,
        version: u32,
        descriptor_kind: DescriptorKind,
        descriptor_id: &DescriptorId,
        source_name: Option<&str>,
        payload: &[u8],
    ) -> Result<String, StorageError> {
        let file_name = match descriptor_kind {
            DescriptorKind::ProtoUpload => source_name.unwrap_or("schema.proto").to_owned(),
            DescriptorKind::DescriptorSet => source_name.unwrap_or("descriptor-set.bin").to_owned(),
            DescriptorKind::ReflectionSnapshot => {
                source_name.unwrap_or("reflection.bin").to_owned()
            }
            DescriptorKind::WsdlUpload => source_name.unwrap_or("service.wsdl").to_owned(),
            DescriptorKind::XsdUpload => source_name.unwrap_or("schema.xsd").to_owned(),
        };
        let path = self
            .root
            .join("descriptors")
            .join(operation_id.as_str())
            .join(format!("v{version}"))
            .join(format!("{}_{}", descriptor_id.as_str(), file_name));

        write_bytes(&path, payload).await?;

        Ok(to_storage_ref(&path))
    }

    pub async fn read_bytes(&self, storage_ref: &str) -> Result<Vec<u8>, StorageError> {
        let path = from_storage_ref(storage_ref)?;
        Ok(fs::read(path).await?)
    }
}

async fn write_json_file(path: &Path, payload: &Value) -> Result<(), StorageError> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).await?;
    }

    let bytes = serde_json::to_vec_pretty(payload)?;
    fs::write(path, bytes).await?;
    Ok(())
}

async fn write_bytes(path: &Path, payload: &[u8]) -> Result<(), StorageError> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).await?;
    }

    fs::write(path, payload).await?;
    Ok(())
}

fn to_storage_ref(path: &Path) -> String {
    format!("file://{}", path.display())
}

fn from_storage_ref(storage_ref: &str) -> Result<PathBuf, StorageError> {
    let Some(path) = storage_ref.strip_prefix("file://") else {
        return Err(StorageError::InvalidStorageRef {
            details: storage_ref.to_owned(),
        });
    };

    Ok(PathBuf::from(path))
}
