use std::{
    collections::HashMap,
    io::{Read as _, Write},
    net::TcpListener,
};

const VALID_HTTP_VERSIONS: &[&str] = &["HTTP/1.0", "HTTP/1.1"];
const HTTP_VERSION: &str = "HTTP/1.1";
const MAX_REQUEST_SIZE: usize = 1024 * 128;

pub type Result<T> = std::result::Result<T, HttpError>;

#[derive(Debug)]
pub enum HttpError {
    InvalidRequest,
    TcpIoError(std::io::Error),
}

impl From<std::io::Error> for HttpError {
    fn from(value: std::io::Error) -> Self {
        Self::TcpIoError(value)
    }
}

pub fn run<S, F>(address: &str, mut state: S, callback: F) -> Result<()>
where
    F: Fn(&mut S, Request) -> Response,
{
    let listener = TcpListener::bind(address)?;

    for mut stream in listener.incoming().flatten() {
        let mut buf = [0; MAX_REQUEST_SIZE];
        let Ok(n) = stream.read(&mut buf) else {
            continue;
        };
        let request_string = String::from_utf8_lossy(&buf[..n]);
        let Ok(request) = Request::try_from(request_string.to_string()) else {
            continue;
        };

        let response = callback(&mut state, request);
        stream.write_all(response.to_string().as_bytes())?;
        stream.flush()?;
        stream.shutdown(std::net::Shutdown::Both)?;
    }

    Ok(())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
    Get,
    Post,
    Other,
}

#[derive(Debug)]
pub struct Request {
    pub method: Method,
    pub url: String,
    pub headers: HashMap<String, String>,
    pub body: String,
}

impl TryFrom<String> for Request {
    type Error = HttpError;

    fn try_from(value: String) -> Result<Self> {
        const E: HttpError = HttpError::InvalidRequest;

        let Some((top, body)) = value.split_once("\r\n\r\n") else {
            return Err(E);
        };

        let mut top = top.split("\r\n");
        let Some(info) = top.next() else {
            return Err(E);
        };
        let headers = top.collect::<Vec<&str>>();

        let mut info = info.split(" ");
        let Some(method) = info.next() else {
            return Err(E);
        };

        let Some(url) = info.next() else {
            return Err(E);
        };

        let Some(version) = info.next() else {
            return Err(E);
        };

        if !VALID_HTTP_VERSIONS.contains(&version) {
            return Err(E);
        }

        let method = match &method.to_uppercase()[..] {
            "GET" => Method::Get,
            "POST" => Method::Post,
            _ => Method::Other,
        };

        let mut headers_map = HashMap::new();
        for h in headers {
            let Some((key, value)) = h.split_once(":") else {
                continue;
            };

            headers_map.insert(key.trim().to_lowercase(), value.trim().to_string());
        }

        Ok(Self {
            method,
            url: url.to_string(),
            headers: headers_map,
            body: body.to_string(),
        })
    }
}

#[derive(Debug)]
pub struct Response {
    pub status_code: u16,
    pub headers: HashMap<String, String>,
    pub body: String,
}

impl Response {
    pub fn new(code: u16) -> Self {
        Self {
            status_code: code,
            headers: HashMap::new(),
            body: String::new(),
        }
    }

    pub fn ok() -> Self {
        Self::new(200)
    }

    pub fn bad_request() -> Self {
        Self::new(400)
    }

    pub fn not_found() -> Self {
        Self::new(404)
    }

    pub fn method_not_allowed() -> Self {
        Self::new(405)
    }

    pub fn header(mut self, key: &str, value: &str) -> Self {
        self.headers.insert(key.to_lowercase(), value.to_string());
        self
    }

    pub fn body(mut self, body: &str) -> Self {
        self.body = body.to_string();
        self
    }
}

impl ToString for Response {
    fn to_string(&self) -> String {
        let mut s = format!("{} {} \r\n", HTTP_VERSION, self.status_code.to_string());

        for (idx, (key, value)) in self.headers.iter().enumerate() {
            s.push_str(&format!("{}: {}", key, value));
            if idx + 1 < self.headers.len() {
                s.push('\n');
            }
        }

        s.push_str("\r\n");
        s.push_str(&self.body);

        s
    }
}
