use std::{
    convert::Infallible,
    hash::{DefaultHasher, Hash, Hasher},
    str::FromStr,
};

use build_info::{BuildInfo, VersionControl};
use letterbox_notmuch::SearchSummary;
use regex::{RegexBuilder, RegexSetBuilder};
use serde::{Deserialize, Serialize};
use tracing::debug;

#[derive(Serialize, Deserialize, Debug)]
pub struct SearchResult {
    pub summary: SearchSummary,
    pub query: String,
    pub page: usize,
    pub results_per_page: usize,
    pub total: usize,
}

#[derive(Serialize, Deserialize, Debug, strum_macros::Display)]
pub enum WebsocketMessage {
    RefreshMessages,
}

pub mod urls {
    pub const MOUNT_POINT: &'static str = "/api";
    pub fn view_original(host: Option<&str>, id: &str) -> String {
        if let Some(host) = host {
            format!("//{host}/api/original/{id}")
        } else {
            format!("/api/original/{id}")
        }
    }
    pub fn cid_prefix(host: Option<&str>, cid: &str) -> String {
        if let Some(host) = host {
            format!("//{host}/api/cid/{cid}/")
        } else {
            format!("/api/cid/{cid}/")
        }
    }
    pub fn download_attachment(host: Option<&str>, id: &str, idx: &str, filename: &str) -> String {
        if let Some(host) = host {
            format!(
                "//{host}/api/download/attachment/{}/{}/{}",
                id, idx, filename
            )
        } else {
            format!("/api/download/attachment/{}/{}/{}", id, idx, filename)
        }
    }
}
pub fn build_version(bi: fn() -> &'static BuildInfo) -> String {
    fn commit(git: &Option<VersionControl>) -> String {
        let Some(VersionControl::Git(git)) = git else {
            return String::new();
        };
        let mut s = vec!["-".to_string(), git.commit_short_id.clone()];
        if let Some(branch) = &git.branch {
            s.push(format!(" ({branch})"));
        }
        s.join("")
    }
    let bi = bi();

    format!("v{}{}", bi.crate_info.version, commit(&bi.version_control)).to_string()
}
pub fn compute_color(data: &str) -> String {
    let mut hasher = DefaultHasher::new();
    data.hash(&mut hasher);
    format!("#{:06x}", hasher.finish() % (1 << 24))
}

#[derive(
    Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize,
)]
pub enum MatchType {
    From,
    Sender,
    To,
    Cc,
    Subject,
    ListId,
    DeliveredTo,
    XForwardedTo,
    ReplyTo,
    XOriginalTo,
    XSpam,
    Body,
    #[default]
    Unknown,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Match {
    pub match_type: MatchType,
    pub needle: String,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Rule {
    pub stop_on_match: bool,
    pub matches: Vec<Match>,
    pub tag: String,
}
impl Rule {
    pub fn is_match(&self, header_key: &str, header_value: &str) -> bool {
        let pats: Vec<_> = self
            .matches
            .iter()
            .filter_map(|m| match m.match_type {
                MatchType::To => Some("^(to|cc|bcc|x-original-to)$"),
                MatchType::From => Some("^from$"),
                MatchType::Sender => Some("^sender$"),
                MatchType::Subject => Some("^subject$"),
                MatchType::ListId => Some("^list-id$"),
                MatchType::XOriginalTo => Some("^x-original-to$"),
                MatchType::ReplyTo => Some("^reply-to$"),
                MatchType::XSpam => Some("^x-spam$"),
                MatchType::Body => None,
                c => panic!("TODO handle '{c:?}' match type"),
            })
            .collect();

        let set = RegexSetBuilder::new(&pats)
            .case_insensitive(true)
            .build()
            .expect("failed to compile regex for matches");
        let matches: Vec<_> = set.matches(header_key).into_iter().collect();
        if !matches.is_empty() {
            //info!("matched key '{header_key}' '{header_value}'");
            for m_idx in matches {
                let needle = regex::escape(&self.matches[m_idx].needle);
                let pat = RegexBuilder::new(&needle)
                    .case_insensitive(true)
                    .build()
                    .expect("failed to compile regex for needle");
                if pat.is_match(header_value) {
                    debug!("{header_key} matched {header_value} against {needle}");
                    return true;
                }
            }
        }
        false
    }
}

mod matches {
    // From https://linux.die.net/man/5/procmailrc
    // If the regular expression contains '^TO_' it will be substituted by '(^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope |Apparently(-Resent)?)-To):(.*[^-a-zA-Z0-9_.])?)'
    // If the regular expression contains '^TO' it will be substituted by '(^((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope |Apparently(-Resent)?)-To):(.*[^a-zA-Z])?)', which should catch all destination specifications containing a specific word.

    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::ListId,
                    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 unescape(s: &str) -> String {
    s.replace('\\', "")
}
pub fn cleanup_match(prefix: &str, s: &str) -> String {
    unescape(&s[prefix.len()..]).replace(".*", "")
}
