use std::{collections::HashMap, io::Write};

use clap::{Parser, Subcommand};
use letterbox_shared::{cleanup_match, Match, MatchType, Rule};
use sqlx::{types::Json, PgPool};

#[derive(Debug, Subcommand)]
enum Mode {
    Debug,
    Notmuchrc,
    LoadSql {
        #[arg(short, long)]
        dsn: String,
    },
}

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    #[arg(short, long, default_value = "/home/wathiede/dotfiles/procmailrc")]
    input: String,

    #[command(subcommand)]
    mode: Mode,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let mut rules = Vec::new();
    let mut cur_rule = Rule::default();
    for l in std::fs::read_to_string(args.input)?.lines() {
        let l = if let Some(idx) = l.find('#') {
            &l[..idx]
        } else {
            l
        }
        .trim();
        if l.is_empty() {
            continue;
        }
        if l.find('=').is_some() {
            // Probably a variable assignment, skip line
            continue;
        }
        let first = l.chars().nth(0).unwrap_or(' ');
        match first {
            ':' => {
                // start of rule

                // If carbon-copy flag present, don't stop on match
                cur_rule.stop_on_match = !l.contains('c');
            }
            '*' => {
                // add to current rule
                let m: Match = l.parse()?;
                cur_rule.matches.push(m);
            }
            '.' => {
                // delivery to folder
                cur_rule.tag = cleanup_match(
                    "",
                    &l.replace('.', "/")
                        .replace(' ', "")
                        .trim_matches('/')
                        .to_string(),
                );
                rules.push(cur_rule);
                cur_rule = Rule::default();
            }
            '/' => cur_rule = Rule::default(), // Ex. /dev/null
            '|' => cur_rule = Rule::default(), // external command
            '$' => {
                // TODO(wathiede): tag messages with no other tag as 'inbox'
                cur_rule.tag = cleanup_match("", "inbox");
                rules.push(cur_rule);
                cur_rule = Rule::default();
            } // variable, should only be $DEFAULT in my config
            _ => panic!("Unhandled first character '{}'\nLine: {}", first, l),
        }
    }
    match args.mode {
        Mode::Debug => print_rules(&rules),
        Mode::Notmuchrc => notmuch_from_rules(std::io::stdout(), &rules)?,
        Mode::LoadSql { dsn } => load_sql(&dsn, &rules).await?,
    }
    Ok(())
}

fn print_rules(rules: &[Rule]) {
    let mut tally = HashMap::new();
    for r in rules {
        for m in &r.matches {
            *tally.entry(m.match_type).or_insert(0) += 1;
        }
    }
    let mut sorted: Vec<_> = tally.iter().map(|(k, v)| (v, k)).collect();
    sorted.sort();
    sorted.reverse();
    for (v, k) in sorted {
        println!("{k:?}: {v}");
    }
}

fn notmuch_from_rules<W: Write>(mut w: W, rules: &[Rule]) -> anyhow::Result<()> {
    // TODO(wathiede): if reindexing this many tags is too slow, see if combining rules per tag is
    // faster.
    let mut lines = Vec::new();
    for r in rules {
        for m in &r.matches {
            let t = &r.tag;
            if let MatchType::Unknown = m.match_type {
                eprintln!("rule has unknown match {:?}", r);
                continue;
            }

            let rule = match m.match_type {
                MatchType::From => "from:",
                // TODO(wathiede): something more specific?
                MatchType::Sender => "from:",
                MatchType::To => "to:",
                MatchType::Cc => "to:",
                MatchType::Subject => "subject:",
                MatchType::ListId => "List-ID:",
                MatchType::Body => "",
                // TODO(wathiede): these will probably require adding fields to notmuch
                // index. Handle them later.
                MatchType::DeliveredTo
                | MatchType::XForwardedTo
                | MatchType::ReplyTo
                | MatchType::XOriginalTo
                | MatchType::XSpam => continue,
                MatchType::Unknown => unreachable!(),
            };
            // Preserve unread status if run with --remove-all
            lines.push(format!(
                r#"-unprocessed +{} +unread -- is:unread tag:unprocessed {}"{}""#,
                t, rule, m.needle
            ));
            lines.push(format!(
                // TODO(wathiede): this assumes `notmuch new` is configured to add
                // `tag:unprocessed` to all new mail.
                r#"-unprocessed +{} -- tag:unprocessed {}"{}""#,
                t, rule, m.needle
            ));
        }
    }
    lines.sort();
    for l in lines {
        writeln!(w, "{l}")?;
    }
    Ok(())
}

async fn load_sql(dsn: &str, rules: &[Rule]) -> anyhow::Result<()> {
    let pool = PgPool::connect(dsn).await?;
    println!("clearing email_rule table");
    sqlx::query!("DELETE FROM email_rule")
        .execute(&pool)
        .await?;

    for (order, rule) in rules.iter().enumerate() {
        println!("inserting {order}: {rule:?}");
        sqlx::query!(
            r#"
        INSERT INTO email_rule (sort_order, rule)
        VALUES ($1, $2)
            "#,
            order as i32,
            Json(rule) as _
        )
        .execute(&pool)
        .await?;
    }
    Ok(())
}
