use std::path::Path;
use std::time::Duration;

use doctate_common::join_url;
use reqwest::multipart::{Form, Part};
use tracing::debug;

#[derive(Debug)]
pub enum CanaryError {
    Io(std::io::Error),
    Http(reqwest::Error),
    Status { status: u16, body: String },
}

impl std::fmt::Display for CanaryError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io(e) => write!(f, "canary io error: {e}"),
            Self::Http(e) => write!(f, "canary http error: {e}"),
            Self::Status { status, body } => {
                write!(f, "canary returned {status}: {body}")
            }
        }
    }
}

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

impl CanaryError {
    /// Mirrors `WhisperError::is_transient`: `Http` (network), 5xx/408/429
    /// → transient (page-load heal re-enqueues); `Io` and other 4xx →
    /// permanent (rename to `.m4a.failed`).
    pub fn is_transient(&self) -> bool {
        match self {
            Self::Http(_) => true,
            Self::Status { status, .. } => {
                *status == 408 || *status == 429 || (500..=599).contains(status)
            }
            Self::Io(_) => false,
        }
    }
}

/// POST the audio file to the Canary `/inference` endpoint and return the
/// transcript text.
///
/// Form fields per docs/canary.md §2.4:
/// - `file`: the multipart upload (any container — Canary's service
///   ffmpegs to 16 kHz mono internally).
/// - `language`: defaults to `de` when the caller passes `None` or empty.
/// - `pnc=yes`: Punctuation and Capitalization, matching the Whisper path.
/// - `timestamps=no`: pipeline does not consume word/segment timestamps.
/// - `response_format=text`: plain-text body, structurally identical to
///   Whisper's `output=txt` response — no downstream parsing needed.
pub async fn transcribe(
    client: &reqwest::Client,
    canary_url: &str,
    audio: &Path,
    timeout: Duration,
    language: Option<&str>,
) -> Result<String, CanaryError> {
    let filename = audio
        .file_name()
        .map(|s| s.to_string_lossy().into_owned())
        .unwrap_or_else(|| "audio.m4a".to_string());

    let bytes = tokio::fs::read(audio).await.map_err(CanaryError::Io)?;

    let part = Part::bytes(bytes)
        .file_name(filename)
        .mime_str("audio/mp4")
        .map_err(CanaryError::Http)?;
    let lang = language.filter(|s| !s.is_empty()).unwrap_or("de");
    let form = Form::new()
        .part("file", part)
        .text("language", lang.to_owned())
        .text("pnc", "yes")
        .text("timestamps", "no")
        .text("response_format", "text");

    let url = join_url(canary_url, "/inference");
    debug!(%url, language = lang, "posting to canary");

    let response = client
        .post(&url)
        .timeout(timeout)
        .multipart(form)
        .send()
        .await
        .map_err(CanaryError::Http)?;

    let status = response.status();
    let body = response.text().await.map_err(CanaryError::Http)?;

    if !status.is_success() {
        return Err(CanaryError::Status {
            status: status.as_u16(),
            body,
        });
    }

    Ok(body)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn is_transient_classifies_status_codes() {
        let cases: &[(u16, bool)] = &[
            (400, false),
            (401, false),
            (403, false),
            (404, false),
            (408, true),
            (415, false),
            (422, false),
            (429, true),
            (500, true),
            (502, true),
            (503, true),
            (504, true),
        ];
        for &(status, expected) in cases {
            let err = CanaryError::Status {
                status,
                body: String::new(),
            };
            assert_eq!(
                err.is_transient(),
                expected,
                "status {status} expected is_transient={expected}"
            );
        }
    }

    #[test]
    fn is_transient_io_is_permanent() {
        let err = CanaryError::Io(std::io::Error::other("disk gone"));
        assert!(!err.is_transient());
    }
}
