use efdn_types::TypeError;

pub fn validate_path(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    validate_path_base(value, type_name)?;
    if value.contains('{') || value.contains('}') {
        return Err(TypeError::new(
            type_name,
            "concrete path must not contain route parameters",
            "provide a concrete path like /instances/i_123",
        ));
    }
    Ok(())
}

pub fn validate_route_pattern(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    validate_path_base(value, type_name)?;
    validate_route_parameters(value, type_name)
}

pub fn validate_query_key(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.is_empty() {
        return Err(TypeError::empty(type_name));
    }
    validate_header_value(value, type_name)
}

pub fn validate_query_value(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    validate_header_value(value, type_name)
}

pub fn validate_query_string(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.contains('?') || value.contains('#') {
        return Err(TypeError::new(
            type_name,
            "query string must not include `?` or fragment",
            "provide only key=value pairs",
        ));
    }
    validate_header_value(value, type_name)
}

pub fn validate_header_name(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.is_empty() {
        return Err(TypeError::empty(type_name));
    }
    if value
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
    {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        "header name contains an unsupported character",
        "provide a header name like Content-Type",
    ))
}

pub fn validate_header_value(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.chars().any(char::is_control) {
        return Err(TypeError::new(
            type_name,
            "value contains a control character",
            "provide a printable value",
        ));
    }
    Ok(())
}

pub fn validate_media_type(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.split('/').count() == 2 && !value.contains(char::is_whitespace) {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        "media type must be type/subtype",
        "provide a media type like application/json",
    ))
}

pub fn encode(value: &str) -> String {
    value
        .bytes()
        .flat_map(|byte| match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
                vec![byte as char]
            }
            _ => format!("%{byte:02X}").chars().collect(),
        })
        .collect()
}

fn validate_path_base(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if !value.starts_with('/') {
        return Err(TypeError::new(
            type_name,
            "path must start with `/`",
            "provide a path like /health/live",
        ));
    }
    if value.contains('?') || value.contains('#') {
        return Err(TypeError::new(
            type_name,
            "path must not include query or fragment",
            "put query values in HttpQueryString",
        ));
    }
    if value.chars().any(char::is_control) {
        return Err(TypeError::new(
            type_name,
            "path contains a control character",
            "provide a printable path",
        ));
    }
    Ok(())
}

fn validate_route_parameters(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    let mut names = Vec::new();
    for piece in value.split('{').skip(1) {
        let Some((name, _)) = piece.split_once('}') else {
            return Err(TypeError::new(
                type_name,
                "route parameter is missing `}`",
                "close the parameter like {instance}",
            ));
        };
        validate_parameter_name(name, type_name)?;
        if names.iter().any(|seen| seen == name) {
            return Err(TypeError::new(
                type_name,
                format!("duplicate route parameter `{name}`"),
                "use each route parameter once",
            ));
        }
        names.push(name.to_owned());
    }
    reject_unopened_parameter(value, names.len(), type_name)
}

fn reject_unopened_parameter(
    value: &str,
    count: usize,
    type_name: &'static str,
) -> Result<(), TypeError> {
    if value.matches('}').count() == count {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        "route parameter is missing `{`",
        "open the parameter like {instance}",
    ))
}

fn validate_parameter_name(value: &str, type_name: &'static str) -> Result<(), TypeError> {
    if value.is_empty() {
        return Err(TypeError::empty(type_name));
    }
    if value
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
    {
        return Ok(());
    }
    Err(TypeError::new(
        type_name,
        "route parameter must use ASCII letters, numbers, or `_`",
        "provide a parameter like {instance_id}",
    ))
}
