use std::io::{Read, Write};
use std::str;

use anyhow::{anyhow, Context};

use crate::s3_listener::constants::{
    MAX_BODY_BYTES, MAX_HEADER_BYTES, MAX_HEADER_FIELDS, MAX_HEADER_KEY_BYTES,
    MAX_HEADER_LINE_BYTES, MAX_HEADER_VALUE_BYTES, MAX_QUERY_BYTES, MAX_QUERY_PAIRS,
    MAX_REQUEST_LINE_BYTES, MAX_REQUEST_URI_BYTES,
};
use crate::s3_listener::errors::HttpParseError;

pub(crate) fn read_http_request(
    stream: &mut std::net::TcpStream,
    buffer: &mut Vec<u8>,
    chunk: &mut [u8],
) -> Result<(String, Vec<u8>), HttpParseError> {
    loop {
        let read = stream.read(chunk).map_err(|error| {
            HttpParseError::bad_request(format!("failed to read HTTP request: {error}"))
        })?;
        if read == 0 {
            return Err(HttpParseError::bad_request("unexpected end of stream"));
        }

        buffer.extend_from_slice(&chunk[..read]);
        if buffer.len() > MAX_HEADER_BYTES {
            return Err(HttpParseError::request_entity_too_large(format!(
                "http headers exceed {MAX_HEADER_BYTES} bytes"
            )));
        }

        let Some(header_end) = find_header_end(buffer) else {
            continue;
        };
        let header_bytes = &buffer[..header_end];
        let headers = str::from_utf8(header_bytes).map_err(|error| {
            HttpParseError::bad_request(format!("invalid UTF-8 in headers: {error}"))
        })?;
        let body_start = header_end + 4;
        let request_line = headers
            .lines()
            .next()
            .ok_or_else(|| HttpParseError::bad_request("missing request line"))?;
        if request_line.len() > MAX_REQUEST_LINE_BYTES {
            return Err(HttpParseError::bad_request("request line too long"));
        }
        let request_line = parse_request_line(request_line)
            .map_err(|error| HttpParseError::bad_request(error.to_string()))?;
        let (method, _target) = request_line;
        let headers = headers.to_string();

        let mut content_length = 0usize;
        let mut content_length_seen = false;
        let mut header_count = 0usize;
        let mut host_seen = false;
        for (index, line) in headers.lines().enumerate() {
            if index == 0 {
                continue;
            }
            if line.is_empty() {
                continue;
            }
            if line.len() > MAX_HEADER_LINE_BYTES {
                return Err(HttpParseError::bad_request(
                    "individual header line too long",
                ));
            }
            let Some((key, value)) = line.split_once(':') else {
                return Err(HttpParseError::bad_request("invalid header line"));
            };
            let (key, value) = validate_header_field(key, value)
                .map_err(|error| HttpParseError::bad_request(error.to_string()))?;
            if key.trim().is_empty() || value.trim().is_empty() {
                return Err(HttpParseError::bad_request("invalid header line"));
            }
            header_count += 1;
            if header_count > MAX_HEADER_FIELDS {
                return Err(HttpParseError::bad_request("too many headers"));
            }
            if key.eq_ignore_ascii_case("host") {
                if host_seen {
                    return Err(HttpParseError::bad_request("duplicate host header"));
                }
                host_seen = true;
            }
            if key.eq_ignore_ascii_case("transfer-encoding") {
                let encoding = value.trim().to_ascii_lowercase();
                if encoding.contains("chunked") {
                    return Err(HttpParseError::unsupported(
                        "chunked transfer-encoding is not supported",
                    ));
                }
                if !encoding.is_empty() && encoding != "identity" {
                    return Err(HttpParseError::bad_request(
                        "invalid transfer-encoding value",
                    ));
                }
            }
            if key.eq_ignore_ascii_case("content-length") {
                if content_length_seen {
                    return Err(HttpParseError::bad_request(
                        "multiple content-length headers",
                    ));
                }
                content_length_seen = true;
                content_length = value.trim().parse::<usize>().map_err(|error| {
                    HttpParseError::bad_request(format!("invalid content-length `{}`", error))
                })?;
            }
        }
        if method.eq_ignore_ascii_case("GET")
            || method.eq_ignore_ascii_case("HEAD")
            || method.eq_ignore_ascii_case("OPTIONS")
        {
            if content_length > 0 {
                return Err(HttpParseError::bad_request(
                    "request body not allowed for GET, HEAD, or OPTIONS",
                ));
            }
        }
        if content_length > MAX_BODY_BYTES {
            return Err(HttpParseError::request_entity_too_large(format!(
                "http content-length exceeds {MAX_BODY_BYTES} bytes"
            )));
        }

        if expects_continue(&headers) {
            stream
                .write_all(b"HTTP/1.1 100 Continue\r\n\r\n")
                .map_err(|error| {
                    HttpParseError::bad_request(format!("failed to write 100-continue: {error}"))
                })?;
            stream.flush().map_err(|error| {
                HttpParseError::bad_request(format!("failed to flush 100-continue: {error}"))
            })?;
        }

        while buffer.len() < body_start + content_length {
            let read = stream.read(chunk).map_err(|error| {
                HttpParseError::bad_request(format!("failed to read request body: {error}"))
            })?;
            if read == 0 {
                return Err(HttpParseError::bad_request(
                    "unexpected end of stream while reading request body",
                ));
            }
            buffer.extend_from_slice(&chunk[..read]);
            if buffer.len() > MAX_HEADER_BYTES + MAX_BODY_BYTES {
                return Err(HttpParseError::request_entity_too_large(format!(
                    "http body exceeds {MAX_BODY_BYTES} bytes"
                )));
            }
        }

        let body = buffer[body_start..body_start + content_length].to_vec();
        if body.len() > MAX_BODY_BYTES {
            return Err(HttpParseError::request_entity_too_large(format!(
                "http body exceeds {MAX_BODY_BYTES} bytes"
            )));
        }
        return Ok((headers, body));
    }
}

