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

use clap::Parser;
use directories_next::ProjectDirs;
use serde::{Deserialize, Serialize};
use tempfile::NamedTempFile;

const APP: &str = "clab";

fn main() -> anyhow::Result<()> {
    let mut opt = Opt::parse();
    let book = if let Some(filename) = &opt.db {
        AddressBook::load(filename)?
    } else {
        let proj_dirs = ProjectDirs::from("", "", APP).expect("couldn't find home directory");
        let filename = proj_dirs.data_dir().join("address-book.yaml");
        opt.db = Some(filename.clone());
        if filename.exists() {
            AddressBook::load(&filename)?
        } else {
            AddressBook::default()
        }
    };
    match &opt.cmd {
        Cmd::Config(x) => x.run(&opt, &book),
        Cmd::Lint(x) => x.run(&opt, &book),
        Cmd::List(x) => x.run(&opt, &book)?,
        Cmd::Search(x) => x.run(&opt, &book)?,
        Cmd::Tagged(x) => x.run(&opt, &book)?,
        Cmd::MuttQuery(x) => x.run(&opt, &book),
        Cmd::Reformat(x) => x.run(&opt, &book)?,
    }
    Ok(())
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Entry {
    name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    org: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    url: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    notes: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    aliases: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    phone: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    irc: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    address: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    tags: Option<Vec<String>>,
    last_checked: String,
}

impl Entry {
    fn is_match(&self, needle: &str) -> bool {
        let text = serde_norway::to_string(self).unwrap();
        contains(&text, needle)
    }

    fn emails(&self) -> Vec<String> {
        if let Some(map) = &self.email {
            map.values().map(|x| x.to_string()).collect()
        } else {
            vec![]
        }
    }
}

fn output_entries(entries: &[Entry]) -> anyhow::Result<()> {
    if !entries.is_empty() {
        serde_norway::to_writer(std::io::stdout(), entries)?;
    }
    Ok(())
}

fn contains(haystack: &str, needle: &str) -> bool {
    let haystack = haystack.to_lowercase();
    let needle = needle.to_lowercase();
    haystack.contains(&needle)
}

#[derive(std::default::Default)]
struct AddressBook {
    filename: PathBuf,
    entries: Vec<Entry>,
}

impl AddressBook {
    fn load(db: &Path) -> anyhow::Result<Self> {
        let mut book = Self {
            filename: db.to_path_buf(),
            entries: vec![],
        };
        book.add_from(db)?;
        Ok(book)
    }

    fn filename(&self) -> &Path {
        &self.filename
    }

    fn add_from(&mut self, filename: &Path) -> anyhow::Result<()> {
        let text = std::fs::read(filename)?;
        let mut entries: Vec<Entry> = serde_norway::from_slice(&text)?;
        self.entries.append(&mut entries);
        Ok(())
    }

    fn entries(&self) -> &[Entry] {
        &self.entries
    }

    fn iter(&self) -> impl Iterator<Item = &Entry> {
        self.entries.iter()
    }
}

/// Command line address book.
#[derive(Debug, Parser)]
#[clap(version)]
struct Opt {
    /// Path to address book.
    #[clap(long)]
    db: Option<PathBuf>,

    #[clap(subcommand)]
    cmd: Cmd,
}

#[derive(Debug, Parser)]
enum Cmd {
    Config(ConfigCommand),
    Lint(LintCommand),
    List(ListCommand),
    Search(SearchCommand),
    Tagged(TaggedCommand),
    MuttQuery(MuttCommand),
    Reformat(Reformat),
}

/// Show run time configuration.
#[derive(Debug, Parser)]
struct ConfigCommand {}

impl ConfigCommand {
    fn run(&self, opt: &Opt, _book: &AddressBook) {
        println!("{opt:#?}");
    }
}

/// Check that address book looks OK.
#[derive(Debug, Parser)]
struct LintCommand {}

impl LintCommand {
    fn run(&self, _opt: &Opt, _book: &AddressBook) {}
}

/// List all entries in address book.
#[derive(Debug, Parser)]
struct ListCommand {}

impl ListCommand {
    fn run(&self, _opt: &Opt, book: &AddressBook) -> anyhow::Result<()> {
        output_entries(book.entries())
    }
}

/// Search for entries using full text search.
#[derive(Debug, Parser)]
#[clap(alias = "find")]
struct SearchCommand {
    words: Vec<String>,
}

impl SearchCommand {
    fn run(&self, _opt: &Opt, book: &AddressBook) -> anyhow::Result<()> {
        let matches: Vec<Entry> = book.iter().filter(|e| self.is_match(e)).cloned().collect();
        output_entries(&matches)
    }

    fn is_match(&self, entry: &Entry) -> bool {
        for word in self.words.iter() {
            if !entry.is_match(word) {
                return false;
            }
        }
        true
    }
}

/// Search for entries with specific tags.
#[derive(Debug, Parser)]
struct TaggedCommand {
    /// Wanted tags.
    wanted_tags: Vec<String>,
}

impl TaggedCommand {
    fn run(&self, _opt: &Opt, book: &AddressBook) -> anyhow::Result<()> {
        let matches: Vec<Entry> = book.iter().filter(|e| self.is_match(e)).cloned().collect();
        output_entries(&matches)
    }

    fn is_match(&self, entry: &Entry) -> bool {
        if let Some(actual_tags) = &entry.tags {
            for wanted_tag in self.wanted_tags.iter() {
                if !actual_tags.contains(wanted_tag) {
                    return false;
                }
            }
            true
        } else {
            false
        }
    }
}

/// Like `search`, but output in mutt search format.
#[derive(Debug, Parser)]
struct MuttCommand {
    word: String,
}

impl MuttCommand {
    fn run(&self, _opt: &Opt, book: &AddressBook) {
        let matches: Vec<Entry> = book.iter().filter(|e| self.is_match(e)).cloned().collect();
        if matches.is_empty() {
            println!("clab found no matches");
            std::process::exit(1);
        }

        println!("clab found matches:");
        for e in matches {
            for email in e.emails() {
                println!("{}\t{}", email, e.name);
            }
        }
    }

    fn is_match(&self, entry: &Entry) -> bool {
        entry.is_match(&self.word)
    }
}

/// Reformat database.
#[derive(Debug, Parser)]
struct Reformat {
    /// Write to the standard output. Default is to write back to normal location.
    #[clap(short, long)]
    stdout: bool,
}

impl Reformat {
    fn run(&self, _opt: &Opt, book: &AddressBook) -> anyhow::Result<()> {
        let mut entries: Vec<Entry> = book.entries().to_vec();
        entries.sort_by_cached_key(|e| e.name.clone());
        if self.stdout {
            serde_norway::to_writer(std::io::stdout(), &entries)?;
        } else {
            let filename = book.filename();
            let dirname = match filename.parent() {
                None => Path::new("/"),
                Some(x) if x.display().to_string().is_empty() => Path::new("."),
                Some(x) => x,
            };
            let temp = NamedTempFile::new_in(dirname)?;
            serde_norway::to_writer(&temp, &entries)?;
            std::fs::rename(temp.path(), filename)?;
        }
        Ok(())
    }
}
