use std::path::PathBuf;

use clap::Parser;

use directories::ProjectDirs;
use obnam::{
    config::Config,
    filesdb::{FilesDb, FilesDbError, FsMetadata},
};

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

const FILES_DB_BASENAME: &str = "files.db";
const QUAL: &str = "org.obnam";
const ORG: &str = "The Obnam project";
const APP: &str = "obnam";

/// Start a new local files database for a new backup.
#[derive(Debug, Parser)]
pub struct Start {}

impl Leaf for Start {
    type Error = BackupCmdError;

    fn run(&self, _args: &Args, _config: &Config) -> Result<(), Self::Error> {
        let filename = files_db_filename()?;
        eprintln!("files database: {}", filename.display());

        if filename.exists() {
            return Err(BackupCmdError::Exists(filename));
        }

        let db = FilesDb::new(filename).map_err(BackupCmdError::Create)?;

        eprintln!("Created files database {}", db.filename().display());
        Ok(())
    }
}

/// Add a file to current local files database.
#[derive(Debug, Parser)]
pub struct Add {
    filename: PathBuf,
}

impl Leaf for Add {
    type Error = BackupCmdError;

    fn run(&self, _args: &Args, _config: &Config) -> Result<(), Self::Error> {
        let db = open_db()?;
        let meta = self
            .filename
            .metadata()
            .map_err(|err| BackupCmdError::get_metadata(self.filename.clone(), err))?;
        let meta = FsMetadata::try_from(&meta).map_err(BackupCmdError::FsMetadata)?;
        db.add(&self.filename, meta).map_err(BackupCmdError::Add)?;
        Ok(())
    }
}

/// Finish current local files database for a new backup.
#[derive(Debug, Parser)]
pub struct Finish {}

impl Leaf for Finish {
    type Error = BackupCmdError;

    fn run(&self, _args: &Args, _config: &Config) -> Result<(), Self::Error> {
        let filename = files_db_filename()?;
        if filename.exists() {
            std::fs::remove_file(&filename)
                .map_err(|err| BackupCmdError::RemoveDb(filename, err))?;
        }
        eprintln!("finished");
        Ok(())
    }
}

fn dirs() -> Result<ProjectDirs, BackupCmdError> {
    let res = ProjectDirs::from(QUAL, ORG, APP).ok_or(BackupCmdError::ProjectDirs);
    // eprintln!("project dirs: {res:#?}");
    res
}

fn state_dir() -> Result<PathBuf, BackupCmdError> {
    let dir = dirs()?;
    let dir = dir.state_dir().ok_or(BackupCmdError::StateDir)?;
    if !dir.exists() {
        std::fs::create_dir_all(dir)
            .map_err(|err| BackupCmdError::CreateStateDir(dir.to_path_buf(), err))?;
    }
    Ok(dir.to_path_buf())
}

fn files_db_filename() -> Result<PathBuf, BackupCmdError> {
    let dir = state_dir()?;
    Ok(dir.join(FILES_DB_BASENAME))
}

fn open_db() -> Result<FilesDb, BackupCmdError> {
    let filename = files_db_filename()?;
    FilesDb::new(filename).map_err(BackupCmdError::Open)
}

#[derive(Debug, thiserror::Error)]
pub enum BackupCmdError {
    #[error("failed to determie XDG project directories")]
    ProjectDirs,

    #[error("failed to determine the XDG project state diretory")]
    StateDir,

    #[error("failed to create state directory {0}")]
    CreateStateDir(PathBuf, #[source] std::io::Error),

    #[error("files database already exists: {0}")]
    Exists(PathBuf),

    #[error("failed to create files database")]
    Create(#[source] FilesDbError),

    #[error("failedc to open files database")]
    Open(#[source] FilesDbError),

    #[error("failed to get file metadata for {filename}")]
    GetMetadata {
        filename: PathBuf,
        source: std::io::Error,
    },

    #[error("failed to convert metadata to internal format")]
    FsMetadata(#[source] FilesDbError),

    #[error("failed to add file to database")]
    Add(#[source] FilesDbError),

    #[error("failed to remove database file {0}")]
    RemoveDb(PathBuf, #[source] std::io::Error),
}

impl BackupCmdError {
    fn get_metadata(filename: impl Into<PathBuf>, source: std::io::Error) -> Self {
        Self::GetMetadata {
            filename: filename.into(),
            source,
        }
    }
}

impl From<BackupCmdError> for ObnamError {
    fn from(value: BackupCmdError) -> Self {
        ObnamError::Backup(value)
    }
}
