use std::fmt;
use std::io::SeekFrom;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
use tokio_rustls::TlsConnector;
use url::Url;

use crate::proxy::{connect, ProxyConfig};
use crate::transport::build_tls_config;

const RESPONSE_LIMIT: usize = 16 * 1024 * 1024;

pub type ProgressFn = Arc<dyn Fn(u64, u64) + Send + Sync>;

pub struct UploadResponse {
    pub status: u16,
    pub body: Vec<u8>,
}

#[derive(Debug)]
pub enum MediaError {
    InvalidUrl(String),
    Io(std::io::Error),
    Transport(String),
    Response(String),
}

impl fmt::Display for MediaError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidUrl(message) => write!(formatter, "invalid upload URL: {message}"),
            Self::Io(error) => error.fmt(formatter),
            Self::Transport(message) => formatter.write_str(message),
            Self::Response(message) => write!(formatter, "invalid HTTP response: {message}"),
        }
    }
}

impl std::error::Error for MediaError {}

impl From<std::io::Error> for MediaError {
    fn from(error: std::io::Error) -> Self {
        Self::Io(error)
    }
}

trait NetworkStream: AsyncRead + AsyncWrite + Unpin + Send {}
impl<T: AsyncRead + AsyncWrite + Unpin + Send> NetworkStream for T {}

struct UploadUrl {
    host: String,
    port: u16,
    path: String,
    tls: bool,
}

impl UploadUrl {
    fn parse(source: &str) -> Result<Self, MediaError> {
        let url = Url::parse(source).map_err(|error| MediaError::InvalidUrl(error.to_string()))?;
        let tls = match url.scheme() {
            "https" => true,
            "http" => false,
            scheme => {
                return Err(MediaError::InvalidUrl(format!(
                    "unsupported scheme {scheme}"
                )))
            }
        };
        let host = url
            .host_str()
            .ok_or_else(|| MediaError::InvalidUrl("host is missing".to_string()))?
            .to_string();
        let port = url
            .port_or_known_default()
            .ok_or_else(|| MediaError::InvalidUrl("port is missing".to_string()))?;
        let mut path = url.path().to_string();
        if path.is_empty() {
            path.push('/');
        }
        if let Some(query) = url.query() {
            path.push('?');
            path.push_str(query);
        }
        Ok(Self {
            host,
            port,
            path,
            tls,
        })
    }
}

pub async fn upload_file(
    url: &str,
    data: &[u8],
    filename: &str,
    insecure_tls: bool,
    proxy: Option<&ProxyConfig>,
    user_agent: &str,
    progress: ProgressFn,
) -> Result<UploadResponse, MediaError> {
    let total = data.len() as u64;
    let headers = vec![
        (
            "Content-Type",
            "application/x-binary; charset=x-user-defined".to_string(),
        ),
        (
            "Content-Disposition",
            format!("attachment; filename={filename}"),
        ),
        ("Connection", "keep-alive".to_string()),
        ("User-Agent", percent_encode(user_agent)),
        (
            "Content-Range",
            format!("bytes 0-{}/{}", total.saturating_sub(1), total),
        ),
    ];
    request_bytes(
        url,
        "POST",
        headers,
        &[data],
        total,
        insecure_tls,
        proxy,
        &progress,
    )
    .await
}

pub async fn upload_file_path(
    url: &str,
    path: &str,
    filename: &str,
    content_type: Option<&str>,
    connection: Option<&str>,
    insecure_tls: bool,
    proxy: Option<&ProxyConfig>,
    user_agent: &str,
    progress: ProgressFn,
) -> Result<UploadResponse, MediaError> {
    let file = File::open(path).await?;
    let total = file.metadata().await?.len();
    let headers = vec![
        (
            "Content-Type",
            content_type
                .unwrap_or("application/x-binary; charset=x-user-defined")
                .to_string(),
        ),
        (
            "Content-Disposition",
            format!("attachment; filename={filename}"),
        ),
        ("Connection", connection.unwrap_or("keep-alive").to_string()),
        ("User-Agent", percent_encode(user_agent)),
        (
            "Content-Range",
            format!("bytes 0-{}/{}", total.saturating_sub(1), total),
        ),
    ];
    request_file(
        url,
        "POST",
        headers,
        &[],
        file,
        total,
        &[],
        insecure_tls,
        proxy,
        &progress,
    )
    .await
}

