// bibiman - a TUI for managing BibLaTeX databases
// Copyright (C) 2025  lukeflo
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
/////

use std::{
    ffi::OsStr,
    fs::OpenOptions,
    io::Write,
    path::{Path, PathBuf},
};

use biblatex::Bibliography;
use color_eyre::eyre::{OptionExt, eyre};
use lexopt::Arg::{Long, Short};
use owo_colors::OwoColorize;
use serde::{Deserialize, Serialize};

use crate::{
    bibiman::{
        bibisetup::collect_file_paths,
        citekeys::citekey_utils::{SKIPPED_ENTRIES, build_citekey, formatting_help},
    },
    config::{BibiConfig, IGNORED_SPECIAL_CHARS, IGNORED_WORDS},
};

mod citekey_utils;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CitekeyCase {
    #[serde(alias = "uppercase", alias = "upper")]
    Upper,
    #[serde(alias = "lowercase", alias = "lower")]
    Lower,
    #[serde(
        alias = "camel",
        alias = "camelcase",
        alias = "camel_case",
        alias = "uppercamelcase",
        alias = "upper_camel_case",
        alias = "pascalcase"
    )]
    Camel,
}

#[derive(Debug, Default, Clone)]
pub(crate) struct CitekeyFormatting<'a> {
    /// bibfile to replace keys at. The optional fields defines a differing
    /// output file to write to, otherwise original file will be overwritten.
    bib_entries: Bibliography,
    fields: Vec<String>,
    case: Option<CitekeyCase>,
    old_new_keys_map: Vec<(String, String)>,
    dry_run: bool,
    ascii_only: bool,
    ignored_chars: &'a [char],
    ignored_words: &'a [String],
}

impl<'a> CitekeyFormatting<'a> {
    pub(crate) fn parse_citekey_cli(
        parser: &mut lexopt::Parser,
        cfg: &BibiConfig,
    ) -> color_eyre::Result<()> {
        let mut formatter = CitekeyFormatting::default();
        let mut source_file = PathBuf::new();
        let mut target_file: Option<PathBuf> = None;
        let mut update_files = false;

        formatter.fields = cfg.citekey_formatter.fields.clone().ok_or_eyre(format!(
            "Need to define {} correctly in config file",
            "citekey pattern fields".red()
        ))?;

        formatter.case = cfg.citekey_formatter.case.clone();

        formatter.ascii_only = cfg.citekey_formatter.ascii_only;

        if formatter.fields.is_empty() {
            return Err(eyre!(
                "To format all citekeys, you need to provide {} values in the config file",
                "fields".bold()
            ));
        }
        while let Some(arg) = parser.next()? {
            match arg {
                Short('h') | Long("help") => {
                    formatting_help();
                    return Ok(());
                }
                Short('d') | Long("dry-run") => formatter.dry_run = true,
                Short('s') | Short('f') | Long("source") | Long("file") => {
                    source_file = parser.value()?.into()
                }
                Short('t') | Short('o') | Long("target") | Long("output") => {
                    target_file = Some(parser.value()?.into())
                }
                Short('u') | Long("update-attachments") => update_files = true,
                _ => return Err(arg.unexpected().into()),
            }
        }

        let bibstring = std::fs::read_to_string(&source_file)?;

        formatter.bib_entries = Bibliography::parse(&bibstring)
            .map_err(|e| eyre!("Couldn't parse bibfile due to {}", e.kind))?;

        formatter.ignored_chars = if let Some(chars) = &cfg.citekey_formatter.ignored_chars {
            chars.as_slice()
        } else {
            IGNORED_SPECIAL_CHARS.as_slice()
        };

        formatter.ignored_words = if let Some(words) = &cfg.citekey_formatter.ignored_words {
            words.as_slice()
        } else {
            &*IGNORED_WORDS.as_slice()
        };

        let mut updated_formatter = formatter.do_formatting().rev_sort_new_keys_by_len();

        updated_formatter.update_file(source_file, target_file)?;

        if update_files {
            updated_formatter.update_notes_pdfs(cfg)?;
        }

        Ok(())
    }

