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

use clap::Parser;
use log::debug;
use reqwest::{StatusCode, blocking::Client};

use obnam::{
    chunk::Label,
    config::Config,
    id::{Id, IdError, IdFactory},
    util::{UtilError, read_file, write_file},
};
use serde::{Deserialize, Serialize};
use url::Url;

use super::{Args, Leaf};

/// Retrieve a chunk from a remote Obnam server, given the chunk ID.
#[derive(Debug, Parser)]
pub struct GetRemote {
    /// Chunk ID.
    #[clap(long, short)]
    id: Option<String>,

    /// Chunk ID from file.
    #[clap(long)]
    id_file: Option<PathBuf>,

    /// Name of file containing the chunk.
    #[clap(long, short)]
    filename: PathBuf,
}

impl Leaf for GetRemote {
    type Error = RemoteCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let id = if let Some(id) = &self.id {
            Id::try_from(id).map_err(RemoteCmdError::Id)?
        } else if let Some(filename) = &self.id_file {
            id_from_file(filename)?
        } else {
            return Err(RemoteCmdError::NoChunkId);
        };

        debug!("Retrieve chunk {} from Obnam server", id);

        let url = chunks_url(config, &id)?;

        let token = server_token(config)?;

        let request = Client::new()
            .get(url)
            .header("Authorization", format!("Bearer {token}"));

        let response = request.send()?;
        if response.status() != StatusCode::OK {
            return Err(RemoteCmdError::NotFound(id));
        }

        let data = response.bytes()?;
        write_file(&self.filename, &data)?;

        Ok(())
    }
}

/// Upload a chunk to a remote Obnam server.
#[derive(Debug, Parser)]
pub struct PutRemote {
    /// Chunk ID from file.
    #[clap(long)]
    id_file: Option<PathBuf>,

    /// Chunk label.
    #[clap(long, short)]
    label: Label,

    /// Name of file containing the chunk.
    #[clap(long, short)]
    filename: PathBuf,
}

impl Leaf for PutRemote {
    type Error = RemoteCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let mut factory = IdFactory::default();
        let id = factory.id();

        debug!("Upload chunk {}, label {}, to Obnam server", id, self.label);

        let data = read_file(&self.filename)?;

        let url = chunks_url(config, &id)?;
        let url = format!("{url}?label={}", self.label);

        let token = server_token(config)?;

        let request = Client::new()
            .put(url)
            .body(data)
            .header("Authorization", format!("Bearer {token}"));

        let response = request.send()?;
        if response.status() != StatusCode::CREATED {
            return Err(RemoteCmdError::NotCreated(id));
        }

        if let Some(filename) = &self.id_file {
            write_file(filename, id.to_string().as_bytes())
                .map_err(|err| RemoteCmdError::WriteId(filename.to_path_buf(), err))?;
        }

        Ok(())
    }
}

/// Find chunks with a label on a remote Obnam server.
#[derive(Debug, Parser)]
pub struct FindRemote {
    /// Chunk label.
    #[clap(long, short)]
    label: Label,
}

impl Leaf for FindRemote {
    type Error = RemoteCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        debug!("Find chunks with label {} on Obnam server", self.label);

        let url = server_url(config)?.to_string();
        assert!(url.ends_with('/'));
        let url = format!("{url}chunks?label={}", self.label);

        let token = server_token(config)?;

        let request = Client::new()
            .get(url)
            .header("Authorization", format!("Bearer {token}"));

        let response = request.send()?;
        if response.status() != StatusCode::OK {
            return Err(RemoteCmdError::NotFoundWithLabel(self.label.clone()));
        }

        let ids: Ids = response.json()?;
        println!(
            "{}",
            serde_json::to_string(&ids).map_err(RemoteCmdError::ToJson)?
        );

        Ok(())
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct Ids {
    chunk_ids: Vec<String>,
}

/// Remove a chunk from a remote Obnam server.
#[derive(Debug, Parser)]
pub struct DeleteRemote {
    /// Chunk ID.
    #[clap(long, short)]
    id: Option<String>,

    /// Chunk ID from file.
    #[clap(long)]
    id_file: Option<PathBuf>,
}

impl Leaf for DeleteRemote {
    type Error = RemoteCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let id = if let Some(id) = &self.id {
            Id::try_from(id).map_err(RemoteCmdError::Id)?
        } else if let Some(filename) = &self.id_file {
            id_from_file(filename)?
        } else {
            return Err(RemoteCmdError::NoChunkId);
        };

        debug!("Delete chunk {} from Obnam server", id);

        let url = chunks_url(config, &id)?;

        let token = server_token(config)?;

        let request = Client::new()
            .delete(url)
            .header("Authorization", format!("Bearer {token}"));

        let response = request.send()?;
        if response.status() != StatusCode::NO_CONTENT {
            return Err(RemoteCmdError::NotDeleted(id));
        }

        Ok(())
    }
}

fn id_from_file(filename: &Path) -> Result<Id, RemoteCmdError> {
    let id =
        read_file(filename).map_err(|err| RemoteCmdError::ReadId(filename.to_path_buf(), err))?;
    let id = String::from_utf8_lossy(&id).to_string();
    let id = Id::try_from(&id).map_err(RemoteCmdError::Id)?;
    Ok(id)
}

fn chunks_url(config: &Config, id: &Id) -> Result<String, RemoteCmdError> {
    let url = server_url(config)?.to_string();
    assert!(url.ends_with('/'));
    Ok(format!("{url}chunks/{id}"))
}

fn server_url(config: &Config) -> Result<Url, RemoteCmdError> {
    config
        .server_url()
        .cloned()
        .ok_or(RemoteCmdError::ServerUrlMissing)
}

fn server_token(config: &Config) -> Result<String, RemoteCmdError> {
    config
        .server_token()
        .map(|s| s.to_string())
        .ok_or(RemoteCmdError::ServerTokenMissing)
}

#[derive(Debug, thiserror::Error)]
pub enum RemoteCmdError {
    #[error(transparent)]
    Util(#[from] UtilError),

    #[error("no server URL in config")]
    ServerUrlMissing,

    #[error("no server API token in config")]
    ServerTokenMissing,

    #[error("HTTP request failed")]
    Reqwest(#[from] reqwest::Error),

    #[error("chunk not found server: {0}")]
    NotFound(Id),

    #[error("chunks with label {0} not found on server")]
    NotFoundWithLabel(Label),

    #[error("chunk not created on server: {0}")]
    NotCreated(Id),

    #[error("chunk not deleted from server: {0}")]
    NotDeleted(Id),

    #[error("failed to encode list of chunk IDs as JSON")]
    ToJson(#[source] serde_json::Error),

    #[error("failed to parse as chunk ID: {0:?}")]
    Id(#[source] IdError),

    #[error("failed to read chunk ID from {0}")]
    ReadId(PathBuf, #[source] UtilError),

    #[error("failed to write chunk ID to {0}")]
    WriteId(PathBuf, #[source] UtilError),

    #[error("no chunk ID specified")]
    NoChunkId,
}