pub async fn upload_photo(
    url: &str,
    data: &[u8],
    filename: &str,
    insecure_tls: bool,
    proxy: Option<&ProxyConfig>,
    user_agent: &str,
    progress: ProgressFn,
) -> Result<UploadResponse, MediaError> {
    let boundary = boundary();
    let prefix = multipart_prefix(&boundary, filename);
    let suffix = format!("\r\n--{boundary}--\r\n").into_bytes();
    let total = prefix.len() as u64 + data.len() as u64 + suffix.len() as u64;
    let headers = vec![
        (
            "Content-Type",
            format!("multipart/form-data; boundary={boundary}"),
        ),
        ("Connection", "keep-alive".to_string()),
        ("User-Agent", percent_encode(user_agent)),
    ];
    request_bytes(
        url,
        "POST",
        headers,
        &[&prefix, data, &suffix],
        total,
        insecure_tls,
        proxy,
        &progress,
    )
    .await
}

pub async fn upload_photo_path(
    url: &str,
    path: &str,
    filename: &str,
    insecure_tls: bool,
    proxy: Option<&ProxyConfig>,
    user_agent: &str,
    progress: ProgressFn,
) -> Result<UploadResponse, MediaError> {
    let file = File::open(path).await?;
    let file_len = file.metadata().await?.len();
    let boundary = boundary();
    let prefix = multipart_prefix(&boundary, filename);
    let suffix = format!("\r\n--{boundary}--\r\n").into_bytes();
    let headers = vec![
        (
            "Content-Type",
            format!("multipart/form-data; boundary={boundary}"),
        ),
        ("Connection", "keep-alive".to_string()),
        ("User-Agent", percent_encode(user_agent)),
    ];
    request_file(
        url,
        "POST",
        headers,
        &prefix,
        file,
        file_len,
        &suffix,
        insecure_tls,
        proxy,
        &progress,
    )
    .await
}

pub async fn upload_video_path(
    url: &str,
    path: &str,
    chunk_size: usize,
    insecure_tls: bool,
    proxy: Option<&ProxyConfig>,
    progress: ProgressFn,
) -> Result<UploadResponse, MediaError> {
    if chunk_size == 0 {
        return Err(MediaError::Response("chunk size is zero".to_string()));
    }
    let total = tokio::fs::metadata(path).await?.len();
    if total == 0 {
        return Err(MediaError::Response("file is empty".to_string()));
    }
    let filename = now_micros().to_string();
    let base_headers = vec![
        (
            "Content-Type",
            "application/x-binary; charset=x-user-defined".to_string(),
        ),
        (
            "Content-Disposition",
            format!("attachment; fileName=\"{filename}\""),
        ),
        ("X-Uploading-Mode", "parallel".to_string()),
        ("Connection", "close".to_string()),
    ];
    let quiet: ProgressFn = Arc::new(|_, _| {});
    let handshake = request_bytes(
        url,
        "GET",
        base_headers.clone(),
        &[],
        0,
        insecure_tls,
        proxy,
        &quiet,
    )
    .await?;
    if handshake.status != 200 {
        return Ok(handshake);
    }
    let start = String::from_utf8_lossy(&handshake.body)
        .trim()
        .parse::<u64>()
        .ok()
        .filter(|value| *value <= total)
        .unwrap_or(0);
    let mut offset = start;
    let mut last = handshake;
    while offset < total {
        let length = (chunk_size as u64).min(total - offset);
        let mut file = File::open(path).await?;
        file.seek(SeekFrom::Start(offset)).await?;
        let mut headers = base_headers.clone();
        headers.push((
            "Content-Range",
            format!("bytes {offset}-{}/{}", offset + length - 1, total),
        ));
        let outer_progress = progress.clone();
        let chunk_progress: ProgressFn =
            Arc::new(move |sent, _| outer_progress(offset + sent, total));
        last = request_file(
            url,
            "POST",
            headers,
            &[],
            file,
            length,
            &[],
            insecure_tls,
            proxy,
            &chunk_progress,
        )
        .await?;
        if last.status != 200 && last.status != 201 {
            return Ok(last);
        }
        offset += length;
        progress(offset, total);
    }
    Ok(UploadResponse {
        status: 200,
        body: last.body,
    })
}

async fn request_bytes(
    source: &str,
    method: &str,
    headers: Vec<(&str, String)>,
    parts: &[&[u8]],
    total: u64,
    insecure_tls: bool,
    proxy: Option<&ProxyConfig>,
    progress: &ProgressFn,
) -> Result<UploadResponse, MediaError> {
    let url = UploadUrl::parse(source)?;
    let mut stream = open_stream(&url, insecure_tls, proxy).await?;
    write_head(&mut stream, &url, method, &headers, total).await?;
    let mut sent = 0;
    for part in parts {
        stream.write_all(part).await?;
        sent += part.len() as u64;
        progress(sent, total);
    }
    stream.flush().await?;
    read_response(&mut stream).await
}

