use std::collections::HashMap;

use crate::router::Method;

enum Node {
    Text(String),
    Param(String),
}

impl From<&str> for Node {
    fn from(s: &str) -> Node {
        if s.starts_with('{') && s.ends_with('}') {
            return Node::Param(s.trim_start_matches('{').trim_end_matches('}').to_owned());
        }

        Node::Text(s.to_owned())
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum MatchResult {
    Mismatch,
    Params(HashMap<String, String>),
}

pub struct Pattern {
    method: Method,
    parts: Vec<Node>,
}

impl Pattern {
    pub fn new(method: Method, path: &str) -> Self {
        assert!(!path.contains('#'));
        assert!(!path.contains('?'));

        let clean = path.trim().trim_matches('/');
        let raw_parts: Vec<_> = clean.split('/').collect();
        let parts: Vec<_> = raw_parts.into_iter().map(Node::from).collect();

        Pattern { method, parts }
    }

    pub fn validate_and_extract_params(&self, method: Method, url: &str) -> MatchResult {
        if self.method != method {
            return MatchResult::Mismatch;
        }

        let url_without_query = url.split_once('?').map(|(u, _)| u).unwrap_or(url);
        let url_without_fragments = url_without_query
            .split_once('#')
            .map(|(u, _)| u)
            .unwrap_or(url_without_query);
        let parts: Vec<_> = url_without_fragments
            .trim()
            .trim_matches('/')
            .split('/')
            .collect();

        if parts.len() != self.parts.len() {
            return MatchResult::Mismatch;
        }

        let mut params: HashMap<String, String> = HashMap::new();
        for (&part, pattern_part) in parts.iter().zip(&self.parts) {
            match pattern_part {
                Node::Text(text) => {
                    if part != text {
                        return MatchResult::Mismatch;
                    }
                }
                Node::Param(param_key) => {
                    params.insert(param_key.to_owned(), part.to_owned());
                }
            }
        }

        MatchResult::Params(params)
    }
}

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

    #[test]
    fn can_match_simple_urls() {
        let pattern = Pattern::new(Method::Get, "/product/list");
        let valid_urls = vec!["/product/list", "product/list", "product/list/"];
        for url in valid_urls {
            let res = pattern.validate_and_extract_params(Method::Get, url);
            assert_eq!(res, MatchResult::Params(HashMap::new()));
        }
    }

    #[test]
    fn checks_method_during_matching() {
        let pattern = Pattern::new(Method::Get, "/product/list");
        let res = pattern.validate_and_extract_params(Method::Get, "/product/list");
        assert_eq!(res, MatchResult::Params(HashMap::new()));
        let res = pattern.validate_and_extract_params(Method::Post, "/product/list");
        assert_eq!(res, MatchResult::Mismatch);
    }

    #[test]
    fn ignores_query_and_fragments_during_matching() {
        let pattern = Pattern::new(Method::Get, "/product/list");
        let valid_urls = vec![
            "/product/list?name=cheese",
            "/product/list#title",
            "/product/list?name=chicken&type=meat#subtitle",
        ];
        for url in valid_urls {
            let res = pattern.validate_and_extract_params(Method::Get, url);
            assert_eq!(res, MatchResult::Params(HashMap::new()), "url={url}");
        }
    }

    #[test]
    fn fails_for_invalid_urls() {
        let pattern = Pattern::new(Method::Get, "/product/list");
        let invalid_urls = vec!["/product/lists", "/product/info", "/meals/list"];
        for url in invalid_urls {
            let res = pattern.validate_and_extract_params(Method::Get, url);
            assert_eq!(res, MatchResult::Mismatch);
        }
    }

    #[test]
    fn correctly_extracts_params() {
        let pattern = Pattern::new(Method::Get, "/meal/{meal_id}/product/{product_id}/edit");
        let res = pattern.validate_and_extract_params(Method::Get, "/meal/13/product/420/edit");
        match res {
            MatchResult::Params(params) => {
                assert_eq!(params.get("meal_id"), Some(&"13".to_owned()));
                assert_eq!(params.get("product_id"), Some(&"420".to_owned()));
            }
            _ => panic!("failed to extract params"),
        }
    }
}
