//! A minimal loopback HTTP server for rewritten HLS playlists.
//!
//! When a streaming quality cap is set we hand the player a filtered master
//! playlist rather than YouTube's. That playlist has to be reachable over
//! `http://`: Safari's native HLS is backed by AVFoundation, which cannot read
//! `blob:` or custom-scheme URLs — only a real HTTP one. Hence ~80 lines of
//! server rather than a one-line Blob URL.
//!
//! It binds to 127.0.0.1 on an ephemeral port and only ever serves playlists it
//! was explicitly handed, addressed by an unguessable token.

use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::sync::Mutex;

/// Playlists are small; keeping the last few is plenty, and it stops a long
/// session from growing the map without bound.
const MAX_ENTRIES: usize = 8;

#[derive(Clone)]
pub struct PlaylistServer {
    port: u16,
    entries: Arc<Mutex<Vec<(String, String)>>>,
}

impl PlaylistServer {
    pub fn start() -> Result<Self, String> {
        let listener = std::net::TcpListener::bind("127.0.0.1:0")
            .map_err(|e| format!("Cannot bind playlist server: {e}"))?;
        listener
            .set_nonblocking(true)
            .map_err(|e| format!("Cannot configure playlist server: {e}"))?;
        let port = listener
            .local_addr()
            .map_err(|e| format!("Cannot read playlist server port: {e}"))?
            .port();

        let server = PlaylistServer {
            port,
            entries: Arc::new(Mutex::new(Vec::new())),
        };

        let entries = server.entries.clone();
        tauri::async_runtime::spawn(async move {
            let Ok(listener) = TcpListener::from_std(listener) else {
                return;
            };
            loop {
                let Ok((stream, _)) = listener.accept().await else {
                    continue;
                };
                let entries = entries.clone();
                tauri::async_runtime::spawn(async move {
                    let _ = serve(stream, entries).await;
                });
            }
        });

        Ok(server)
    }

    /// Publishes a playlist and returns the URL to hand the player.
    pub async fn publish(&self, body: String) -> String {
        let token = token();
        let mut entries = self.entries.lock().await;
        entries.push((token.clone(), body));
        if entries.len() > MAX_ENTRIES {
            entries.remove(0);
        }
        format!("http://127.0.0.1:{}/{token}.m3u8", self.port)
    }
}

/// Not cryptographic, just unguessable enough that another local process
/// cannot stumble onto a playlist by iterating short paths.
fn token() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    let addr = Box::into_raw(Box::new(0u8)) as usize;
    // Reclaim the allocation used purely as an address source.
    unsafe { drop(Box::from_raw(addr as *mut u8)) };
    format!("{nanos:x}{addr:x}")
}

async fn serve(
    mut stream: tokio::net::TcpStream,
    entries: Arc<Mutex<Vec<(String, String)>>>,
) -> std::io::Result<()> {
    let mut buf = vec![0u8; 2048];
    let n = stream.read(&mut buf).await?;
    let request = String::from_utf8_lossy(&buf[..n]);

    // "GET /<token>.m3u8 HTTP/1.1"
    let path = request
        .lines()
        .next()
        .and_then(|l| l.split_whitespace().nth(1))
        .unwrap_or("");
    let wanted = path.trim_start_matches('/').trim_end_matches(".m3u8");

    let body = {
        let entries = entries.lock().await;
        entries
            .iter()
            .find(|(t, _)| t == wanted)
            .map(|(_, b)| b.clone())
    };

    let response = match body {
        Some(body) => format!(
            "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.apple.mpegurl\r\n\
             Content-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n{}",
            body.len(),
            body
        ),
        None => "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".into(),
    };

    stream.write_all(response.as_bytes()).await?;
    stream.flush().await
}