async fn request_file(
    source: &str,
    method: &str,
    headers: Vec<(&str, String)>,
    prefix: &[u8],
    mut file: File,
    file_len: u64,
    suffix: &[u8],
    insecure_tls: bool,
    proxy: Option<&ProxyConfig>,
    progress: &ProgressFn,
) -> Result<UploadResponse, MediaError> {
    let url = UploadUrl::parse(source)?;
    let total = prefix.len() as u64 + file_len + suffix.len() as u64;
    let mut stream = open_stream(&url, insecure_tls, proxy).await?;
    write_head(&mut stream, &url, method, &headers, total).await?;
    stream.write_all(prefix).await?;
    let mut sent = prefix.len() as u64;
    let mut remaining = file_len;
    let mut buffer = vec![0u8; 64 * 1024];
    while remaining > 0 {
        let requested = remaining.min(buffer.len() as u64) as usize;
        let count = file.read(&mut buffer[..requested]).await?;
        if count == 0 {
            return Err(MediaError::Response(
                "file ended before declared length".to_string(),
            ));
        }
        stream.write_all(&buffer[..count]).await?;
        remaining -= count as u64;
        sent += count as u64;
        progress(sent, total);
    }
    stream.write_all(suffix).await?;
    progress(total, total);
    stream.flush().await?;
    read_response(&mut stream).await
}

async fn open_stream(
    url: &UploadUrl,
    insecure_tls: bool,
    proxy: Option<&ProxyConfig>,
) -> Result<Box<dyn NetworkStream>, MediaError> {
    let tcp = connect(&url.host, url.port, proxy)
        .await
        .map_err(|error| MediaError::Transport(error.to_string()))?;
    if !url.tls {
        return Ok(Box::new(tcp));
    }
    let name = rustls::pki_types::ServerName::try_from(url.host.clone())
        .map_err(|error| MediaError::Transport(error.to_string()))?;
    let connector = TlsConnector::from(
        build_tls_config(insecure_tls).map_err(|error| MediaError::Transport(error.to_string()))?,
    );
    let tls = connector
        .connect(name, tcp)
        .await
        .map_err(|error| MediaError::Transport(error.to_string()))?;
    Ok(Box::new(tls))
}

async fn write_head(
    stream: &mut Box<dyn NetworkStream>,
    url: &UploadUrl,
    method: &str,
    headers: &[(&str, String)],
    content_length: u64,
) -> Result<(), MediaError> {
    let mut head = format!("{method} {} HTTP/1.1\r\nHost: {}\r\n", url.path, url.host);
    for (name, value) in headers {
        head.push_str(name);
        head.push_str(": ");
        head.push_str(value);
        head.push_str("\r\n");
    }
    head.push_str(&format!("Content-Length: {content_length}\r\n\r\n"));
    stream.write_all(head.as_bytes()).await?;
    Ok(())
}

async fn read_response(stream: &mut Box<dyn NetworkStream>) -> Result<UploadResponse, MediaError> {
    let mut data = Vec::new();
    let mut buffer = vec![0u8; 16 * 1024];
    loop {
        let count = tokio::time::timeout(Duration::from_secs(120), stream.read(&mut buffer))
            .await
            .map_err(|_| MediaError::Response("timeout".to_string()))??;
        if count == 0 {
            break;
        }
        data.extend_from_slice(&buffer[..count]);
        if data.len() > RESPONSE_LIMIT {
            return Err(MediaError::Response("body exceeds limit".to_string()));
        }
        if response_complete(&data) {
            break;
        }
    }
    parse_response(&data)
}

fn response_complete(data: &[u8]) -> bool {
    let Some(end) = find(data, b"\r\n\r\n") else {
        return false;
    };
    let headers = String::from_utf8_lossy(&data[..end]);
    if headers
        .to_ascii_lowercase()
        .contains("transfer-encoding: chunked")
    {
        return data[end + 4..]
            .windows(5)
            .any(|window| window == b"0\r\n\r\n");
    }
    let length = headers.lines().find_map(|line| {
        let (name, value) = line.split_once(':')?;
        name.eq_ignore_ascii_case("content-length")
            .then(|| value.trim().parse::<usize>().ok())
            .flatten()
    });
    length.is_some_and(|length| data.len() >= end + 4 + length)
}

fn parse_response(data: &[u8]) -> Result<UploadResponse, MediaError> {
    let header_end = find(data, b"\r\n\r\n")
        .ok_or_else(|| MediaError::Response("headers are incomplete".to_string()))?;
    let headers = String::from_utf8_lossy(&data[..header_end]);
    let status = headers
        .lines()
        .next()
        .and_then(|line| line.split_whitespace().nth(1))
        .and_then(|value| value.parse().ok())
        .ok_or_else(|| MediaError::Response("status is missing".to_string()))?;
    let raw_body = &data[header_end + 4..];
    let body = if headers
        .to_ascii_lowercase()
        .contains("transfer-encoding: chunked")
    {
        decode_chunked(raw_body)?
    } else {
        raw_body.to_vec()
    };
    Ok(UploadResponse { status, body })
}

