use std::{process::Stdio, sync::Arc};

use axum::{
    Json,
    body::Body,
    extract::{Path, Query, State},
    http::header,
    response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tokio_util::io::ReaderStream;

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

mod discovery;
mod matching;

#[derive(Serialize)]
pub struct Subtitle {
    index: Option<u32>,
    id: Option<String>,
    source: &'static str,
    codec: String,
    language: Option<String>,
    title: Option<String>,
    supported: bool,
    confidence: Option<&'static str>,
    score: Option<u8>,
}

#[derive(Deserialize)]
pub struct ExtractQuery {
    path: String,
    #[serde(default)]
    start: f64,
}

pub async fn list(
    State(state): State<Arc<AppState>>,
    Query(query): Query<PathQuery>,
) -> Result<Json<Vec<Subtitle>>, AppError> {
    let media = path::resolve(&state.config, &query.path)?;
    let info = probe::run(&state.config, &media).await?;
    let mut subtitles = info
        .streams
        .into_iter()
        .filter(|stream| stream.codec_type.as_deref() == Some("subtitle"))
        .map(|stream| {
            let codec = stream.codec_name.unwrap_or_else(|| "unknown".into());
            Subtitle {
                index: Some(stream.index),
                id: None,
                source: "embedded",
                supported: matches!(
                    codec.as_str(),
                    "subrip" | "srt" | "ass" | "ssa" | "webvtt" | "mov_text"
                ),
                language: tag(&stream.tags, "language"),
                title: tag(&stream.tags, "title"),
                codec,
                confidence: None,
                score: None,
            }
        })
        .collect::<Vec<_>>();
    subtitles.extend(
        discovery::find(&state.config, &media)
            .await?
            .into_iter()
            .map(|candidate| Subtitle {
                index: None,
                id: Some(candidate.id),
                source: "external",
                codec: candidate.codec,
                language: None,
                title: Some(candidate.label),
                supported: true,
                confidence: Some(candidate.confidence.as_str()),
                score: Some(candidate.score),
            }),
    );
    Ok(Json(subtitles))
}

pub async fn extract(
    State(state): State<Arc<AppState>>,
    Path(index): Path<u32>,
    Query(query): Query<ExtractQuery>,
) -> Result<Response, AppError> {
    if !query.start.is_finite() || query.start < 0.0 {
        return Err(AppError::bad_request("invalid subtitle start position"));
    }
    let media = path::resolve(&state.config, &query.path)?;
    stream_webvtt(
        &state.config.ffmpeg,
        &media,
        &format!("0:{index}"),
        query.start,
    )
    .await
}

pub async fn extract_external(
    State(state): State<Arc<AppState>>,
    Path(id): Path<String>,
    Query(query): Query<ExtractQuery>,
) -> Result<Response, AppError> {
    if !query.start.is_finite() || query.start < 0.0 {
        return Err(AppError::bad_request("invalid subtitle start position"));
    }
    let media = path::resolve(&state.config, &query.path)?;
    let candidate = discovery::resolve_id(&state.config, &media, &id).await?;
    stream_webvtt(&state.config.ffmpeg, &candidate.path, "0:0", query.start).await
}

async fn stream_webvtt(
    ffmpeg: &std::path::Path,
    input: &std::path::Path,
    map: &str,
    start: f64,
) -> Result<Response, AppError> {
    let mut command = Command::new(ffmpeg);
    command.args(["-v", "error"]);
    if start > 0.0 {
        command.args(["-ss", &format!("{start:.3}")]);
    }
    let mut child = command
        .arg("-i")
        .arg(input)
        .args(["-map", map, "-f", "webvtt", "pipe:1"])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .map_err(AppError::internal)?;
    let stdout = child
        .stdout
        .take()
        .ok_or_else(|| AppError::internal("ffmpeg stdout unavailable"))?;
    tokio::spawn(async move {
        let _ = child.wait().await;
    });
    Ok((
        [(header::CONTENT_TYPE, "text/vtt; charset=utf-8")],
        Body::from_stream(ReaderStream::new(stdout)),
    )
        .into_response())
}

fn tag(tags: &serde_json::Map<String, serde_json::Value>, name: &str) -> Option<String> {
    tags.get(name)
        .or_else(|| tags.get(&name.to_uppercase()))
        .and_then(|value| value.as_str())
        .map(str::to_owned)
}
