use std::sync::Arc;

use axum::{
    body::Body,
    extract::{Query, State},
    http::{HeaderMap, StatusCode, header},
    response::Response,
};
use tokio::{
    fs::File,
    io::{AsyncReadExt, AsyncSeekExt, SeekFrom},
};

use crate::{
    app::AppState,
    error::AppError,
    media::{PathQuery, path},
};

pub async fn stream(
    State(state): State<Arc<AppState>>,
    Query(query): Query<PathQuery>,
    headers: HeaderMap,
) -> Result<Response<Body>, AppError> {
    let media = path::resolve(&state.config, &query.path)?;
    if !media.is_file() {
        return Err(AppError::bad_request("path is not a file"));
    }
    let mut file = File::open(&media).await.map_err(AppError::internal)?;
    let size = file.metadata().await.map_err(AppError::internal)?.len();
    let (start, end, status) = match headers.get(header::RANGE).and_then(|v| v.to_str().ok()) {
        Some(value) => {
            let (start, end) = parse_range(value, size)?;
            (start, end, StatusCode::PARTIAL_CONTENT)
        }
        None => (0, size.saturating_sub(1), StatusCode::OK),
    };
    let length = end - start + 1;
    file.seek(SeekFrom::Start(start))
        .await
        .map_err(AppError::internal)?;
    let body = Body::from_stream(tokio_util::io::ReaderStream::new(file.take(length)));
    let mime = mime_guess::from_path(&media).first_or_octet_stream();

    let mut response = Response::builder()
        .status(status)
        .header(header::ACCEPT_RANGES, "bytes")
        .header(header::CONTENT_LENGTH, length)
        .header(header::CONTENT_TYPE, mime.as_ref());
    if status == StatusCode::PARTIAL_CONTENT {
        response = response.header(header::CONTENT_RANGE, format!("bytes {start}-{end}/{size}"));
    }
    response.body(body).map_err(AppError::internal)
}

fn parse_range(value: &str, size: u64) -> Result<(u64, u64), AppError> {
    let value = value
        .strip_prefix("bytes=")
        .ok_or_else(|| AppError::bad_request("invalid range"))?;
    let (start, end) = value
        .split_once('-')
        .ok_or_else(|| AppError::bad_request("invalid range"))?;
    let start = start
        .parse::<u64>()
        .map_err(|_| AppError::bad_request("invalid range"))?;
    let end = if end.is_empty() {
        size.saturating_sub(1)
    } else {
        end.parse()
            .map_err(|_| AppError::bad_request("invalid range"))?
    };
    if size == 0 || start > end || end >= size {
        return Err(AppError::bad_request("range outside file"));
    }
    Ok((start, end))
}