fn decode_chunked(data: &[u8]) -> Result<Vec<u8>, MediaError> {
    let mut output = Vec::new();
    let mut offset = 0;
    loop {
        let line_end = find(&data[offset..], b"\r\n")
            .map(|value| offset + value)
            .ok_or_else(|| MediaError::Response("chunk size is incomplete".to_string()))?;
        let size = usize::from_str_radix(
            String::from_utf8_lossy(&data[offset..line_end])
                .split(';')
                .next()
                .unwrap_or_default(),
            16,
        )
        .map_err(|error| MediaError::Response(error.to_string()))?;
        if size == 0 {
            return Ok(output);
        }
        let start = line_end + 2;
        let end = start + size;
        if end + 2 > data.len() {
            return Err(MediaError::Response("chunk is incomplete".to_string()));
        }
        output.extend_from_slice(&data[start..end]);
        offset = end + 2;
    }
}

fn multipart_prefix(boundary: &str, filename: &str) -> Vec<u8> {
    format!(
        "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\nContent-Type: {}\r\n\r\n",
        content_type(filename),
    )
    .into_bytes()
}

fn content_type(filename: &str) -> &'static str {
    match filename
        .rsplit('.')
        .next()
        .unwrap_or_default()
        .to_ascii_lowercase()
        .as_str()
    {
        "png" => "image/png",
        "gif" => "image/gif",
        "webp" => "image/webp",
        "heic" | "heif" => "image/heic",
        "bmp" => "image/bmp",
        _ => "image/jpeg",
    }
}

fn boundary() -> String {
    format!("----KusoftBoundary{}", now_micros())
}

fn now_micros() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_micros() as u64 & 0x7fff_ffff)
        .unwrap_or_default()
}

fn percent_encode(value: &str) -> String {
    url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
}

fn find(data: &[u8], needle: &[u8]) -> Option<usize> {
    data.windows(needle.len())
        .position(|window| window == needle)
}

#[cfg(test)]
mod tests {
    use std::sync::{Arc, Mutex};

    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::TcpListener;

    use super::{decode_chunked, parse_response, upload_file, ProgressFn};

    #[test]
    fn parses_content_length_response() {
        let response = parse_response(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n{}").unwrap();
        assert_eq!(response.status, 200);
        assert_eq!(response.body, b"{}");
    }

    #[test]
    fn decodes_chunked_response() {
        assert_eq!(decode_chunked(b"2\r\n{}\r\n0\r\n\r\n").unwrap(), b"{}");
    }

    #[tokio::test]
    async fn streams_synthetic_upload_to_http_server() {
        let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
        let address = listener.local_addr().unwrap();
        let server = tokio::spawn(async move {
            let (mut stream, _) = listener.accept().await.unwrap();
            let mut request = Vec::new();
            let mut buffer = [0u8; 1024];
            loop {
                let count = stream.read(&mut buffer).await.unwrap();
                assert!(count > 0);
                request.extend_from_slice(&buffer[..count]);
                if let Some(header_end) = super::find(&request, b"\r\n\r\n") {
                    let headers = String::from_utf8_lossy(&request[..header_end]);
                    let length = headers
                        .lines()
                        .find_map(|line| {
                            let (name, value) = line.split_once(':')?;
                            name.eq_ignore_ascii_case("content-length")
                                .then(|| value.trim().parse::<usize>().unwrap())
                        })
                        .unwrap();
                    if request.len() >= header_end + 4 + length {
                        assert!(headers.contains("Content-Range: bytes 0-3/4"));
                        assert_eq!(&request[header_end + 4..header_end + 8], b"test");
                        break;
                    }
                }
            }
            stream
                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}")
                .await
                .unwrap();
        });
        let progress_values = Arc::new(Mutex::new(Vec::new()));
        let output = progress_values.clone();
        let progress: ProgressFn = Arc::new(move |sent, total| {
            output.lock().unwrap().push((sent, total));
        });
        let response = upload_file(
            &format!("http://{address}/upload"),
            b"test",
            "synthetic.bin",
            false,
            None,
            "Synthetic/1.0",
            progress,
        )
        .await
        .unwrap();
        assert_eq!(response.status, 200);
        assert_eq!(response.body, b"{}");
        assert_eq!(progress_values.lock().unwrap().last(), Some(&(4, 4)));
        server.await.unwrap();
    }
}