    /// Start Citekey formatting with building a new instance of `CitekeyFormatting`
    pub fn new(cfg: &'a BibiConfig, bib_entries: Bibliography) -> Option<Self> {
        let fields = cfg.citekey_formatter.fields.clone().unwrap_or(Vec::new());
        if fields.is_empty() {
            return None;
        }
        let ignored_chars = if let Some(chars) = &cfg.citekey_formatter.ignored_chars {
            chars.as_slice()
        } else {
            IGNORED_SPECIAL_CHARS.as_slice()
        };

        let ignored_words = if let Some(words) = &cfg.citekey_formatter.ignored_words {
            words.as_slice()
        } else {
            &*IGNORED_WORDS.as_slice()
        };

        Some(Self {
            bib_entries,
            fields,
            case: cfg.citekey_formatter.case.clone(),
            old_new_keys_map: Vec::new(),
            dry_run: false,
            ascii_only: cfg.citekey_formatter.ascii_only,
            ignored_chars,
            ignored_words,
        })
    }

    /// Process the actual formatting. Updated citekeys will be stored in a the
    /// `self.old_new_keys_map` vector consisting of pairs (old key, new key).
    pub fn do_formatting(mut self) -> Self {
        let mut old_new_keys: Vec<(String, String)> = Vec::new();
        for entry in self.bib_entries.iter() {
            // Skip specific entries
            if SKIPPED_ENTRIES.contains(&entry.entry_type.to_string().to_lowercase().as_str()) {
                continue;
            }
            old_new_keys.push((
                entry.key.clone(),
                build_citekey(
                    entry,
                    &self.fields,
                    self.case.as_ref(),
                    self.ascii_only,
                    self.ignored_chars,
                    self.ignored_words,
                ),
            ));
        }

        self.old_new_keys_map = old_new_keys;

        self
    }

    /// Write formatted citekeys to bibfile replacing the old keys in all fields
    pub fn update_file<P: AsRef<Path>>(
        &mut self,
        source_file: P,
        target_file: Option<P>,
    ) -> color_eyre::Result<()> {
        if self.dry_run {
            println!(
                "{}\n",
                "Following citekeys would be formatted: old => new"
                    .bold()
                    .underline()
                    .white()
            );
            self.old_new_keys_map.sort_by(|a, b| a.0.cmp(&b.0));
            for (old, new) in &self.old_new_keys_map {
                println!("{} => {}", old.italic(), new.bold())
            }
        } else {
            let target_file = if let Some(path) = target_file {
                path.as_ref().to_path_buf()
            } else {
                source_file.as_ref().to_path_buf()
            };
            let mut content = std::fs::read_to_string(source_file)?;

            for (old_key, new_key) in self.old_new_keys_map.iter() {
                content = content.replace(old_key, new_key);
            }

            let mut new_file = OpenOptions::new()
                .truncate(true)
                .write(true)
                .create(true)
                .open(target_file)?;

            new_file.write_all(content.as_bytes())?;
        }
        Ok(())
    }

    /// Sort the vector containing old/new citekey pairs by the length of the latter.
    /// That will prevent the replacement longer key parts that equal a full shorter
    /// key.
    ///
    /// You are **very encouraged** to call this method before `update_file()`
    /// or `update_notes_pdfs` to prevent replacing citekeys partly which
    /// afterwards wont match the pattern anymore.
    pub fn rev_sort_new_keys_by_len(mut self) -> Self {
        self.old_new_keys_map
            .sort_by(|a, b| b.0.len().cmp(&a.0.len()));
        self
    }

    pub fn update_notes_pdfs(&self, cfg: &BibiConfig) -> color_eyre::Result<()> {
        if let Some(pdf_path) = &cfg.general.pdf_path {
            self.update_files_by_citekey_basename(pdf_path, vec!["pdf".into()].as_slice())?;
        }
        if let Some(note_path) = &cfg.general.note_path
            && let Some(ext) = &cfg.general.note_extensions
        {
            self.update_files_by_citekey_basename(note_path, ext.as_slice())?;
        }
        Ok(())
    }

