use std::collections::HashMap;
use std::sync::Mutex;

use crate::events::EventEmitter;

/// Result of matching a URL path against a route pattern.
#[derive(Debug, Clone, PartialEq)]
pub struct RouteMatch {
    /// Extracted parameters (e.g., `{":id": "123"}`).
    pub params: HashMap<String, String>,
    /// Parsed query parameters.
    pub query: HashMap<String, String>,
    /// The matched path.
    pub path: String,
}

/// Current route state.
#[derive(Debug, Clone)]
pub struct RouteState {
    pub current_path: String,
    pub params: HashMap<String, String>,
    pub query: HashMap<String, String>,
    pub previous_path: Option<String>,
}

/// A URL pattern-based router with history and parameter extraction.
///
/// # Pattern syntax
///
/// - Exact: `/dashboard`
/// - Wildcard: `/dashboard/*`
/// - Parameter: `/users/:id`
/// - Multiple params: `/users/:id/posts/:postId`
///
/// # Example
///
/// ```rust
/// use hypen_server::router::HypenRouter;
///
/// let router = HypenRouter::new();
///
/// // Navigate
/// router.push("/users/42?tab=profile");
///
/// let state = router.state();
/// assert_eq!(state.current_path, "/users/42");
/// assert_eq!(state.query.get("tab").map(String::as_str), Some("profile"));
///
/// // Match against a pattern
/// let m = router.match_path("/users/:id", "/users/42").unwrap();
/// assert_eq!(m.params["id"], "42");
/// ```
pub struct HypenRouter {
    inner: Mutex<RouterInner>,
    events: EventEmitter,
}

struct RouterInner {
    current_path: String,
    params: HashMap<String, String>,
    query: HashMap<String, String>,
    previous_path: Option<String>,
    history: Vec<String>,
}

impl HypenRouter {
    pub fn new() -> Self {
        Self {
            inner: Mutex::new(RouterInner {
                current_path: "/".to_string(),
                params: HashMap::new(),
                query: HashMap::new(),
                previous_path: None,
                history: vec!["/".to_string()],
            }),
            events: EventEmitter::new(),
        }
    }

    /// Navigate to a path, adding to history.
    pub fn push(&self, path: &str) {
        let (clean_path, query) = parse_path_and_query(path);
        let mut inner = self.inner.lock().unwrap();
        let prev = inner.current_path.clone();
        inner.previous_path = Some(prev);
        inner.current_path = clean_path.clone();
        inner.query = query;
        inner.params.clear();
        inner.history.push(clean_path);
        drop(inner);

        self.events.emit(
            crate::events::framework::ROUTE_CHANGED,
            &serde_json::json!({
                "path": path,
            }),
        );
    }

    /// Replace the current path without adding to history.
    pub fn replace(&self, path: &str) {
        let (clean_path, query) = parse_path_and_query(path);
        let mut inner = self.inner.lock().unwrap();
        inner.current_path = clean_path.clone();
        inner.query = query;
        inner.params.clear();
        if let Some(last) = inner.history.last_mut() {
            *last = clean_path;
        }
    }

    /// Get the current route state.
    pub fn state(&self) -> RouteState {
        let inner = self.inner.lock().unwrap();
        RouteState {
            current_path: inner.current_path.clone(),
            params: inner.params.clone(),
            query: inner.query.clone(),
            previous_path: inner.previous_path.clone(),
        }
    }

    /// Get the current path.
    pub fn current_path(&self) -> String {
        self.inner.lock().unwrap().current_path.clone()
    }

    /// Get current query parameters.
    pub fn query(&self) -> HashMap<String, String> {
        self.inner.lock().unwrap().query.clone()
    }

    /// Match a pattern against a given path.
    ///
    /// Returns `None` if the pattern doesn't match.
    pub fn match_path(&self, pattern: &str, path: &str) -> Option<RouteMatch> {
        let (clean_path, query) = parse_path_and_query(path);
        match_pattern(pattern, &clean_path).map(|params| RouteMatch {
            params,
            query,
            path: clean_path,
        })
    }

    /// Check if a pattern matches the current route.
    pub fn is_active(&self, pattern: &str) -> bool {
        let inner = self.inner.lock().unwrap();
        match_pattern(pattern, &inner.current_path).is_some()
    }

