use athena_auth::core_paths;
use athena_auth::{AthenaAuth, AuthRoute, DatabaseAdapter, HttpMethod};

#[derive(Debug, Clone, Default)]
pub(crate) struct AuthRouteLabelResolver {
    entries: Vec<CompiledAuthRouteLabel>,
}

#[derive(Debug, Clone)]
struct CompiledAuthRouteLabel {
    method: &'static str,
    template: String,
    segments: Vec<RouteSegment>,
    dynamic_segment_count: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum RouteSegment {
    Static(String),
    Dynamic,
}

impl AuthRouteLabelResolver {
    pub(crate) fn register_auth<DB: DatabaseAdapter>(&mut self, auth: &AthenaAuth<DB>) {
        let config = auth.config();
        let disabled_paths = &config.disabled_paths;
        let mut mount_paths = vec![normalize_path(config.base_path.as_str())];
        let public_mount_path = normalize_path(config.effective_public_base_path());
        if !mount_paths.contains(&public_mount_path) {
            mount_paths.push(public_mount_path);
        }

        for route in core_route_specs() {
            if disabled_paths.contains(&route.path) {
                continue;
            }
            for mount_path in &mount_paths {
                self.insert_template(
                    route.method.clone(),
                    join_mount_path(mount_path, route.path.as_str()),
                );
            }
        }

        for plugin in auth.plugins() {
            for route in plugin.routes() {
                if disabled_paths.contains(&route.path) {
                    continue;
                }
                for mount_path in &mount_paths {
                    self.insert_template(
                        route.method.clone(),
                        join_mount_path(mount_path, route.path.as_str()),
                    );
                }
            }
        }

        self.entries.sort_by(|left, right| {
            left.dynamic_segment_count
                .cmp(&right.dynamic_segment_count)
                .then_with(|| right.segments.len().cmp(&left.segments.len()))
                .then_with(|| right.template.len().cmp(&left.template.len()))
                .then_with(|| left.template.cmp(&right.template))
                .then_with(|| left.method.cmp(&right.method))
        });
    }

    pub(crate) fn resolve<'a>(&'a self, method: &str, request_path: &str) -> Option<&'a str> {
        let normalized_path = normalize_path(request_path);
        self.entries
            .iter()
            .find(|entry| entry.method == method && entry.matches(normalized_path.as_str()))
            .map(|entry| entry.template.as_str())
    }

    fn insert_template(&mut self, method: HttpMethod, template: String) {
        let normalized_template = normalize_path(template.as_str());
        let method = http_method_name(&method);
        if self
            .entries
            .iter()
            .any(|entry| entry.method == method && entry.template == normalized_template)
        {
            return;
        }

        self.entries
            .push(CompiledAuthRouteLabel::new(method, normalized_template));
    }
}

impl CompiledAuthRouteLabel {
    fn new(method: &'static str, template: String) -> Self {
        let segments = split_segments(template.as_str())
            .iter()
            .map(|segment| {
                if is_dynamic_segment(segment.as_str()) {
                    RouteSegment::Dynamic
                } else {
                    RouteSegment::Static(segment.clone())
                }
            })
            .collect::<Vec<_>>();
        let dynamic_segment_count = segments
            .iter()
            .filter(|segment| matches!(segment, RouteSegment::Dynamic))
            .count();

        Self {
            method,
            template,
            segments,
            dynamic_segment_count,
        }
    }

    fn matches(&self, request_path: &str) -> bool {
        let request_segments = split_segments(request_path);
        if self.segments.len() != request_segments.len() {
            return false;
        }

        self.segments.iter().zip(request_segments.iter()).all(
            |(template_segment, request_segment)| match template_segment {
                RouteSegment::Static(static_segment) => static_segment == request_segment,
                RouteSegment::Dynamic => !request_segment.is_empty(),
            },
        )
    }
}

