use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use axum::extract::connect_info::ConnectInfo;
use axum::extract::{Request, State};
use axum::http::{HeaderMap, Method, StatusCode};
use axum::middleware::Next;
use axum::response::Response;
use ipnet::IpNet;
use sha2::{Digest, Sha256};

use secrecy::{ExposeSecret, SecretString};
use subtle::ConstantTimeEq;

use crate::lfd::http;
use crate::lfd::http::state::HttpState;

const THROTTLE_WINDOW: Duration = Duration::from_secs(60);
const MAX_BEARER_TOKEN_BYTES: usize = 4096;

/// Bearer-token auth for lfd connections.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum AuthProvider {
    Bearer { session_token: SecretString },
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ParsedToken<'a> {
    Missing,
    Malformed,
    Present(&'a str),
}

impl AuthProvider {
    pub async fn validate(
        &self,
        provided_token: Option<&str>,
        _source: IpAddr,
    ) -> Result<(), (StatusCode, &'static str)> {
        match self {
            AuthProvider::Bearer { session_token } => {
                authorize_expected_token(session_token, provided_token)
            }
        }
    }

    pub fn local_admin_authorized(&self, provided_token: Option<&str>, source: IpAddr) -> bool {
        if !source.is_loopback() {
            return false;
        }
        let Some(token) = provided_token else {
            return false;
        };

        match self {
            AuthProvider::Bearer { session_token } => token_matches(session_token, token),
        }
    }
}

#[derive(Clone, Debug, Default)]
pub struct AuthFailureThrottle {
    buckets: Arc<Mutex<HashMap<AuthThrottleKey, AuthThrottleBucket>>>,
}

impl AuthFailureThrottle {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn is_throttled(&self, key: &AuthThrottleKey, limit: u32) -> bool {
        let Ok(mut buckets) = self.buckets.lock() else {
            return false;
        };
        let now = Instant::now();
        buckets.retain(|_, bucket| now.duration_since(bucket.window_started) < THROTTLE_WINDOW);
        buckets
            .get(key)
            .is_some_and(|bucket| bucket.failures_in_window >= limit)
    }

    pub fn record_failure(&self, key: AuthThrottleKey, limit: u32) -> bool {
        let Ok(mut buckets) = self.buckets.lock() else {
            return false;
        };
        let now = Instant::now();
        buckets.retain(|_, bucket| now.duration_since(bucket.window_started) < THROTTLE_WINDOW);
        let bucket = buckets.entry(key).or_insert(AuthThrottleBucket {
            window_started: now,
            failures_in_window: 0,
        });
        if now.duration_since(bucket.window_started) >= THROTTLE_WINDOW {
            bucket.window_started = now;
            bucket.failures_in_window = 0;
        }
        bucket.failures_in_window = bucket.failures_in_window.saturating_add(1);
        bucket.failures_in_window > limit
    }
}

#[derive(Debug)]
struct AuthThrottleBucket {
    window_started: Instant,
    failures_in_window: u32,
}

#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct AuthThrottleKey {
    source: IpAddr,
    auth_context_hash: String,
    endpoint_group: &'static str,
}

impl AuthThrottleKey {
    pub fn new(source: IpAddr, auth_context_hash: String, endpoint_group: &'static str) -> Self {
        Self {
            source,
            auth_context_hash,
            endpoint_group,
        }
    }
}

/// Axum middleware that enforces auth on all requests.
pub async fn auth_middleware(
    State(state): State<HttpState>,
    headers: HeaderMap,
    request: Request,
    next: Next,
) -> Response {
    let provided_token = extract_token(&headers);
    let throttle_limit = state.http_security.auth_failures_per_minute;
    let source = resolved_source(&request, &headers, &state.http_security.trusted_proxy_cidrs);
    let endpoint_group = endpoint_group(request.method(), request.uri().path());
    let throttle_key =
        AuthThrottleKey::new(source, auth_context_hash(provided_token), endpoint_group);

    if state
        .auth_failure_throttle
        .is_throttled(&throttle_key, throttle_limit)
    {
        return throttled_response();
    }

    let auth_result = match provided_token {
        ParsedToken::Malformed => Err(malformed_token_error(&state.auth, source)),
        ParsedToken::Missing => state.auth.validate(None, source).await,
        ParsedToken::Present(token) => state.auth.validate(Some(token), source).await,
    };

    match auth_result {
        Ok(()) => next.run(request).await,
        Err((status, message)) => {
            if state
                .auth_failure_throttle
                .record_failure(throttle_key, throttle_limit)
            {
                tracing::warn!(
                    source = %source,
                    endpoint_group,
                    "auth failures exceeded limit; throttling"
                );
                throttled_response()
            } else {
                http::api_error_response(status, message)
            }
        }
    }
}

