use std::{convert::Infallible, io::Write, str::FromStr};

#[derive(Debug, Default)]
enum MatchType {
    From,
    Sender,
    To,
    Cc,
    Subject,
    List,
    DeliveredTo,
    XForwardedTo,
    ReplyTo,
    XOriginalTo,
    XSpam,
    Body,
    #[default]
    Unknown,
}
#[derive(Debug, Default)]
struct Match {
    match_type: MatchType,
    needle: String,
}

#[derive(Debug, Default)]
struct Rule {
    matches: Vec<Match>,
    tags: Vec<String>,
}

fn unescape(s: &str) -> String {
    s.replace('\\', "")
}

fn cleanup_match(prefix: &str, s: &str) -> String {
    unescape(&s[prefix.len()..]).replace(".*", "")
}

mod matches {
    pub const TO: &'static str = "TO";
    pub const CC: &'static str = "Cc";
    pub const TOCC: &'static str = "(TO|Cc)";
    pub const FROM: &'static str = "From";
    pub const SENDER: &'static str = "Sender";
    pub const SUBJECT: &'static str = "Subject";
    pub const DELIVERED_TO: &'static str = "Delivered-To";
    pub const X_FORWARDED_TO: &'static str = "X-Forwarded-To";
    pub const REPLY_TO: &'static str = "Reply-To";
    pub const X_ORIGINAL_TO: &'static str = "X-Original-To";
    pub const LIST_ID: &'static str = "List-ID";
    pub const X_SPAM: &'static str = "X-Spam";
    pub const X_SPAM_FLAG: &'static str = "X-Spam-Flag";
}

impl FromStr for Match {
    type Err = Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Examples:
        //   "* 1^0 ^TOsonyrewards.com@xinu.tv"
        //   "* ^TOsonyrewards.com@xinu.tv"
        let mut it = s.split_whitespace().skip(1);
        let mut needle = it.next().unwrap();
        if needle == "1^0" {
            needle = it.next().unwrap();
        }
        let mut needle = vec![needle];
        needle.extend(it);
        let needle = needle.join(" ");
        let first = needle.chars().nth(0).unwrap_or(' ');
        use matches::*;
        if first == '^' {
            let needle = &needle[1..];
            if needle.starts_with(TO) {
                return Ok(Match {
                    match_type: MatchType::To,
                    needle: cleanup_match(TO, needle),
                });
            } else if needle.starts_with(FROM) {
                return Ok(Match {
                    match_type: MatchType::From,
                    needle: cleanup_match(FROM, needle),
                });
            } else if needle.starts_with(CC) {
                return Ok(Match {
                    match_type: MatchType::Cc,
                    needle: cleanup_match(CC, needle),
                });
            } else if needle.starts_with(TOCC) {
                return Ok(Match {
                    match_type: MatchType::To,
                    needle: cleanup_match(TOCC, needle),
                });
            } else if needle.starts_with(SENDER) {
                return Ok(Match {
                    match_type: MatchType::Sender,
                    needle: cleanup_match(SENDER, needle),
                });
            } else if needle.starts_with(SUBJECT) {
                return Ok(Match {
                    match_type: MatchType::Subject,
                    needle: cleanup_match(SUBJECT, needle),
                });
            } else if needle.starts_with(X_ORIGINAL_TO) {
                return Ok(Match {
                    match_type: MatchType::XOriginalTo,
                    needle: cleanup_match(X_ORIGINAL_TO, needle),
                });
            } else if needle.starts_with(LIST_ID) {
                return Ok(Match {
                    match_type: MatchType::List,
                    needle: cleanup_match(LIST_ID, needle),
                });
            } else if needle.starts_with(REPLY_TO) {
                return Ok(Match {
                    match_type: MatchType::ReplyTo,
                    needle: cleanup_match(REPLY_TO, needle),
                });
            } else if needle.starts_with(X_SPAM_FLAG) {
                return Ok(Match {
                    match_type: MatchType::XSpam,
                    needle: '*'.to_string(),
                });
            } else if needle.starts_with(X_SPAM) {
                return Ok(Match {
                    match_type: MatchType::XSpam,
                    needle: '*'.to_string(),
                });
            } else if needle.starts_with(DELIVERED_TO) {
                return Ok(Match {
                    match_type: MatchType::DeliveredTo,
                    needle: cleanup_match(DELIVERED_TO, needle),
                });
            } else if needle.starts_with(X_FORWARDED_TO) {
                return Ok(Match {
                    match_type: MatchType::XForwardedTo,
                    needle: cleanup_match(X_FORWARDED_TO, needle),
                });
            } else {
                unreachable!("needle: '{needle}'")
            }
        } else {
            return Ok(Match {
                match_type: MatchType::Body,
                needle: cleanup_match("", &needle),
            });
        }
    }
}

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 {
            for t in &r.tags {
                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::List => "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(())
}

fn main() -> anyhow::Result<()> {
    let input = "/home/wathiede/dotfiles/procmailrc";
    let mut rules = Vec::new();
    let mut cur_rule = Rule::default();
    for l in std::fs::read_to_string(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
            }
            '*' => {
                // add to current rule
                let m: Match = l.parse()?;
                cur_rule.matches.push(m);
            }
            '.' => {
                // delivery to folder
                cur_rule.tags.push(cleanup_match(
                    "",
                    &l.replace('.', "/")
                        .replace(' ', "")
                        .trim_matches('/')
                        .to_string(),
                ));
                rules.push(cur_rule);
                cur_rule = Rule::default();
            }
            '|' => cur_rule = Rule::default(), // external command
            '$' => {
                // TODO(wathiede): tag messages with no other tag as 'inbox'
                cur_rule.tags.push(cleanup_match("", "inbox"));
                rules.push(cur_rule);
                cur_rule = Rule::default();
            } // variable, should only be $DEFAULT in my config
            _ => panic!("Unhandled first character '{}' {}", first, l),
        }
    }
    notmuch_from_rules(std::io::stdout(), &rules)?;
    Ok(())
}