    /// Subscribe to route changes.
    pub fn on_navigate<F>(&self, handler: F) -> crate::events::SubscriptionId
    where
        F: Fn(&serde_json::Value) + Send + Sync + 'static,
    {
        self.events
            .on(crate::events::framework::ROUTE_CHANGED, handler)
    }

    /// Build a URL from a path and query parameters.
    pub fn build_url(path: &str, query: &HashMap<String, String>) -> String {
        if query.is_empty() {
            return path.to_string();
        }
        let qs: Vec<String> = query
            .iter()
            .map(|(k, v)| format!("{}={}", encode_uri_component(k), encode_uri_component(v)))
            .collect();
        format!("{path}?{}", qs.join("&"))
    }
}

impl Default for HypenRouter {
    fn default() -> Self {
        Self::new()
    }
}

/// Percent-encode a string for use in URL query parameters.
///
/// Encodes characters that are not unreserved (per RFC 3986) or that have
/// special meaning in the query component: space, `&`, `=`, `?`, `#`, `%`, `+`.
fn encode_uri_component(input: &str) -> String {
    let mut out = String::with_capacity(input.len());
    for byte in input.bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                out.push(byte as char)
            }
            _ => {
                out.push('%');
                out.push(char::from(HEX_CHARS[(byte >> 4) as usize]));
                out.push(char::from(HEX_CHARS[(byte & 0x0F) as usize]));
            }
        }
    }
    out
}

/// Decode a percent-encoded string.
fn decode_uri_component(input: &str) -> String {
    let mut out = Vec::with_capacity(input.len());
    let bytes = input.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'%' && i + 2 < bytes.len() {
            if let (Some(hi), Some(lo)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) {
                out.push((hi << 4) | lo);
                i += 3;
                continue;
            }
        }
        // Also treat '+' as space (common in query strings)
        if bytes[i] == b'+' {
            out.push(b' ');
        } else {
            out.push(bytes[i]);
        }
        i += 1;
    }
    String::from_utf8_lossy(&out).into_owned()
}

const HEX_CHARS: [u8; 16] = *b"0123456789ABCDEF";

fn hex_val(b: u8) -> Option<u8> {
    match b {
        b'0'..=b'9' => Some(b - b'0'),
        b'A'..=b'F' => Some(b - b'A' + 10),
        b'a'..=b'f' => Some(b - b'a' + 10),
        _ => None,
    }
}

/// Split a path into the clean path and query parameters.
///
/// Query parameter keys and values are percent-decoded so that the
/// in-memory representation contains the original characters.
fn parse_path_and_query(full_path: &str) -> (String, HashMap<String, String>) {
    if let Some((path, query_str)) = full_path.split_once('?') {
        let query = query_str
            .split('&')
            .filter_map(|pair| {
                let (k, v) = pair.split_once('=')?;
                Some((decode_uri_component(k), decode_uri_component(v)))
            })
            .collect();
        (path.to_string(), query)
    } else {
        (full_path.to_string(), HashMap::new())
    }
}

