//! HTTP byte-range file streaming for `<video>` elements.
//!
//! Python's stock `http.server` returns `HTTP/1.0 200` with the full
//! body for any request, which makes Chromium's `<video>` stall on
//! seek because it's waiting for a `206 Partial Content` response to
//! a `Range: bytes=a-b` request that never comes. We hit this bug
//! live in the session that produced this module — the voiceover
//! subcommand has always done the right thing because it used this
//! code; the storyboard workflow didn't, and we had to write a
//! throwaway Python range-capable server to work around it before
//! this module existed.

use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use tiny_http::{Header, Request, Response, StatusCode};

/// Stream a file in response to a request, respecting any
/// `Range: bytes=a-b` header. Always sends `Accept-Ranges: bytes`
/// so browsers know seeking is supported. Returns 206 Partial
/// Content if the request had a range header, 200 otherwise.
pub fn serve_file_range(request: Request, path: &Path, content_type: &str) -> Result<(), String> {
    let mut file = File::open(path).map_err(|e| format!("open {}: {}", path.display(), e))?;
    let total_len = file.metadata().map_err(|e| e.to_string())?.len();

    let range = request
        .headers()
        .iter()
        .find(|h| h.field.as_str().as_str().eq_ignore_ascii_case("Range"))
        .and_then(|h| parse_range(h.value.as_str(), total_len));

    let (start, end) = range.unwrap_or((0, total_len.saturating_sub(1)));
    let length = end - start + 1;

    file.seek(SeekFrom::Start(start)).map_err(|e| e.to_string())?;
    let body = ReadLimit {
        inner: file,
        remaining: length,
    };

    let mut resp = Response::new(
        StatusCode(if range.is_some() { 206 } else { 200 }),
        vec![
            Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes()).unwrap(),
            Header::from_bytes(&b"Accept-Ranges"[..], &b"bytes"[..]).unwrap(),
            Header::from_bytes(&b"Cache-Control"[..], &b"no-cache"[..]).unwrap(),
        ],
        body,
        Some(length as usize),
        None,
    );
    if range.is_some() {
        let cr = format!("bytes {}-{}/{}", start, end, total_len);
        resp.add_header(Header::from_bytes(&b"Content-Range"[..], cr.as_bytes()).unwrap());
    }
    request.respond(resp).map_err(|e| e.to_string())
}

/// Wrap a `Read` source so at most `remaining` bytes will ever be
/// yielded. Used to cap tiny_http's streaming body at the end of
/// the requested range.
struct ReadLimit<R: Read> {
    inner: R,
    remaining: u64,
}

impl<R: Read> Read for ReadLimit<R> {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        if self.remaining == 0 {
            return Ok(0);
        }
        let max = std::cmp::min(buf.len() as u64, self.remaining) as usize;
        let n = self.inner.read(&mut buf[..max])?;
        self.remaining -= n as u64;
        Ok(n)
    }
}

/// Parse an HTTP `Range: bytes=a-b` header into inclusive `(start, end)`
/// offsets. Returns `None` for malformed or out-of-range values —
/// the caller then falls back to serving the whole file.
fn parse_range(header: &str, total: u64) -> Option<(u64, u64)> {
    let s = header.strip_prefix("bytes=")?;
    let mut parts = s.split('-');
    let start = parts.next()?.parse::<u64>().ok()?;
    let end = parts
        .next()
        .and_then(|p| if p.is_empty() { None } else { p.parse::<u64>().ok() })
        .unwrap_or(total.saturating_sub(1));
    if start > end || end >= total {
        return None;
    }
    Some((start, end))
}