    fn update_files_by_citekey_basename<P: AsRef<Path>>(
        &self,
        path: P,
        ext: &[String],
    ) -> color_eyre::Result<()> {
        let files = collect_file_paths(path.as_ref(), Some(ext));
        if self.dry_run {
            println!(
                "\n{}\n",
                "Following paths would be updated:"
                    .underline()
                    .bold()
                    .white()
            )
        }
        if let Some(mut f) = files {
            for (old_key, new_key) in self.old_new_keys_map.iter() {
                for e in ext {
                    let old_basename = old_key.to_owned() + "." + e;
                    if let Some(item) = f.get_mut(&old_basename) {
                        for p in item {
                            let ext = p.extension();
                            let basename = new_key.to_owned()
                                + "."
                                + ext.unwrap_or(OsStr::new("")).to_str().unwrap_or("");
                            let new_name = p
                                .parent()
                                .expect("parent expected")
                                .join(Path::new(&basename));
                            if !self.dry_run {
                                std::fs::rename(p, new_name)?;
                            } else {
                                println!(
                                    "{} => {}",
                                    p.display().to_string().italic().dimmed(),
                                    new_name.display().to_string().bold()
                                )
                            }
                        }
                    }
                }
            }
        }
        Ok(())
    }

    /// Update the `Bibliography` of the `CitekeyFormatting` struct and return
    /// it as `String`.
    pub fn print_updated_bib_as_string(&mut self) -> String {
        let mut content = self.bib_entries.to_biblatex_string();
        for (old_key, new_key) in self.old_new_keys_map.iter() {
            content = content.replace(old_key, new_key);
        }
        content
    }

    pub fn get_citekey_pair(&self, idx: usize) -> Option<(String, String)> {
        self.old_new_keys_map.get(idx).map(|pair| pair.to_owned())
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        bibiman::citekeys::{CitekeyCase, CitekeyFormatting},
        config::{IGNORED_SPECIAL_CHARS, IGNORED_WORDS},
    };
    use biblatex::Bibliography;

    #[test]
    fn format_citekey_test() {
        let src = r"
        @article{Bos2023,
            title         = {{LaTeX}, metadata, and publishing workflows},
            author        = {Bos, Joppe W. and {McCurley}, Kevin S.},
            year          = {2023},
            month         = apr,
            journal       = {arXiv},
            number        = {{arXiv}:2301.08277},
            doi           = {10.48550/arXiv.2301.08277},
            url           = {http://arxiv.org/abs/2301.08277},
            urldate       = {2023-08-22},
            note          = {type: article},
        }
        @book{Bhambra2021,
            title         = {Colonialism and \textbf{Modern Social Theory}},
            author        = {Bhambra, Gurminder K. and Holmwood, John},
            location      = {Cambridge and Medford},
            publisher     = {Polity Press},
            date          = {2021},
        }
        ";
        let bibliography = Bibliography::parse(src).unwrap();
        let formatting_struct = CitekeyFormatting {
            bib_entries: bibliography,
            fields: vec![
                "entrytype;;;;:".into(),
                "author;;;-;_".into(),
                "title;4;3;=;_".into(),
                "location;;4;:;_".into(),
                "year".into(),
            ],
            case: Some(CitekeyCase::Lower),
            old_new_keys_map: Vec::new(),
            dry_run: false,
            ascii_only: true,
            ignored_chars: &IGNORED_SPECIAL_CHARS,
            ignored_words: &IGNORED_WORDS,
        };
        let formatting_struct = formatting_struct.do_formatting();
        assert_eq!(
            formatting_struct.old_new_keys_map.get(0).unwrap().1,
            "article:bos-mccurley_lat=met=pub=wor_2023"
        );
        assert_eq!(
            formatting_struct.old_new_keys_map.get(1).unwrap().1,
            "book:bhambra-holmwood_col=mod=soc=the_camb:medf_2021"
        );
    }

    #[test]
    fn sorting_appended_citekeys() {
        let mut keys: Vec<(String, String)> = vec![
            ("smith2000".into(), "smith_book_2000".into()),
            ("smith2000a".into(), "smith_book_2000a".into()),
            ("smith2000ab".into(), "smith_book_2000ab".into()),
        ];
        keys.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
        let mut keys = keys.iter();
        assert_eq!(keys.next().unwrap().1, "smith_book_2000ab");
        assert_eq!(keys.next().unwrap().1, "smith_book_2000a");
        assert_eq!(keys.next().unwrap().1, "smith_book_2000");
    }
}