pub(crate) fn parse_request_line(request_line: &str) -> anyhow::Result<(String, &str)> {
    if request_line.len() > MAX_REQUEST_LINE_BYTES {
        anyhow::bail!("invalid HTTP request line");
    }
    let mut request_parts = request_line.split_whitespace();
    let method = request_parts
        .next()
        .ok_or_else(|| anyhow!("missing HTTP method"))?;
    if !method
        .bytes()
        .all(|byte| matches!(byte, b'A'..=b'Z' | b'-' | b'_'))
    {
        anyhow::bail!("invalid HTTP method `{method}`");
    }
    let method = method.to_ascii_uppercase();
    if !matches!(
        method.as_str(),
        "GET" | "HEAD" | "PUT" | "POST" | "DELETE" | "OPTIONS"
    ) {
        anyhow::bail!("unsupported HTTP method `{method}`");
    }
    let target = request_parts
        .next()
        .ok_or_else(|| anyhow!("missing HTTP target"))?;
    let version = request_parts
        .next()
        .ok_or_else(|| anyhow!("missing HTTP version"))?;
    if request_parts.next().is_some() {
        anyhow::bail!("invalid HTTP request line");
    }
    if !version.eq_ignore_ascii_case("HTTP/1.1") {
        anyhow::bail!("unsupported HTTP version `{version}`");
    }
    Ok((method, target))
}