/// Match a route pattern against a path, extracting parameters.
fn match_pattern(pattern: &str, path: &str) -> Option<HashMap<String, String>> {
    let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
    let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();

    // Wildcard: /foo/* matches /foo/anything/here
    if pattern_parts.last() == Some(&"*") {
        let prefix = &pattern_parts[..pattern_parts.len() - 1];
        if path_parts.len() < prefix.len() {
            return None;
        }
        let mut params = HashMap::new();
        for (pp, rp) in prefix.iter().zip(path_parts.iter()) {
            if let Some(name) = pp.strip_prefix(':') {
                params.insert(name.to_string(), rp.to_string());
            } else if pp != rp {
                return None;
            }
        }
        return Some(params);
    }

    if pattern_parts.len() != path_parts.len() {
        return None;
    }

    let mut params = HashMap::new();
    for (pp, rp) in pattern_parts.iter().zip(path_parts.iter()) {
        if let Some(name) = pp.strip_prefix(':') {
            params.insert(name.to_string(), rp.to_string());
        } else if pp != rp {
            return None;
        }
    }

    Some(params)
}

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

    #[test]
    fn test_push_and_state() {
        let router = HypenRouter::new();
        router.push("/users/42?tab=profile");

        let state = router.state();
        assert_eq!(state.current_path, "/users/42");
        assert_eq!(state.query.get("tab").map(String::as_str), Some("profile"));
        assert_eq!(state.previous_path, Some("/".to_string()));
    }

    #[test]
    fn test_replace() {
        let router = HypenRouter::new();
        router.push("/page1");
        router.replace("/page2");

        let state = router.state();
        assert_eq!(state.current_path, "/page2");
        // previous_path should still be "/" (from before push), not "page1"
        // because replace doesn't update previous_path
        assert_eq!(state.previous_path, Some("/".to_string()));
    }

    #[test]
    fn test_match_exact() {
        let router = HypenRouter::new();
        let m = router.match_path("/dashboard", "/dashboard");
        assert!(m.is_some());
        assert!(m.unwrap().params.is_empty());
    }

    #[test]
    fn test_match_params() {
        let router = HypenRouter::new();
        let m = router
            .match_path("/users/:id/posts/:postId", "/users/42/posts/99")
            .unwrap();
        assert_eq!(m.params["id"], "42");
        assert_eq!(m.params["postId"], "99");
    }

    #[test]
    fn test_match_wildcard() {
        let router = HypenRouter::new();
        assert!(router.match_path("/api/*", "/api/users/list").is_some());
        assert!(router.match_path("/api/*", "/api").is_some());
        assert!(router.match_path("/api/*", "/other").is_none());
    }

    #[test]
    fn test_no_match() {
        let router = HypenRouter::new();
        assert!(router.match_path("/users/:id", "/posts/42").is_none());
        assert!(router.match_path("/users/:id", "/users/42/extra").is_none());
    }

    #[test]
    fn test_is_active() {
        let router = HypenRouter::new();
        router.push("/users/42");

        assert!(router.is_active("/users/:id"));
        assert!(!router.is_active("/posts/:id"));
    }

    #[test]
    fn test_query_parsing() {
        let (path, query) = parse_path_and_query("/search?q=hello&page=2");
        assert_eq!(path, "/search");
        assert_eq!(query["q"], "hello");
        assert_eq!(query["page"], "2");
    }

    #[test]
    fn test_build_url() {
        let mut query = HashMap::new();
        query.insert("tab".to_string(), "profile".to_string());

        let url = HypenRouter::build_url("/users/42", &query);
        assert_eq!(url, "/users/42?tab=profile");
    }

    #[test]
    fn test_build_url_no_query() {
        let url = HypenRouter::build_url("/home", &HashMap::new());
        assert_eq!(url, "/home");
    }

    #[test]
    fn test_build_url_encodes_special_chars() {
        let mut query = HashMap::new();
        query.insert("msg".to_string(), "hello world".to_string());
        query.insert("a&b".to_string(), "1=2".to_string());

        let url = HypenRouter::build_url("/search", &query);
        // Both keys and values must be percent-encoded
        assert!(url.contains("msg=hello%20world"));
        assert!(url.contains("a%26b=1%3D2"));
        assert!(url.starts_with("/search?"));
    }

    #[test]
    fn test_parse_decodes_encoded_query() {
        let (path, query) = parse_path_and_query("/search?msg=hello%20world&a%26b=1%3D2");
        assert_eq!(path, "/search");
        assert_eq!(query.get("msg").map(String::as_str), Some("hello world"));
        assert_eq!(query.get("a&b").map(String::as_str), Some("1=2"));
    }

    #[test]
    fn test_encode_decode_roundtrip() {
        let original = "hello world&foo=bar?baz#qux%plus+sign";
        let encoded = encode_uri_component(original);
        let decoded = decode_uri_component(&encoded);
        assert_eq!(decoded, original);
        // Encoded form must not contain raw special characters
        assert!(!encoded.contains(' '));
        assert!(!encoded.contains('&'));
        assert!(!encoded.contains('='));
        assert!(!encoded.contains('?'));
        assert!(!encoded.contains('#'));
    }

    #[test]
    fn test_plus_decodes_to_space() {
        let (path, query) = parse_path_and_query("/search?q=hello+world");
        assert_eq!(path, "/search");
        assert_eq!(query.get("q").map(String::as_str), Some("hello world"));
    }

    #[test]
    fn test_on_navigate() {
        use std::sync::atomic::{AtomicI32, Ordering};
        use std::sync::Arc;

        let router = HypenRouter::new();
        let count = Arc::new(AtomicI32::new(0));
        let count_clone = count.clone();

        router.on_navigate(move |_| {
            count_clone.fetch_add(1, Ordering::SeqCst);
        });

        router.push("/a");
        router.push("/b");

        assert_eq!(count.load(Ordering::SeqCst), 2);
    }
}
