use std::path::PathBuf;

use clap::Parser;

use obnam::{
    api_client::{ApiClient, ApiClientError},
    chunk::{ChunkError, Label},
    config::Config,
    id::{Id, IdError},
};

use super::{Args, Leaf, MainError};

/// Get a chunk using server API.
#[derive(Debug, Parser)]
pub struct Get {
    /// ID of chunk.
    id: String,

    /// Write chunk to this file.
    #[clap(long)]
    filename: PathBuf,
}

impl Leaf for Get {
    type Error = ApiCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let id = Id::try_from(&self.id).map_err(|err| ApiCmdError::BadId(self.id.clone(), err))?;

        let url = config.server_url().ok_or(ApiCmdError::ServerUrlMissing)?;
        let token = config
            .server_token()
            .ok_or(ApiCmdError::ServerTokenMissing)?;
        let api_client = ApiClient::new(url, token).map_err(ApiCmdError::ApiCreate)?;

        let chunk = api_client.get(&id).map_err(ApiCmdError::Api)?;
        let chunk = chunk.serialize().map_err(ApiCmdError::SerializeChunk)?;
        std::fs::write(&self.filename, &chunk)
            .map_err(|err| ApiCmdError::WriteChunk(self.filename.clone(), err))?;

        Ok(())
    }
}

/// Find chunks with a given label.
#[derive(Debug, Parser)]
pub struct Find {
    /// Label of chunk.
    label: Label,
}

impl Leaf for Find {
    type Error = ApiCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let url = config.server_url().ok_or(ApiCmdError::ServerUrlMissing)?;
        let token = config
            .server_token()
            .ok_or(ApiCmdError::ServerTokenMissing)?;
        let api_client = ApiClient::new(url, token).map_err(ApiCmdError::ApiCreate)?;

        let ids = api_client.find(&self.label).map_err(ApiCmdError::Api)?;
        for id in ids {
            println!("{id}");
        }

        Ok(())
    }
}

/// Put a chunk using server API.
#[derive(Debug, Parser)]
pub struct Put {
    /// ID of chunk.
    id: String,

    /// Label of chunk.
    label: Label,

    /// Read chunk from this file.
    #[clap(long)]
    filename: PathBuf,
}

impl Leaf for Put {
    type Error = ApiCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let id = Id::try_from(&self.id).map_err(|err| ApiCmdError::BadId(self.id.clone(), err))?;

        let url = config.server_url().ok_or(ApiCmdError::ServerUrlMissing)?;
        let token = config
            .server_token()
            .ok_or(ApiCmdError::ServerTokenMissing)?;
        let api_client = ApiClient::new(url, token).map_err(ApiCmdError::ApiCreate)?;

        let chunk = std::fs::read(&self.filename)
            .map_err(|err| ApiCmdError::ReadChunk(self.filename.clone(), err))?;
        api_client
            .put(&id, &self.label, chunk)
            .map_err(ApiCmdError::Api)?;

        Ok(())
    }
}

/// Delete chunk via API
#[derive(Debug, Parser)]
pub struct Delete {
    /// ID of chunk.
    id: String,
}

impl Leaf for Delete {
    type Error = ApiCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let id = Id::try_from(&self.id).map_err(|err| ApiCmdError::BadId(self.id.clone(), err))?;
        let url = config.server_url().ok_or(ApiCmdError::ServerUrlMissing)?;
        let token = config
            .server_token()
            .ok_or(ApiCmdError::ServerTokenMissing)?;
        let api_client = ApiClient::new(url, token).map_err(ApiCmdError::ApiCreate)?;

        api_client.delete(&id).map_err(ApiCmdError::Api)?;

        Ok(())
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ApiCmdError {
    /// Bad ID.
    #[error("failed to understand chunk ID: {0:?}")]
    BadId(String, IdError),

    /// Can't create API client.
    #[error("failed to create API client")]
    ApiCreate(#[source] ApiClientError),

    /// No server URL.
    #[error("configuration lacks server URL")]
    ServerUrlMissing,

    /// No server token.
    #[error("configuration lacks server API token")]
    ServerTokenMissing,

    /// API client failed.
    #[error("server HTTP API request failed")]
    Api(#[source] ApiClientError),

    /// Failed to serialize chunk.
    #[error("failed to serialize chunk")]
    SerializeChunk(#[source] ChunkError),

    /// Read error.
    #[error("failed to read chunk from file {0}")]
    ReadChunk(PathBuf, #[source] std::io::Error),

    /// Write error.
    #[error("failed to write chunk to file {0}")]
    WriteChunk(PathBuf, #[source] std::io::Error),
}

impl From<ApiCmdError> for MainError {
    fn from(value: ApiCmdError) -> Self {
        MainError::ApiError(value)
    }
}