pub(crate) fn split_target(target: &str) -> anyhow::Result<(String, Vec<(String, String)>)> {
    if target.starts_with("http://") || target.starts_with("https://") {
        anyhow::bail!("absolute-form request target not supported");
    }
    if target.contains(' ') {
        anyhow::bail!("invalid request target");
    }

    let Some((path, query)) = target.split_once('?') else {
        if target.len() > MAX_REQUEST_URI_BYTES {
            anyhow::bail!("request target too long");
        }
        let path = url_decode(target)?;
        sanitize_request_target(&path)?;
        if path.is_empty() || !path.starts_with('/') {
            anyhow::bail!("invalid request target path");
        }
        return Ok((path, Vec::new()));
    };
    if query.len() > MAX_QUERY_BYTES {
        anyhow::bail!("query string too long");
    }
    if target.len() > MAX_REQUEST_URI_BYTES {
        anyhow::bail!("request target too long");
    }
    let path = url_decode(path)?;
    sanitize_request_target(&path)?;
    if path.is_empty() || !path.starts_with('/') {
        anyhow::bail!("invalid request target path");
    }

    let query = query
        .split('&')
        .filter(|part| !part.is_empty())
        .take(MAX_QUERY_PAIRS + 1)
        .map(|part| -> anyhow::Result<(String, String)> {
            let (key, value) = part.split_once('=').unwrap_or((part, ""));
            let key = url_decode(key)?;
            let value = url_decode(value)?;
            Ok((key, value))
        })
        .collect::<Result<Vec<_>, _>>()?;
    if query.len() > MAX_QUERY_PAIRS {
        anyhow::bail!("too many query parameters");
    }
    if query.iter().any(|(key, value)| {
        key.is_empty() || key.len() > MAX_QUERY_BYTES || value.len() > MAX_QUERY_BYTES
    }) {
        anyhow::bail!("query parameter value too large");
    }
    Ok((path, query))
}

pub(crate) fn validate_header_field(key: &str, value: &str) -> anyhow::Result<(String, String)> {
    let key = key.trim();
    let value = value.trim();
    if key.is_empty() || value.is_empty() {
        anyhow::bail!("invalid header line");
    }
    if key.len() > MAX_HEADER_KEY_BYTES || value.len() > MAX_HEADER_VALUE_BYTES {
        anyhow::bail!("header too large");
    }
    if !key
        .bytes()
        .all(|byte| matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_'))
    {
        anyhow::bail!("invalid header field-name");
    }
    if value
        .bytes()
        .any(|byte| byte == b'\r' || byte == b'\n' || byte <= 0x1F || byte == 0x7F)
    {
        anyhow::bail!("invalid header value");
    }

    Ok((key.to_string(), value.to_string()))
}

pub(crate) fn sanitize_request_target(path: &str) -> anyhow::Result<()> {
    if path
        .bytes()
        .any(|byte| byte <= 0x1f || byte == 0x7f || byte == b'\\')
    {
        anyhow::bail!("invalid request target path");
    }

    for segment in path.split('/') {
        if segment == "." || segment == ".." {
            anyhow::bail!("path traversal attempt");
        }
    }

    Ok(())
}

pub(crate) fn url_decode(value: &str) -> anyhow::Result<String> {
    let bytes = value.as_bytes();
    let mut decoded = Vec::new();
    let mut index = 0;

    while index < bytes.len() {
        if bytes[index] == b'%' {
            if index + 2 >= bytes.len() {
                anyhow::bail!("malformed percent-escape");
            }
            let hex = u8::from_str_radix(&value[index + 1..index + 3], 16)
                .map_err(|_| anyhow!("malformed percent-escape"))?;
            decoded.push(hex);
            index += 3;
            continue;
        }
        if bytes[index] == b'+' {
            decoded.push(b' ');
        } else {
            decoded.push(bytes[index]);
        }
        index += 1;
    }

    String::from_utf8(decoded).map_err(|error| anyhow!("invalid UTF-8 in query parameter: {error}"))
}

pub(crate) fn content_length(headers: &str) -> anyhow::Result<usize> {
    Ok(headers
        .lines()
        .find_map(|line| {
            let (key, value) = line.split_once(':')?;
            key.eq_ignore_ascii_case("content-length").then(|| {
                value
                    .trim()
                    .parse::<usize>()
                    .with_context(|| format!("invalid content-length `{}`", value.trim()))
            })
        })
        .transpose()?
        .unwrap_or(0))
}

pub(crate) fn expects_continue(headers: &str) -> bool {
    headers.lines().any(|line| {
        let Some((key, value)) = line.split_once(':') else {
            return false;
        };
        key.eq_ignore_ascii_case("expect") && value.trim().eq_ignore_ascii_case("100-continue")
    })
}

fn find_header_end(buffer: &[u8]) -> Option<usize> {
    buffer.windows(4).position(|window| window == b"\r\n\r\n")
}
