use super::*;

pub(super) fn send_sse_headers_with_cors(
    stream: &mut impl Write,
    cors: bool,
) -> Result<(), String> {
    let cors = if cors {
        "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\n"
    } else {
        ""
    };
    stream
        .write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\n{cors}Connection: close\r\n\r\n").as_bytes())
        .map_err(|error| error.to_string())
}

pub(super) fn send_sse(stream: &mut impl Write, value: &Value) -> Result<(), String> {
    let body = serde_json::to_vec(value).map_err(|error| error.to_string())?;
    stream
        .write_all(b"data: ")
        .map_err(|error| error.to_string())?;
    stream.write_all(&body).map_err(|error| error.to_string())?;
    stream.write_all(b"\n\n").map_err(|error| error.to_string())
}

pub(super) fn send_sse_error(stream: &mut impl Write, message: &str) -> Result<(), String> {
    let body = serde_json::to_vec(&json!({"error": {"message": message, "type": "server_error"}}))
        .map_err(|error| error.to_string())?;
    stream
        .write_all(b"event: error\ndata: ")
        .map_err(|error| error.to_string())?;
    stream.write_all(&body).map_err(|error| error.to_string())?;
    stream.write_all(b"\n\n").map_err(|error| error.to_string())
}

pub(super) fn send_json_with_cors(
    stream: &mut TcpStream,
    code: u16,
    value: &Value,
    cors: bool,
) -> Result<(), String> {
    let mut body = serde_json::to_vec(value).map_err(|error| error.to_string())?;
    body.push(b'\n');
    send_response_with_cors(stream, code, Some("application/json"), &body, cors)
}

pub(super) fn send_error_with_cors(
    stream: &mut TcpStream,
    code: u16,
    message: &str,
    cors: bool,
) -> Result<(), String> {
    send_json_with_cors(
        stream,
        code,
        &json!({"error": {"message": message, "type": "invalid_request_error"}}),
        cors,
    )
}

pub(super) fn send_response_with_cors(
    stream: &mut impl Write,
    code: u16,
    content_type: Option<&str>,
    body: &[u8],
    cors: bool,
) -> Result<(), String> {
    let reason = match code {
        200 => "OK",
        204 => "No Content",
        400 => "Bad Request",
        404 => "Not Found",
        409 => "Conflict",
        500 => "Internal Server Error",
        _ => "Error",
    };
    let mut header = format!(
        "HTTP/1.1 {code} {reason}\r\nContent-Length: {}\r\n",
        body.len()
    );
    if let Some(content_type) = content_type {
        header.push_str("Content-Type: ");
        header.push_str(content_type);
        header.push_str("\r\n");
    }
    if cors {
        header.push_str("Access-Control-Allow-Origin: *\r\n");
        header.push_str("Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n");
        header.push_str("Access-Control-Allow-Headers: *\r\n");
    }
    header.push_str("Connection: close\r\n\r\n");
    stream
        .write_all(header.as_bytes())
        .and_then(|()| stream.write_all(body))
        .map_err(|error| error.to_string())
}

pub(super) fn read_request(
    stream: &mut TcpStream,
    deadline: Instant,
) -> Result<HttpRequest, String> {
    let mut bytes = Vec::new();
    let header_end = loop {
        let mut chunk = [0_u8; 4096];
        let read = read_with_deadline(stream, &mut chunk, deadline)?;
        if read == 0 {
            return Err("bad HTTP request".into());
        }
        bytes.extend_from_slice(&chunk[..read]);
        if let Some(end) = find_header_end(&bytes) {
            if end > MAX_HEADER_BYTES {
                return Err("HTTP headers are too large".into());
            }
            break end;
        }
        if bytes.len() >= MAX_HEADER_BYTES {
            return Err("HTTP headers are too large".into());
        }
    };
    let header = std::str::from_utf8(&bytes[..header_end])
        .map_err(|_| "HTTP headers are not UTF-8".to_owned())?;
    let mut lines = header.lines();
    let request_line = lines.next().ok_or_else(|| "bad HTTP request".to_owned())?;
    let mut parts = request_line.split_whitespace();
    let method = parts
        .next()
        .ok_or_else(|| "bad HTTP request".to_owned())?
        .to_owned();
    let path = parts
        .next()
        .ok_or_else(|| "bad HTTP request".to_owned())?
        .to_owned();
    let length = lines
        .find_map(|line| {
            line.split_once(':').and_then(|(name, value)| {
                name.eq_ignore_ascii_case("content-length")
                    .then(|| value.trim().parse::<usize>().ok())
                    .flatten()
            })
        })
        .unwrap_or(0);
    if length > MAX_BODY_BYTES {
        return Err("HTTP body is too large".into());
    }
    while bytes.len() < header_end + length {
        let mut chunk = [0_u8; 8192];
        let read = read_with_deadline(stream, &mut chunk, deadline)?;
        if read == 0 {
            return Err("incomplete HTTP body".into());
        }
        bytes.extend_from_slice(&chunk[..read]);
    }
    Ok(HttpRequest {
        method,
        path: path.split('?').next().unwrap_or(&path).to_owned(),
        body: bytes[header_end..header_end + length].to_vec(),
    })
}

fn read_with_deadline(
    stream: &mut TcpStream,
    buffer: &mut [u8],
    deadline: Instant,
) -> Result<usize, String> {
    let remaining = deadline.saturating_duration_since(Instant::now());
    if remaining.is_zero() {
        return Err("HTTP request timed out".into());
    }
    stream
        .set_read_timeout(Some(remaining.min(HTTP_IO_TIMEOUT)))
        .map_err(|error| error.to_string())?;
    stream.read(buffer).map_err(|error| {
        if matches!(
            error.kind(),
            std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
        ) {
            "HTTP request timed out".into()
        } else {
            error.to_string()
        }
    })
}

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

pub(super) fn unix_time() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

pub(super) fn random_id(prefix: &str) -> String {
    random_hex_id::<12>(prefix)
}

pub(super) fn random_tool_id(prefix: &str) -> String {
    random_hex_id::<16>(prefix)
}

fn random_hex_id<const N: usize>(prefix: &str) -> String {
    let mut bytes = [0_u8; N];
    if File::open("/dev/urandom")
        .and_then(|mut file| file.read_exact(&mut bytes))
        .is_err()
    {
        let fallback = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        bytes.copy_from_slice(&fallback.to_le_bytes()[..N]);
    }
    let mut id = String::with_capacity(prefix.len() + N * 2);
    id.push_str(prefix);
    for byte in bytes {
        use std::fmt::Write as _;
        let _ = write!(id, "{byte:02x}");
    }
    id
}
