use keepassxc_proxy_lib::Login;

#[derive(Debug, PartialEq)]
pub enum FilterOp {
    Exact(String, String),
    Contains(String, String),
}

#[derive(Debug, PartialEq)]
pub enum FilterError {
    InvalidSyntax(String),
    UnknownField(String),
}

impl std::fmt::Display for FilterError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidSyntax(s) => {
                write!(f, "invalid filter syntax: {s} (use field=value or field~value)")
            }
            Self::UnknownField(s) => write!(f, "unknown field: {s} (valid: name, login, uuid, group)"),
        }
    }
}

impl std::error::Error for FilterError {}

const VALID_FIELDS: &[&str] = &["name", "login", "uuid", "group"];

pub fn parse_filter(s: &str) -> Result<FilterOp, FilterError> {
    let (field, value, is_contains) = if let Some((f, v)) = s.split_once('~') {
        (f, v, true)
    } else if let Some((f, v)) = s.split_once('=') {
        (f, v, false)
    } else {
        return Err(FilterError::InvalidSyntax(s.to_string()));
    };

    if !VALID_FIELDS.contains(&field) {
        return Err(FilterError::UnknownField(field.to_string()));
    }

    if is_contains {
        Ok(FilterOp::Contains(field.to_string(), value.to_lowercase()))
    } else {
        Ok(FilterOp::Exact(field.to_string(), value.to_string()))
    }
}

pub fn parse_filters(filters: &[String]) -> Result<Vec<FilterOp>, FilterError> {
    filters.iter().map(|s| parse_filter(s)).collect()
}

fn get_field<'a>(login: &'a Login, field: &str) -> Option<&'a str> {
    match field {
        "name" => Some(&login.name),
        "login" => Some(&login.login),
        "uuid" => Some(&login.uuid),
        "group" => Some(&login.group),
        _ => None,
    }
}

fn matches_filter(login: &Login, filter: &FilterOp) -> bool {
    match filter {
        FilterOp::Exact(field, value) => get_field(login, field).is_some_and(|v| v == value),
        FilterOp::Contains(field, value) => {
            get_field(login, field).is_some_and(|v| v.to_lowercase().contains(value))
        }
    }
}