fn core_route_specs() -> Vec<AuthRoute> {
    vec![
        AuthRoute::get(core_paths::OK, "ok"),
        AuthRoute::get(core_paths::ERROR, "error"),
        AuthRoute::get(core_paths::HEALTH, "health"),
        AuthRoute::get(core_paths::OPENAPI_SPEC, "openapi_spec"),
        AuthRoute::post(core_paths::UPDATE_USER, "update_user"),
        AuthRoute::post(core_paths::DELETE_USER, "delete_user"),
        AuthRoute::delete(core_paths::DELETE_USER, "delete_user_delete"),
        AuthRoute::post(core_paths::CHANGE_EMAIL, "change_email"),
        AuthRoute::get(core_paths::DELETE_USER_CALLBACK, "delete_user_callback"),
    ]
}

fn http_method_name(method: &HttpMethod) -> &'static str {
    match method {
        HttpMethod::Get => "GET",
        HttpMethod::Post => "POST",
        HttpMethod::Put => "PUT",
        HttpMethod::Delete => "DELETE",
        HttpMethod::Patch => "PATCH",
        HttpMethod::Options => "OPTIONS",
        HttpMethod::Head => "HEAD",
    }
}

fn join_mount_path(mount_path: &str, route_path: &str) -> String {
    let mount_path = normalize_path(mount_path);
    let route_path = normalize_path(route_path);
    if mount_path == "/" {
        route_path
    } else if route_path == "/" {
        mount_path
    } else {
        format!("{mount_path}{route_path}")
    }
}

fn normalize_path(path: &str) -> String {
    let trimmed = path.trim();
    if trimmed.is_empty() || trimmed == "/" {
        return "/".to_string();
    }

    let with_leading_slash = if trimmed.starts_with('/') {
        trimmed.to_string()
    } else {
        format!("/{trimmed}")
    };
    let normalized = with_leading_slash.trim_end_matches('/');
    if normalized.is_empty() {
        "/".to_string()
    } else {
        normalized.to_string()
    }
}

fn split_segments(path: &str) -> Vec<String> {
    let normalized = normalize_path(path);
    normalized
        .trim_matches('/')
        .split('/')
        .filter(|segment| !segment.is_empty())
        .map(str::to_string)
        .collect()
}

fn is_dynamic_segment(segment: &str) -> bool {
    segment.starts_with('{') && segment.ends_with('}') && segment.len() > 2
}

#[cfg(test)]
mod tests {
    use super::*;
    use athena_auth::plugins::OAuthPlugin;
    use athena_auth::{AuthBuilder, AuthConfig, MemoryDatabaseAdapter};

    #[tokio::test]
    async fn resolves_dynamic_plugin_route_under_base_path() {
        let auth = AuthBuilder::new(AuthConfig::new("route-label-test-secret-32-characters"))
            .database(MemoryDatabaseAdapter::new())
            .plugin(OAuthPlugin::new())
            .build()
            .await
            .expect("auth should build");

        let mut resolver = AuthRouteLabelResolver::default();
        resolver.register_auth(&auth);

        assert_eq!(
            resolver.resolve("GET", "/api/auth/callback/github"),
            Some("/api/auth/callback/{provider}")
        );
        assert_eq!(
            resolver.resolve("POST", "/api/auth/sign-in/social"),
            Some("/api/auth/sign-in/social")
        );
    }

    #[tokio::test]
    async fn resolves_public_base_path_for_root_mount() {
        let auth = AuthBuilder::new(
            AuthConfig::new("route-label-test-secret-32-characters")
                .base_path("/")
                .public_base_path("/api/auth"),
        )
        .database(MemoryDatabaseAdapter::new())
        .plugin(OAuthPlugin::new())
        .build()
        .await
        .expect("auth should build");

        let mut resolver = AuthRouteLabelResolver::default();
        resolver.register_auth(&auth);

        assert_eq!(
            resolver.resolve("GET", "/api/auth/callback/github"),
            Some("/api/auth/callback/{provider}")
        );
        assert_eq!(
            resolver.resolve("GET", "/callback/github"),
            Some("/callback/{provider}")
        );
    }
}
