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

use clap::Parser;

use log::debug;
use obnam::{
    chunk::Label,
    config::Config,
    id::{Id, IdError},
    repository::BackupRepository,
    util::{UtilError, read_file, write, write_file},
};

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

/// Has a directory been initialized as a backup repository?
#[derive(Parser)]
pub struct IsRepo {}

impl Leaf for IsRepo {
    type Error = RepoCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let dir = Self::need_repository(config)?;
        debug!(
            "Check if directory is a chunk repository: {}",
            dir.display()
        );
        if BackupRepository::is_init(dir) {
            Ok(())
        } else {
            Err(RepoCmdError::NotInit(dir.into()))
        }
    }
}

/// Initialize a directory as a backup repository. The directory must exist.
#[derive(Parser)]
pub struct InitRepo {}

impl Leaf for InitRepo {
    type Error = RepoCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        let dir = Self::need_repository(config)?;
        debug!(
            "Initialize a direcory as a chunk repository: {}",
            dir.display()
        );
        BackupRepository::init(dir).map_err(RepoCmdError::Init)?;
        Ok(())
    }
}

/// List identifies for all chunks in the backup repository.
#[derive(Parser)]
pub struct ListRepoChunks {}

impl Leaf for ListRepoChunks {
    type Error = RepoCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        debug!("List chunks in the chunk repository");
        let repo = Self::open_repository(config)?;
        for metadata in repo.all_chunks().map_err(RepoCmdError::ListChunks)? {
            println!("{} {}", metadata.id(), metadata.label());
        }
        Ok(())
    }
}

/// Find chunks with a given label in in the backup repository.
#[derive(Parser)]
pub struct FindRepoChunks {
    label: Label,

    /// If there is only one match, write the chunk ID to this file.
    /// This is primarily useful for the Obnam test suite.
    #[clap(long)]
    one_match_output: Option<PathBuf>,
}

impl Leaf for FindRepoChunks {
    type Error = RepoCmdError;

    fn run(&self, _args: &Args, config: &Config) -> Result<(), Self::Error> {
        debug!(
            "Find chunks in the chunk repository with label {}",
            self.label
        );
        let repo = Self::open_repository(config)?;
        let matches = repo
            .find_chunks(&self.label)
            .map_err(RepoCmdError::FindChunks)?;
        if let Some(filename) = &self.one_match_output
            && matches.len() == 1
        {
            let metadata = matches.first().unwrap();
            write_file(filename, metadata.id().to_string().as_bytes())
                .map_err(|err| RepoCmdError::WriteId(filename.to_path_buf(), err))?;
        } else {
            for metadata in repo
                .find_chunks(&self.label)
                .map_err(RepoCmdError::ListChunks)?
            {
                println!("{}", metadata.id());
            }
        }

        Ok(())
    }
}

/// Output the path to a chunk with a given identifier in the backup repository.
#[derive(Parser)]
pub struct RepoChunkPath {
    #[clap(long)]
    id: Option<String>,

    #[clap(long)]
    id_file: Option<PathBuf>,

    #[clap(long, short)]
    output: Option<PathBuf>,
}

impl Leaf for RepoChunkPath {
    type Error = RepoCmdError;

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

        debug!("Show file system path to chunks in the chunk repository: {id}");
        let repo = Self::open_repository(config)?;
        let filename = repo.chunk_filename(&id);
        write(&self.output, filename.as_os_str().as_encoded_bytes())
            .map_err(RepoCmdError::WriteCHunkFilename)?;

        Ok(())
    }
}

/// Remove a chunk from a backup repository.
#[derive(Parser)]
pub struct RepoRemoveChunk {
    #[clap(long)]
    id: Option<String>,

    #[clap(long)]
    id_file: Option<PathBuf>,
}

impl Leaf for RepoRemoveChunk {
    type Error = RepoCmdError;

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

        debug!("Remove chunk from the chunk repository: {}", id);
        let repo = Self::open_repository(config)?;
        repo.remove_chunk(&id)
            .map_err(|err| RepoCmdError::Remove(id, err))?;

        Ok(())
    }
}

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

#[derive(Debug, thiserror::Error)]
pub enum RepoCmdError {
    #[error("backup repository directory has not been initialized: {0}")]
    NotInit(PathBuf),

    #[error("failed to initialize backup repository directory")]
    Init(#[source] obnam::repository::RepositoryError),

    #[error("failed list chunks in backup repository")]
    ListChunks(#[source] obnam::repository::RepositoryError),

    #[error("failed to search for chunks in backup repository")]
    FindChunks(#[source] obnam::repository::RepositoryError),

    #[error("failed to write chunk filename")]
    WriteCHunkFilename(#[source] obnam::util::UtilError),

    #[error("failed to remove chunk from backup repository: {0:?}")]
    Remove(Id, #[source] obnam::repository::RepositoryError),

    #[error(transparent)]
    Main(#[from] MainError),

    #[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,
}