pub fn apply_filters(logins: Vec<Login>, filters: &[FilterOp]) -> Vec<Login> {
    if filters.is_empty() {
        return logins;
    }
    logins
        .into_iter()
        .filter(|login| filters.iter().all(|f| matches_filter(login, f)))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_login(name: &str, login: &str, uuid: &str, group: &str) -> Login {
        Login {
            name: name.to_string(),
            login: login.to_string(),
            password: "pw".to_string(),
            uuid: uuid.to_string(),
            group: group.to_string(),
            string_fields: vec![],
        }
    }

    mod parse_filter {
        use super::*;

        #[test]
        fn exact_match() {
            assert_eq!(
                parse_filter("name=foo").unwrap(),
                FilterOp::Exact("name".to_string(), "foo".to_string())
            );
        }

        #[test]
        fn contains_match() {
            assert_eq!(
                parse_filter("name~foo").unwrap(),
                FilterOp::Contains("name".to_string(), "foo".to_string())
            );
        }

        #[test]
        fn contains_lowercases_value() {
            assert_eq!(
                parse_filter("name~FoO").unwrap(),
                FilterOp::Contains("name".to_string(), "foo".to_string())
            );
        }

        #[test]
        fn exact_preserves_case() {
            assert_eq!(
                parse_filter("name=FoO").unwrap(),
                FilterOp::Exact("name".to_string(), "FoO".to_string())
            );
        }

        #[test]
        fn invalid_syntax_no_operator() {
            assert_eq!(
                parse_filter("namefoo").unwrap_err(),
                FilterError::InvalidSyntax("namefoo".to_string())
            );
        }

        #[test]
        fn unknown_field() {
            assert_eq!(
                parse_filter("password=foo").unwrap_err(),
                FilterError::UnknownField("password".to_string())
            );
        }

        #[test]
        fn all_valid_fields() {
            for field in VALID_FIELDS {
                assert!(parse_filter(&format!("{field}=test")).is_ok());
                assert!(parse_filter(&format!("{field}~test")).is_ok());
            }
        }

        #[test]
        fn empty_value() {
            assert_eq!(
                parse_filter("name=").unwrap(),
                FilterOp::Exact("name".to_string(), String::new())
            );
        }

        #[test]
        fn value_with_operator_chars() {
            assert_eq!(
                parse_filter("name=foo=bar").unwrap(),
                FilterOp::Exact("name".to_string(), "foo=bar".to_string())
            );
            assert_eq!(
                parse_filter("name~foo~bar").unwrap(),
                FilterOp::Contains("name".to_string(), "foo~bar".to_string())
            );
        }
    }

    mod apply_filters {
        use super::*;

        fn test_logins() -> Vec<Login> {
            vec![
                make_login("GitHub", "user1", "uuid1", "Work"),
                make_login("GitLab", "user2", "uuid2", "Work"),
                make_login("Gmail", "personal@gmail.com", "uuid3", "Personal"),
            ]
        }

        #[test]
        fn no_filters_returns_all() {
            let logins = test_logins();
            let result = apply_filters(logins.clone(), &[]);
            assert_eq!(result.len(), 3);
        }

        #[test]
        fn exact_match_name() {
            let logins = test_logins();
            let filters = vec![FilterOp::Exact("name".to_string(), "GitHub".to_string())];
            let result = apply_filters(logins, &filters);
            assert_eq!(result.len(), 1);
            assert_eq!(result[0].name, "GitHub");
        }

        #[test]
        fn exact_match_case_sensitive() {
            let logins = test_logins();
            let filters = vec![FilterOp::Exact("name".to_string(), "github".to_string())];
            let result = apply_filters(logins, &filters);
            assert!(result.is_empty());
        }

        #[test]
        fn contains_case_insensitive() {
            let logins = test_logins();
            let filters = vec![FilterOp::Contains("name".to_string(), "git".to_string())];
            let result = apply_filters(logins, &filters);
            assert_eq!(result.len(), 2);
            assert!(result.iter().all(|l| l.name.to_lowercase().contains("git")));
        }

        #[test]
        fn multiple_filters_and() {
            let logins = test_logins();
            let filters = vec![
                FilterOp::Exact("group".to_string(), "Work".to_string()),
                FilterOp::Contains("name".to_string(), "hub".to_string()),
            ];
            let result = apply_filters(logins, &filters);
            assert_eq!(result.len(), 1);
            assert_eq!(result[0].name, "GitHub");
        }

        #[test]
        fn filter_on_login_field() {
            let logins = test_logins();
            let filters = vec![FilterOp::Contains("login".to_string(), "gmail".to_string())];
            let result = apply_filters(logins, &filters);
            assert_eq!(result.len(), 1);
            assert_eq!(result[0].name, "Gmail");
        }

        #[test]
        fn filter_on_uuid() {
            let logins = test_logins();
            let filters = vec![FilterOp::Exact("uuid".to_string(), "uuid2".to_string())];
            let result = apply_filters(logins, &filters);
            assert_eq!(result.len(), 1);
            assert_eq!(result[0].name, "GitLab");
        }

        #[test]
        fn filter_on_group() {
            let logins = test_logins();
            let filters = vec![FilterOp::Exact("group".to_string(), "Work".to_string())];
            let result = apply_filters(logins, &filters);
            assert_eq!(result.len(), 2);
            assert!(result.iter().all(|l| l.group == "Work"));
        }

        #[test]
        fn no_matches_returns_empty() {
            let logins = test_logins();
            let filters = vec![FilterOp::Exact("name".to_string(), "Nonexistent".to_string())];
            let result = apply_filters(logins, &filters);
            assert!(result.is_empty());
        }
    }
}