fn authorize_expected_token(
    expected_token: &SecretString,
    provided_token: Option<&str>,
) -> Result<(), (StatusCode, &'static str)> {
    match provided_token {
        Some(provided) if token_matches(expected_token, provided) => Ok(()),
        Some(_) => Err((StatusCode::UNAUTHORIZED, "invalid token")),
        None => Err((StatusCode::UNAUTHORIZED, "missing token")),
    }
}

fn token_matches(expected: &SecretString, provided: &str) -> bool {
    expected
        .expose_secret()
        .as_bytes()
        .ct_eq(provided.as_bytes())
        .into()
}

fn malformed_token_error(_auth: &AuthProvider, _source: IpAddr) -> (StatusCode, &'static str) {
    (StatusCode::UNAUTHORIZED, "malformed token")
}

pub(crate) fn extract_token(headers: &HeaderMap) -> ParsedToken<'_> {
    let Some(auth_value) = headers.get("authorization") else {
        return ParsedToken::Missing;
    };
    let Ok(auth) = auth_value.to_str() else {
        return ParsedToken::Malformed;
    };
    let Some((scheme, value)) = auth.split_once(' ') else {
        return ParsedToken::Malformed;
    };
    if !scheme.eq_ignore_ascii_case("bearer") {
        return ParsedToken::Malformed;
    }

    let token = value.trim();
    if token.is_empty() || token.len() > MAX_BEARER_TOKEN_BYTES {
        return ParsedToken::Malformed;
    }

    if token
        .chars()
        .any(|ch| ch.is_whitespace() || ch.is_control())
    {
        return ParsedToken::Malformed;
    }

    ParsedToken::Present(token)
}

pub fn bearer_token(headers: &HeaderMap) -> Option<String> {
    match extract_token(headers) {
        ParsedToken::Present(token) => Some(token.to_string()),
        ParsedToken::Missing | ParsedToken::Malformed => None,
    }
}

fn auth_context_hash(token: ParsedToken<'_>) -> String {
    match token {
        ParsedToken::Missing => "missing".to_string(),
        ParsedToken::Malformed => "malformed".to_string(),
        ParsedToken::Present(value) => {
            let digest = Sha256::digest(value.as_bytes());
            hex::encode(digest)[..16].to_string()
        }
    }
}

fn endpoint_group(method: &Method, path: &str) -> &'static str {
    if path == "/ws" {
        return "ws";
    }
    if method == Method::GET || method == Method::HEAD {
        "read"
    } else {
        "mutate"
    }
}

fn resolved_source(
    request: &Request,
    headers: &HeaderMap,
    trusted_proxy_cidrs: &[IpNet],
) -> IpAddr {
    let peer_ip = request
        .extensions()
        .get::<ConnectInfo<SocketAddr>>()
        .map(|connect_info| connect_info.0.ip())
        .unwrap_or(IpAddr::from([0, 0, 0, 0]));

    resolve_client_source(peer_ip, headers, trusted_proxy_cidrs)
}

fn resolve_client_source(
    peer_ip: IpAddr,
    headers: &HeaderMap,
    trusted_proxy_cidrs: &[IpNet],
) -> IpAddr {
    if !trusted_proxy_cidrs
        .iter()
        .any(|cidr| cidr.contains(&peer_ip))
    {
        return peer_ip;
    }

    let forwarded_for = headers
        .get("x-forwarded-for")
        .and_then(|value| value.to_str().ok())
        .map(str::trim)
        .filter(|value| !value.is_empty());
    let Some(forwarded_for) = forwarded_for else {
        return peer_ip;
    };
    let first_hop = forwarded_for
        .split(',')
        .next()
        .map(str::trim)
        .filter(|value| !value.is_empty());
    let Some(first_hop) = first_hop else {
        return peer_ip;
    };

    first_hop.parse::<IpAddr>().unwrap_or(peer_ip)
}

