use codex_exec_server::HttpHeader;
use reqwest::header::WWW_AUTHENTICATE;

#[derive(Debug, PartialEq, Eq)]
pub(super) struct InsufficientScopeChallenge {
    pub(super) www_authenticate_header: String,
    pub(super) required_scope: Option<String>,
}

#[derive(Debug, PartialEq, Eq)]
struct BearerInsufficientScope {
    required_scope: Option<String>,
}

type AuthParameter<'a> = (&'a str, Option<String>);
type ChallengeStart<'a> = (&'a str, Option<AuthParameter<'a>>);

#[derive(Default)]
enum Parameter {
    #[default]
    Missing,
    Value(String),
    Invalid,
}

#[derive(Default)]
struct BearerChallenge {
    error: Parameter,
    scope: Parameter,
}

impl BearerChallenge {
    fn add_parameter(&mut self, name: &str, value: Option<String>) {
        let parameter = if name.eq_ignore_ascii_case("error") {
            &mut self.error
        } else if name.eq_ignore_ascii_case("scope") {
            &mut self.scope
        } else {
            return;
        };

        *parameter = match (&*parameter, value) {
            (Parameter::Missing, Some(value)) => Parameter::Value(value),
            (Parameter::Missing, None) | (Parameter::Value(_), _) | (Parameter::Invalid, _) => {
                Parameter::Invalid
            }
        };
    }

    fn into_insufficient_scope(self) -> Option<BearerInsufficientScope> {
        match self.error {
            Parameter::Value(error) if error == "insufficient_scope" => {
                Some(BearerInsufficientScope {
                    required_scope: match self.scope {
                        Parameter::Value(scope) if valid_scope(&scope) => Some(scope),
                        Parameter::Missing | Parameter::Value(_) | Parameter::Invalid => None,
                    },
                })
            }
            Parameter::Missing | Parameter::Value(_) | Parameter::Invalid => None,
        }
    }
}

/// Finds a Bearer insufficient-scope challenge among all `WWW-Authenticate`
/// response header field values.
pub(super) fn insufficient_scope_challenge(
    headers: &[HttpHeader],
) -> Option<InsufficientScopeChallenge> {
    headers
        .iter()
        .filter(|header| header.name.eq_ignore_ascii_case(WWW_AUTHENTICATE.as_str()))
        .find_map(|header| {
            parse_bearer_insufficient_scope(&header.value).map(|challenge| {
                InsufficientScopeChallenge {
                    www_authenticate_header: header.value.clone(),
                    required_scope: challenge.required_scope,
                }
            })
        })
}

/// Parses a Bearer `WWW-Authenticate` challenge with an `insufficient_scope`
/// error and extracts its optional required scope.
///
/// RFC 9110 section 11.2 defines challenge parameters as `auth-param` values
/// whose values are either `token` or `quoted-string`. Quoted strings use HTTP
/// syntax rather than JSON: section 5.6.4 requires recipients to replace each
/// `quoted-pair` with its escaped octet.
///
/// RFC 6750 section 3 permits `scope` in the Bearer challenge at most once.
/// After HTTP quoted-string processing, each scope token can contain `%x21`,
/// `%x23-5B`, or `%x5D-7E`, with `%x20` separating multiple tokens. Therefore
/// returned scopes cannot contain `"` or `\`, even when those characters occur
/// in the header encoding.
///
/// RMCP has related parsing logic, but it is private to that crate.
fn parse_bearer_insufficient_scope(header: &str) -> Option<BearerInsufficientScope> {
    let segments = split_unquoted_segments(header)?;
    let mut bearer_challenge: Option<BearerChallenge> = None;

    for segment in segments {
        if let Some((name, value)) = parse_auth_param(segment) {
            if let Some(challenge) = bearer_challenge.as_mut() {
                challenge.add_parameter(name, value);
            }
            continue;
        }

        if let Some(challenge) = bearer_challenge
            .take()
            .and_then(BearerChallenge::into_insufficient_scope)
        {
            return Some(challenge);
        }

        let (scheme, parameter) = parse_challenge_start(segment)?;
        if scheme.eq_ignore_ascii_case("Bearer") {
            let mut challenge = BearerChallenge::default();
            if let Some((name, value)) = parameter {
                challenge.add_parameter(name, value);
            }
            bearer_challenge = Some(challenge);
        }
    }

    bearer_challenge.and_then(BearerChallenge::into_insufficient_scope)
}

fn parse_challenge_start(segment: &str) -> Option<ChallengeStart<'_>> {
    let segment = segment.trim();
    let parameter_start = segment.find(char::is_whitespace);
    let (scheme, parameter) = match parameter_start {
        Some(parameter_start) => (
            &segment[..parameter_start],
            parse_auth_param(&segment[parameter_start..]),
        ),
        None => (segment, None),
    };

    is_http_token(scheme).then_some((scheme, parameter))
}

fn parse_auth_param(segment: &str) -> Option<AuthParameter<'_>> {
    let (name, value) = segment.trim().split_once('=')?;
    let name = name.trim();
    is_http_token(name).then_some((name, parse_auth_param_value(value.trim())))
}

fn parse_auth_param_value(value: &str) -> Option<String> {
    if let Some(quoted_value) = value.strip_prefix('"') {
        let quoted_value = quoted_value.strip_suffix('"')?;
        let mut decoded = String::with_capacity(quoted_value.len());
        let mut characters = quoted_value.chars();
        while let Some(character) = characters.next() {
            if character == '\\' {
                decoded.push(characters.next()?);
            } else {
                decoded.push(character);
            }
        }
        Some(decoded)
    } else {
        is_http_token(value).then(|| value.to_string())
    }
}

fn split_unquoted_segments(header: &str) -> Option<Vec<&str>> {
    let mut segments = Vec::new();
    let mut segment_start = 0;
    let mut in_quotes = false;
    let mut escaped = false;

    for (position, character) in header.char_indices() {
        if escaped {
            escaped = false;
            continue;
        }
        match character {
            '\\' if in_quotes => escaped = true,
            '"' => in_quotes = !in_quotes,
            ',' | ';' if !in_quotes => {
                segments.push(&header[segment_start..position]);
                segment_start = position + character.len_utf8();
            }
            _ => {}
        }
    }

    if in_quotes || escaped {
        None
    } else {
        segments.push(&header[segment_start..]);
        Some(segments)
    }
}

fn valid_scope(scope: &str) -> bool {
    scope.split(' ').all(|token| {
        !token.is_empty()
            && token
                .bytes()
                .all(|byte| matches!(byte, b'!' | b'#'..=b'[' | b']'..=b'~'))
    })
}

fn is_http_token(value: &str) -> bool {
    !value.is_empty()
        && value.bytes().all(|byte| {
            byte.is_ascii_alphanumeric()
                || matches!(
                    byte,
                    b'!' | b'#'
                        | b'$'
                        | b'%'
                        | b'&'
                        | b'\''
                        | b'*'
                        | b'+'
                        | b'-'
                        | b'.'
                        | b'^'
                        | b'_'
                        | b'`'
                        | b'|'
                        | b'~'
                )
        })
}

#[cfg(test)]
#[path = "www_authenticate_tests.rs"]
mod tests;
