use core::mem;

use beancount_pretty_printer::PrettyPrinter;
use beancount_tree_writer::Config as TreeWriterConfig;
use beancount_tree_writer::TreeWriter;
use beancount_types::common_keys;
use beancount_types::Directive;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use miette::Context as _;
use miette::IntoDiagnostic as _;
use miette::Report as ErrorReport;
use miette::Result;
use relative_path::RelativePathBuf; // TODO

use crate::ImporterProtocol;
use crate::ImporterRegistry;

mod archive;

#[derive(Debug)]
pub struct Builder {
    archive_directory: Utf8PathBuf,

    dry_run: bool,

    importers: ImporterRegistry<ErrorReport>,

    overwrite: bool,
}

impl Builder {
    pub fn archive_directory(&mut self, directory: Utf8PathBuf) -> &mut Self {
        self.archive_directory = directory;
        self
    }

    pub fn build(&mut self) -> Runner {
        let Self {
            archive_directory,
            dry_run,
            importers,
            overwrite,
        } = mem::take(self);

        let archive_directory = archive_directory.canonicalize_utf8().unwrap();

        Runner {
            archive_directory,
            dry_run,
            importers,
            overwrite,
        }
    }

    pub fn dry_run(&mut self) -> &mut Self {
        self.dry_run = true;
        self
    }

    pub fn overwrite(&mut self) -> &mut Self {
        self.overwrite = true;
        self
    }

    pub fn register_importer(
        &mut self,
        importer: impl ImporterProtocol<Error = ErrorReport> + 'static,
    ) -> &mut Self {
        self.importers.register_importer(importer);
        self
    }

    pub fn with_dry_run(&mut self, dry_run: bool) -> &mut Self {
        self.dry_run = dry_run;
        self
    }

    pub fn with_overwrite(&mut self, overwrite: bool) -> &mut Self {
        self.overwrite = overwrite;
        self
    }
}

impl Default for Builder {
    fn default() -> Self {
        let (dry_run, importers, overwrite) = Default::default();

        Self {
            archive_directory: Utf8PathBuf::from("archive"),
            dry_run,
            importers,
            overwrite,
        }
    }
}

#[derive(Debug)]
pub struct Runner {
    archive_directory: Utf8PathBuf,

    dry_run: bool,

    importers: ImporterRegistry<ErrorReport>,

    overwrite: bool,
}

impl Runner {
    pub fn builder() -> Builder {
        Builder::default()
    }
}

impl Runner {
    #[tracing::instrument]
    pub fn run(&self, paths: &[Utf8PathBuf], tree_writer_config: TreeWriterConfig) -> Result<()> {
        let directives = self.process_paths(paths)?;

        if directives.is_empty() {
            tracing::warn!("extracted no directives");
            return Ok(());
        }

        tracing::info!("extracted {} directives", directives.len());

        if self.dry_run {
            let config = beancount_pretty_printer::Config::derive_from_directives(&directives);
            let mut printer = PrettyPrinter::unbuffered(config, std::io::stdout().lock());
            printer.print_directives(&directives).into_diagnostic()?;
        } else {
            TreeWriter::new(tree_writer_config).write_directives(directives)?;

            tracing::info!("successfully wrote tree of accounts");
        }

        Ok(())
    }
}

impl Runner {
    #[tracing::instrument(fields(importer = importer.name()), skip(self))]
    fn archive_file<'a>(
        &self,
        importer: &(dyn ImporterProtocol<Error = ErrorReport>),
        file: &'a Utf8Path,
        destination: RelativePathBuf,
    ) -> Result<()> {
        if self.dry_run {
            return Ok(());
        }

        let destination = destination.to_logical_path(&self.archive_directory);
        let destination = Utf8Path::from_path(&destination).expect("should be UTF-8");

        if file != destination {
            tracing::info!(?file, ?destination, "archiving file");

            if destination.exists() {
                miette::ensure!(
                    self.overwrite,
                    "destination {destination:?} already exists",
                    destination = destination,
                );
            }

            archive::move_file(file, destination).wrap_err("while moving file into archive")?;
        }

        Ok(())
    }

    #[tracing::instrument(skip(self, buffer))]
    fn identify_file(&self, buffer: &[u8]) -> Option<&dyn ImporterProtocol<Error = ErrorReport>> {
        self.importers
            .iter()
            .find(|importer| importer.identify(buffer).unwrap_or_default())
    }

    #[tracing::instrument(skip(self, directives))]
    fn process_dir(&self, directives: &mut Vec<Directive>, path: &Utf8Path) -> Result<()> {
        let entries = path
            .read_dir_utf8()
            .into_diagnostic()
            .wrap_err("could not read directory")?;

        for entry in entries {
            let entry = entry.into_diagnostic()?;
            let path = entry.path();

            self.process_path(directives, path)
        }

        Ok(())
    }

    #[tracing::instrument(skip(self, directives))]
    fn process_file(&self, directives: &mut Vec<Directive>, file: &Utf8Path) -> Result<()> {
        let buffer = std::fs::read(file).into_diagnostic()?;

        let Some(importer) = self.identify_file(&buffer) else {
            tracing::warn!(%file, "ignoring file since no importer could identify it");
            return Ok(());
        };

        let destination = archive::file_name(importer, file, &buffer)?;

        let mut extracted_directives = importer
            .extract(&buffer, &[]) // TODO load existing transactions
            .wrap_err_with(|| format!("error in importer {:?}", importer.name()))?;

        for directive in &mut extracted_directives {
            directive.add_meta(common_keys::IMPORTED_FROM, destination.as_str());
        }

        self.archive_file(importer, file, destination)
            .wrap_err("while archiving file")?;

        directives.append(&mut extracted_directives);

        Ok(())
    }

    #[tracing::instrument(skip(self, directives))]
    fn process_path(&self, directives: &mut Vec<Directive>, path: &Utf8Path) {
        let (kind, result) = if path.is_dir() {
            ("directory", self.process_dir(directives, path))
        } else {
            ("file", self.process_file(directives, path))
        };

        if let Err(error) = result {
            tracing::error!(%path, ?error, "error while importing {kind}");
        }
    }

    #[tracing::instrument(skip(self))]
    fn process_paths(&self, paths: &[Utf8PathBuf]) -> Result<Vec<Directive>> {
        paths.iter().try_fold(Vec::new(), |mut directives, path| {
            let path = path.canonicalize_utf8().into_diagnostic()?;

            self.process_path(&mut directives, &path);
            Ok(directives)
        })
    }
}