use std::fmt::Write;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::creds::Credentials;
use crate::error::Error;
use crate::region::{Region, endpoint_host};
use crate::sigv4::{self, CanonicalRequest, SigningParams};
use crate::transport;

const HTTPS_PORT: u16 = 443;

fn now_unix() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

pub struct Request {
    pub method: String,
    pub service: String,
    pub path: String,
    pub query: Vec<(String, String)>,
    pub headers: Vec<(String, String)>,
    pub body: Vec<u8>,
}

impl Request {
    pub fn new(method: &str, service: &str) -> Self {
        Self {
            method: method.to_string(),
            service: service.to_string(),
            path: "/".to_string(),
            query: Vec::new(),
            headers: Vec::new(),
            body: Vec::new(),
        }
    }

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

    pub fn body(mut self, body: Vec<u8>) -> Self {
        self.body = body;
        self
    }
}

pub struct Response {
    pub status: u16,
    pub headers: Vec<(String, String)>,
    pub body: Vec<u8>,
}

impl Response {
    pub fn body_str(&self) -> String {
        String::from_utf8_lossy(&self.body).into_owned()
    }

    pub fn is_success(&self) -> bool {
        (200..300).contains(&self.status)
    }

    fn header(&self, name: &str) -> Option<&str> {
        self.headers
            .iter()
            .find(|(k, _)| k.eq_ignore_ascii_case(name))
            .map(|(_, v)| v.as_str())
    }
}

pub fn send(creds: &Credentials, region: &Region, req: &Request) -> Result<Response, Error> {
    let host = endpoint_host(&req.service, region);
    let unix_secs = now_unix();
    let (amz_date, date_stamp) = sigv4::timestamps(unix_secs);

    let mut headers = req.headers.clone();
    headers.push(("Host".to_string(), host.clone()));
    headers.push(("X-Amz-Date".to_string(), amz_date.clone()));
    if let Some(token) = &creds.session_token {
        headers.push(("X-Amz-Security-Token".to_string(), token.clone()));
    }

    let params = SigningParams {
        access_key_id: &creds.access_key_id,
        secret_access_key: &creds.secret_access_key,
        region: region.as_str(),
        service: &req.service,
    };
    let canonical = CanonicalRequest {
        method: &req.method,
        path: &req.path,
        query: &req.query,
        headers: &headers,
        body: &req.body,
    };
    let authorization = sigv4::authorization(&params, &canonical, &amz_date, &date_stamp);
    headers.push(("Authorization".to_string(), authorization));

    let wire = serialize(req, &headers);
    let raw = transport::fetch(&host, HTTPS_PORT, unix_secs, wire)?;
    parse(&raw)
}

pub fn get_unsigned(host: &str, path: &str) -> Result<Response, Error> {
    let target = if path.is_empty() { "/" } else { path };
    let wire = format!(
        "GET {target} HTTP/1.1\r\n\
         Host: {host}\r\n\
         User-Agent: dope-aws\r\n\
         Accept: */*\r\n\
         Connection: close\r\n\r\n"
    )
    .into_bytes();
    let raw = transport::fetch(host, HTTPS_PORT, now_unix(), wire)?;
    parse(&raw)
}

fn serialize(req: &Request, headers: &[(String, String)]) -> Vec<u8> {
    let target = request_target(&req.path, &req.query);
    let mut out = format!("{} {target} HTTP/1.1\r\n", req.method);
    for (k, v) in headers {
        out.push_str(k);
        out.push_str(": ");
        out.push_str(v);
        out.push_str("\r\n");
    }
    let _ = write!(out, "Content-Length: {}\r\n", req.body.len());
    out.push_str("Connection: close\r\n\r\n");

    let mut bytes = out.into_bytes();
    bytes.extend_from_slice(&req.body);
    bytes
}

fn request_target(path: &str, query: &[(String, String)]) -> String {
    let path = if path.is_empty() { "/" } else { path };
    if query.is_empty() {
        return path.to_string();
    }
    format!("{path}?{}", sigv4::encode_query(query, true))
}

fn parse(raw: &[u8]) -> Result<Response, Error> {
    let split = find_subslice(raw, b"\r\n\r\n")
        .ok_or_else(|| Error::parse("malformed response: no header terminator"))?;
    let head = std::str::from_utf8(&raw[..split])
        .map_err(|_| Error::parse("non-utf8 response headers"))?;
    let body_start = split + 4;

    let mut lines = head.split("\r\n");
    let status = parse_status(lines.next().ok_or_else(|| Error::parse("empty response"))?)?;

    let mut headers = Vec::new();
    for line in lines {
        if let Some((k, v)) = line.split_once(':') {
            headers.push((k.trim().to_string(), v.trim().to_string()));
        }
    }
    let resp = Response {
        status,
        headers,
        body: Vec::new(),
    };

    let body = if resp
        .header("Transfer-Encoding")
        .is_some_and(|te| te.eq_ignore_ascii_case("chunked"))
    {
        decode_chunked(&raw[body_start..])?
    } else if let Some(len) = resp.header("Content-Length") {
        let n: usize = len
            .parse()
            .map_err(|_| Error::parse("invalid Content-Length"))?;
        let end = body_start + n;
        if end > raw.len() {
            return Err(Error::parse(
                "truncated body: fewer bytes than Content-Length",
            ));
        }
        raw[body_start..end].to_vec()
    } else {
        raw[body_start..].to_vec()
    };

    Ok(Response { body, ..resp })
}

fn parse_status(line: &str) -> Result<u16, Error> {
    line.split(' ')
        .nth(1)
        .and_then(|s| s.parse().ok())
        .ok_or_else(|| Error::parse(format!("bad status line: {line}")))
}

fn decode_chunked(mut data: &[u8]) -> Result<Vec<u8>, Error> {
    let mut out = Vec::new();
    loop {
        let line_end = find_subslice(data, b"\r\n")
            .ok_or_else(|| Error::parse("chunked: missing size line"))?;
        let size_hex = std::str::from_utf8(&data[..line_end])
            .map_err(|_| Error::parse("chunked: bad size line"))?
            .split(';')
            .next()
            .unwrap_or("")
            .trim();
        let size = usize::from_str_radix(size_hex, 16)
            .map_err(|_| Error::parse("chunked: invalid chunk size"))?;
        data = &data[line_end + 2..];
        if size == 0 {
            break;
        }
        if data.len() < size {
            return Err(Error::parse("chunked: truncated chunk"));
        }
        out.extend_from_slice(&data[..size]);
        data = &data[size..];
        if data.len() >= 2 {
            data = &data[2..];
        }
    }
    Ok(out)
}

fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    haystack.windows(needle.len()).position(|w| w == needle)
}