fn throttled_response() -> Response {
    http::api_error_response(
        StatusCode::TOO_MANY_REQUESTS,
        "too many authentication failures",
    )
}

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

    use axum::http::HeaderValue;

    fn token(value: &str) -> SecretString {
        SecretString::new(value.to_string())
    }

    #[test]
    fn extract_token_classifies_authorization_headers() {
        struct Case {
            name: &'static str,
            authorization: Option<&'static str>,
            expected: ParsedToken<'static>,
        }

        let cases = [
            Case {
                name: "missing header",
                authorization: None,
                expected: ParsedToken::Missing,
            },
            Case {
                name: "bearer token",
                authorization: Some("Bearer test-token-123"),
                expected: ParsedToken::Present("test-token-123"),
            },
            Case {
                name: "bearer lowercase",
                authorization: Some("bearer test-token"),
                expected: ParsedToken::Present("test-token"),
            },
            Case {
                name: "bearer uppercase",
                authorization: Some("BEARER upper-token"),
                expected: ParsedToken::Present("upper-token"),
            },
            Case {
                name: "trimmed token",
                authorization: Some("Bearer   trimmed-token   "),
                expected: ParsedToken::Present("trimmed-token"),
            },
            Case {
                name: "empty bearer token",
                authorization: Some("Bearer "),
                expected: ParsedToken::Malformed,
            },
            Case {
                name: "wrong auth scheme",
                authorization: Some("Basic dXNlcjpwYXNz"),
                expected: ParsedToken::Malformed,
            },
            Case {
                name: "embedded spaces in token",
                authorization: Some("Bearer token with-space"),
                expected: ParsedToken::Malformed,
            },
            Case {
                name: "embedded tab in token",
                authorization: Some("Bearer token\tinjected"),
                expected: ParsedToken::Malformed,
            },
        ];

        for case in cases {
            let mut headers = HeaderMap::new();
            if let Some(authorization) = case.authorization {
                headers.insert(
                    "authorization",
                    authorization.parse().expect("header value"),
                );
            }
            assert_eq!(
                extract_token(&headers),
                case.expected,
                "case {:?} failed",
                case.name
            );
        }
    }

    #[test]
    fn extract_token_rejects_non_utf8_header() {
        let mut headers = HeaderMap::new();
        headers.insert(
            "authorization",
            HeaderValue::from_bytes(&[b'B', b'e', b'a', b'r', b'e', b'r', b' ', 0x80])
                .expect("header value"),
        );
        assert_eq!(extract_token(&headers), ParsedToken::Malformed);
    }

    #[test]
    fn extract_token_rejects_overlength() {
        let mut headers = HeaderMap::new();
        let long_token = format!("Bearer {}", "a".repeat(MAX_BEARER_TOKEN_BYTES + 1));
        headers.insert("authorization", long_token.parse().expect("header value"));
        assert_eq!(extract_token(&headers), ParsedToken::Malformed);
    }

    #[test]
    fn extract_token_allows_max_length() {
        let mut headers = HeaderMap::new();
        let token = "a".repeat(MAX_BEARER_TOKEN_BYTES);
        let header = format!("Bearer {token}");
        headers.insert("authorization", header.parse().expect("header value"));
        assert_eq!(
            extract_token(&headers),
            ParsedToken::Present(token.as_str())
        );
    }

    #[test]
    fn valid_token_is_allowed() {
        let expected = token("session-token");
        let result = authorize_expected_token(&expected, Some("session-token"));
        assert_eq!(result, Ok(()));
    }

    #[test]
    fn invalid_token_is_rejected() {
        let expected = token("session-token");
        let result = authorize_expected_token(&expected, Some("wrong"));
        assert_eq!(result, Err((StatusCode::UNAUTHORIZED, "invalid token")));
    }

    #[test]
    fn missing_token_is_rejected() {
        let expected = token("session-token");
        let result = authorize_expected_token(&expected, None);
        assert_eq!(result, Err((StatusCode::UNAUTHORIZED, "missing token")));
    }

    #[test]
    fn trusted_forwarded_for_is_used_for_source() {
        let mut headers = HeaderMap::new();
        headers.insert("x-forwarded-for", "203.0.113.9, 10.0.0.1".parse().unwrap());
        let source = resolve_client_source(
            IpAddr::from([127, 0, 0, 1]),
            &headers,
            &["127.0.0.1/32".parse::<IpNet>().expect("cidr")],
        );
        assert_eq!(source, IpAddr::from([203, 0, 113, 9]));
    }

    #[test]
    fn untrusted_peer_ignores_forwarded_headers() {
        let mut headers = HeaderMap::new();
        headers.insert("x-forwarded-for", "203.0.113.9".parse().unwrap());
        let source = resolve_client_source(
            IpAddr::from([10, 0, 0, 5]),
            &headers,
            &["127.0.0.1/32".parse::<IpNet>().expect("cidr")],
        );
        assert_eq!(source, IpAddr::from([10, 0, 0, 5]));
    }

    #[test]
    fn malformed_forwarded_header_falls_back_to_peer_ip() {
        let mut headers = HeaderMap::new();
        headers.insert("x-forwarded-for", "not-an-ip".parse().unwrap());
        let source = resolve_client_source(
            IpAddr::from([127, 0, 0, 1]),
            &headers,
            &["127.0.0.1/32".parse::<IpNet>().expect("cidr")],
        );
        assert_eq!(source, IpAddr::from([127, 0, 0, 1]));
    }

    #[test]
    fn auth_failure_throttle_limits_after_threshold() {
        let throttle = AuthFailureThrottle::new();
        let key = AuthThrottleKey::new(IpAddr::from([127, 0, 0, 1]), "hash".to_string(), "mutate");

        assert!(!throttle.record_failure(key.clone(), 2));
        assert!(!throttle.record_failure(key.clone(), 2));
        assert!(throttle.record_failure(key.clone(), 2));
        assert!(throttle.is_throttled(&key, 2));
    }
}
