use crate::config::{Config, VALID_PARAKEET_MODELS};
use crate::error::TranscribeError;
use crate::health::HealthItem;
use crate::markdown::ContentType;
use crate::parakeet;
use crate::transcribe::{self, TranscribeResult};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};

use whisper_guard::segments as wg_segments;

#[derive(Debug, Clone)]
pub struct TranscriptionRequest {
    pub audio_path: PathBuf,
    pub content_type: ContentType,
    pub decode_hints: transcribe::DecodeHints,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParakeetBackendStatus {
    pub backend_id: String,
    pub compiled: bool,
    pub model: String,
    /// True once the backend has completed at least one transcription this
    /// process lifetime. NOT "a warm server is resident" — use `sidecar`
    /// for the user-facing throughput story (#295).
    pub warm: bool,
    /// Human-readable effective sidecar state: what live throughput will
    /// actually do, and why.
    pub sidecar: String,
    /// Effective sidecar decision (auto-resolved or explicit).
    pub sidecar_enabled: bool,
    /// Whether the example-server binary resolves on this machine.
    pub sidecar_binary_found: bool,
    pub ready: bool,
    pub binary: String,
    pub binary_found: bool,
    pub model_found: bool,
    pub tokenizer_found: bool,
    pub binary_path: Option<String>,
    pub model_path: Option<String>,
    pub tokenizer_path: Option<String>,
    pub tokenizer_label: Option<String>,
    pub install_dir: String,
    pub setup_command: String,
    pub guide_url: String,
    pub issues: Vec<String>,
    pub metadata: Option<parakeet::ParakeetInstallMetadata>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BackendWarmupResult {
    pub backend_id: String,
    pub model: String,
    pub elapsed_ms: u64,
    pub used_gpu: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TranscriptionDiagnosticsSnapshot {
    pub backend_id: String,
    pub model: String,
    pub ready: bool,
    pub warm: bool,
    pub used_gpu: bool,
    pub chunking_strategy: String,
    pub issues: Vec<String>,
    pub metadata: Option<parakeet::ParakeetInstallMetadata>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParakeetBenchmarkReport {
    pub backend_id: String,
    pub model: String,
    pub gpu: bool,
    pub direct_elapsed_ms: u64,
    pub direct_segments: usize,
    pub helper_elapsed_ms: u64,
    pub helper_segments: usize,
}

fn warmed_backends() -> &'static Mutex<std::collections::HashSet<String>> {
    static WARMED: OnceLock<Mutex<std::collections::HashSet<String>>> = OnceLock::new();
    WARMED.get_or_init(|| Mutex::new(std::collections::HashSet::new()))
}

fn warm_key(backend_id: &str, model: &str) -> String {
    format!("{backend_id}:{model}")
}

#[cfg(feature = "parakeet")]
fn mark_backend_warm(backend_id: &str, model: &str) {
    let mut warmed = warmed_backends()
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    warmed.insert(warm_key(backend_id, model));
}

fn backend_is_warm(backend_id: &str, model: &str) -> bool {
    let warmed = warmed_backends()
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    warmed.contains(&warm_key(backend_id, model))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TranscriptCleanupStage {
    DedupSegments,
    DedupInterleaved,
    StripForeignScript,
    CollapseNoiseMarkers,
    TrimTrailingNoise,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TranscriptCleanupStageStat {
    pub stage: TranscriptCleanupStage,
    pub before: usize,
    pub after: usize,
}

#[derive(Debug, Clone)]
pub(crate) struct TranscriptCleanupResult {
    pub lines: Vec<String>,
    pub stats: Vec<TranscriptCleanupStageStat>,
}

type TranscriptCleanupFn = fn(Vec<String>) -> Vec<String>;
type TranscriptCleanupStep = (TranscriptCleanupStage, TranscriptCleanupFn);

pub(crate) fn dedup_segments(lines: Vec<String>) -> Vec<String> {
    wg_segments::dedup_segments(&lines)
}

pub(crate) fn dedup_interleaved(lines: Vec<String>) -> Vec<String> {
    wg_segments::dedup_interleaved(&lines)
}

pub(crate) fn trim_trailing_noise(lines: Vec<String>) -> Vec<String> {
    wg_segments::trim_trailing_noise(&lines)
}

pub(crate) fn strip_foreign_script(lines: Vec<String>) -> Vec<String> {
    wg_segments::strip_foreign_script(&lines)
}

pub(crate) fn collapse_noise_markers(lines: Vec<String>) -> Vec<String> {
    wg_segments::collapse_noise_markers(&lines)
}

impl TranscriptCleanupResult {
    pub(crate) fn after(&self, stage: TranscriptCleanupStage) -> usize {
        self.stats
            .iter()
            .find(|stat| stat.stage == stage)
            .map(|stat| stat.after)
            .unwrap_or(self.lines.len())
    }
}

pub(crate) fn run_transcript_cleanup_pipeline(lines: Vec<String>) -> TranscriptCleanupResult {
    let mut stats = Vec::new();
    let mut current = lines;

    let stages: &[TranscriptCleanupStep] = &[
        (TranscriptCleanupStage::DedupSegments, dedup_segments),
        (TranscriptCleanupStage::DedupInterleaved, dedup_interleaved),
        (
            TranscriptCleanupStage::StripForeignScript,
            strip_foreign_script,
        ),
        (
            TranscriptCleanupStage::CollapseNoiseMarkers,
            collapse_noise_markers,
        ),
        (
            TranscriptCleanupStage::TrimTrailingNoise,
            trim_trailing_noise,
        ),
    ];

    for (stage, apply) in stages {
        let before = current.len();
        current = apply(current);
        stats.push(TranscriptCleanupStageStat {
            stage: *stage,
            before,
            after: current.len(),
        });
    }

    TranscriptCleanupResult {
        lines: current,
        stats,
    }
}

pub fn transcribe_request(
    request: &TranscriptionRequest,
    config: &Config,
) -> Result<TranscribeResult, TranscribeError> {
    match request.content_type {
        ContentType::Meeting => transcribe::transcribe_meeting_with_hints(
            &request.audio_path,
            config,
            &request.decode_hints,
        ),
        _ => transcribe::transcribe_with_hints(&request.audio_path, config, &request.decode_hints),
    }
}

pub fn transcribe_path_for_content(
    audio_path: &Path,
    content_type: ContentType,
    config: &Config,
) -> Result<TranscribeResult, TranscribeError> {
    let request = TranscriptionRequest {
        audio_path: audio_path.to_path_buf(),
        content_type,
        decode_hints: transcribe::DecodeHints::default(),
    };
    transcribe_request(&request, config)
}

pub fn transcribe_path_for_content_with_hints(
    audio_path: &Path,
    content_type: ContentType,
    config: &Config,
    decode_hints: transcribe::DecodeHints,
) -> Result<TranscribeResult, TranscribeError> {
    let request = TranscriptionRequest {
        audio_path: audio_path.to_path_buf(),
        content_type,
        decode_hints,
    };
    transcribe_request(&request, config)
}

pub fn parakeet_guide_url() -> &'static str {
    "https://github.com/silverstein/minutes/blob/main/docs/PARAKEET.md"
}

pub fn parakeet_setup_command(model: &str) -> String {
    format!("minutes setup --parakeet --parakeet-model {}", model)
}

/// One honest sentence about what the sidecar will actually do (#295).
fn sidecar_state_label(config: &Config, effective: bool, binary_found: bool) -> String {
    let mode = match config.transcription.parakeet_sidecar_enabled {
        None => "auto",
        Some(true) => "forced on",
        Some(false) => "forced off",
    };
    if effective && binary_found {
        format!("warm server ({mode})")
    } else if effective {
        format!("{mode}, but example-server is missing; falling back to cold per-utterance runs")
    } else if binary_found {
        format!("off ({mode}); cold per-utterance runs")
    } else {
        format!(
            "unavailable ({mode}); cold per-utterance runs. Install example-server to enable (see {})",
            parakeet_guide_url()
        )
    }
}

pub fn parakeet_backend_status(config: &Config) -> ParakeetBackendStatus {
    let sidecar_effective = crate::parakeet_sidecar::sidecar_enabled_effective(config);
    let backend_id = if sidecar_effective {
        "parakeet-sidecar".to_string()
    } else {
        "parakeet".to_string()
    };
    let compiled = cfg!(feature = "parakeet");
    let binary = config.transcription.parakeet_binary.clone();
    let model = config.transcription.parakeet_model.clone();
    let resolved_binary = crate::parakeet::resolve_parakeet_binary(
        &binary,
        crate::parakeet::ResolveParakeetBinaryMode::WarnAndFallback,
    )
    .ok();
    let resolved_model = parakeet::resolve_model_file(config, &model);
    let resolved_tokenizer =
        parakeet::resolve_tokenizer_file(config, &model, &config.transcription.parakeet_vocab);
    let metadata = parakeet::read_install_metadata(config, &model);
    let mut issues = Vec::new();

    if !compiled {
        issues.push("Parakeet support is not compiled into this build".to_string());
    }
    if !VALID_PARAKEET_MODELS.contains(&model.as_str()) {
        issues.push(format!(
            "unknown parakeet model '{}'. Valid: {}",
            model,
            VALID_PARAKEET_MODELS.join(", ")
        ));
    }
    if resolved_binary.is_none() {
        issues.push(format!("binary '{}' could not be resolved", binary));
    }
    if resolved_model.is_none() {
        issues.push(format!("model assets for '{}' are not installed", model));
    }
    if resolved_tokenizer.is_none() {
        issues.push("SentencePiece tokenizer is not installed".to_string());
    }
    if metadata.is_none() && resolved_model.is_some() && resolved_tokenizer.is_some() {
        issues.push("install metadata is missing; rerun setup to persist provenance".to_string());
    }
    // Note: sidecar_enabled_effective above may also have resolved the binary;
    // status assembly is cold-path (settings/health), so the repeat lookup is
    // accepted for clarity over threading a resolved path through.
    let sidecar_binary_found = crate::parakeet_sidecar::resolve_server_binary(&binary).is_some();
    if config.transcription.parakeet_sidecar_enabled == Some(true) && !sidecar_binary_found {
        issues.push(
            "sidecar forced on in config but example-server could not be resolved; live transcription will run the cold per-utterance path".to_string(),
        );
    }

    let tokenizer_label = resolved_tokenizer.as_ref().and_then(|path| {
        path.file_name()
            .and_then(|name| name.to_str())
            .map(|name| name.to_string())
    });

    ParakeetBackendStatus {
        backend_id: backend_id.clone(),
        compiled,
        model: model.clone(),
        warm: backend_is_warm(&backend_id, &model),
        sidecar: sidecar_state_label(config, sidecar_effective, sidecar_binary_found),
        sidecar_enabled: sidecar_effective,
        sidecar_binary_found,
        ready: compiled
            && VALID_PARAKEET_MODELS.contains(&model.as_str())
            && resolved_binary.is_some()
            && resolved_model.is_some()
            && resolved_tokenizer.is_some(),
        binary,
        binary_found: resolved_binary.is_some(),
        model_found: resolved_model.is_some(),
        tokenizer_found: resolved_tokenizer.is_some(),
        binary_path: resolved_binary.map(|path| path.display().to_string()),
        model_path: resolved_model.map(|path| path.display().to_string()),
        tokenizer_path: resolved_tokenizer
            .as_ref()
            .map(|path| path.display().to_string()),
        tokenizer_label,
        install_dir: parakeet::install_dir(config, &model).display().to_string(),
        setup_command: parakeet_setup_command(&model),
        guide_url: parakeet_guide_url().to_string(),
        issues,
        metadata,
    }
}

pub fn parakeet_health_item(config: &Config) -> HealthItem {
    let status = parakeet_backend_status(config);
    let detail = if status.ready {
        let metadata_suffix = if let Some(metadata) = status.metadata.as_ref() {
            format!(
                " Metadata: {} from {}. Sidecar: {}.",
                metadata.source_artifact, metadata.source_repo, status.sidecar
            )
        } else {
            format!(
                " Metadata missing; rerun `{}` after installing files to persist provenance. Sidecar: {}.",
                status.setup_command,
                status.sidecar
            )
        };
        format!(
            "Parakeet {} ready. Model: {}. Tokenizer: {}.{}",
            status.model,
            status.model_path.as_deref().unwrap_or("unknown"),
            status.tokenizer_path.as_deref().unwrap_or("unknown"),
            metadata_suffix
        )
    } else {
        format!(
            "Parakeet not ready: {}. Run `{}` for the guided install path.",
            status.issues.join(", "),
            status.setup_command
        )
    };

    HealthItem {
        label: "Speech model".into(),
        state: if status.ready { "ready" } else { "attention" }.into(),
        detail,
        optional: false,
    }
}

pub fn diagnostics_snapshot(config: &Config) -> TranscriptionDiagnosticsSnapshot {
    let status = parakeet_backend_status(config);
    TranscriptionDiagnosticsSnapshot {
        backend_id: status.backend_id.clone(),
        model: status.model.clone(),
        ready: status.ready,
        warm: status.warm,
        used_gpu: cfg!(all(target_os = "macos", target_arch = "aarch64")),
        chunking_strategy: "meeting-vad-or-45s-fixed-chunks".into(),
        issues: status.issues.clone(),
        metadata: status.metadata.clone(),
    }
}

fn parakeet_warmup_selected(config: &Config) -> bool {
    config.transcription.engine == "parakeet"
        || config.effective_live_transcript_backend() == "parakeet"
}

pub fn warmup_active_backend(config: &Config) -> Result<BackendWarmupResult, TranscribeError> {
    if !parakeet_warmup_selected(config) {
        return Err(TranscribeError::EngineNotAvailable("parakeet".into()));
    }

    #[cfg(feature = "parakeet")]
    {
        if crate::parakeet_sidecar::sidecar_enabled_effective(config) {
            let model_path = transcribe::resolve_parakeet_model_path(config)?;
            let vocab_path = transcribe::resolve_parakeet_vocab_path(config)?;
            let vad_path = transcribe::resolve_parakeet_native_vad_path(config);
            let started = std::time::Instant::now();
            let _started_now = crate::parakeet_sidecar::warmup_global_sidecar(
                config,
                &model_path,
                &vocab_path,
                vad_path.as_deref(),
            )
            .map_err(|error| TranscribeError::ParakeetFailed(error.to_string()))?;
            mark_backend_warm("parakeet-sidecar", &config.transcription.parakeet_model);
            return Ok(BackendWarmupResult {
                backend_id: "parakeet-sidecar".into(),
                model: config.transcription.parakeet_model.clone(),
                elapsed_ms: started.elapsed().as_millis() as u64,
                used_gpu: cfg!(all(target_os = "macos", target_arch = "aarch64")),
            });
        }

        let stats = transcribe::warmup_parakeet(config)?;
        mark_backend_warm("parakeet", &stats.model);
        Ok(BackendWarmupResult {
            backend_id: "parakeet".into(),
            model: stats.model,
            elapsed_ms: stats.elapsed_ms,
            used_gpu: stats.used_gpu,
        })
    }

    #[cfg(not(feature = "parakeet"))]
    {
        Err(TranscribeError::EngineNotAvailable("parakeet".into()))
    }
}

#[cfg(feature = "parakeet")]
#[allow(clippy::too_many_arguments)]
pub fn benchmark_parakeet(
    helper_binary: &Path,
    binary: &str,
    model_path: &Path,
    audio_path: &Path,
    vocab_path: &Path,
    model_id: &str,
    gpu: bool,
    vad_path: Option<&Path>,
    vad_threshold: f32,
    config: &Config,
) -> Result<ParakeetBenchmarkReport, String> {
    let started = std::time::Instant::now();
    let direct = transcribe::run_parakeet_cli_structured(
        binary,
        model_path,
        audio_path,
        vocab_path,
        model_id,
        gpu,
        vad_path,
        vad_threshold,
        config,
        &transcribe::DecodeHints::default(),
    )
    .map_err(|error| error.to_string())?;
    let direct_elapsed_ms = started.elapsed().as_millis() as u64;

    let helper_started = std::time::Instant::now();
    let mut helper_command = std::process::Command::new(helper_binary);
    helper_command
        .arg("parakeet-helper")
        .args(["--binary", binary])
        .args([
            "--model-path",
            model_path
                .to_str()
                .ok_or_else(|| "model path is not valid UTF-8".to_string())?,
        ])
        .args([
            "--audio-path",
            audio_path
                .to_str()
                .ok_or_else(|| "audio path is not valid UTF-8".to_string())?,
        ])
        .args([
            "--vocab-path",
            vocab_path
                .to_str()
                .ok_or_else(|| "vocab path is not valid UTF-8".to_string())?,
        ])
        .args(["--model-id", model_id])
        .args(if gpu { vec!["--gpu"] } else { Vec::new() })
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped());
    if let Some(vad_path) = vad_path.and_then(|path| path.to_str()) {
        helper_command
            .args(["--vad-path", vad_path])
            .args(["--vad-threshold", &vad_threshold.to_string()]);
    }
    let helper = helper_command.output().map_err(|error| error.to_string())?;
    if !helper.status.success() {
        return Err(format!(
            "helper benchmark failed: {}",
            String::from_utf8_lossy(&helper.stderr)
        ));
    }
    let helper_json: serde_json::Value =
        serde_json::from_slice(&helper.stdout).map_err(|error| error.to_string())?;

    Ok(ParakeetBenchmarkReport {
        backend_id: "parakeet".into(),
        model: model_id.into(),
        gpu,
        direct_elapsed_ms,
        direct_segments: direct.segments.len(),
        helper_elapsed_ms: helper_started.elapsed().as_millis() as u64,
        helper_segments: helper_json
            .get("segments")
            .and_then(|v| v.as_array())
            .map(|v| v.len())
            .unwrap_or(0),
    })
}

#[cfg(test)]
mod tests {
    use super::parakeet_warmup_selected;
    use crate::Config;

    #[test]
    fn parakeet_warmup_selection_includes_live_backend() {
        let mut config = Config::default();
        config.transcription.engine = "whisper".into();
        config.live_transcript.backend = "parakeet".into();

        assert!(parakeet_warmup_selected(&config));
    }

    #[test]
    fn parakeet_warmup_selection_skips_non_parakeet_backends() {
        let mut config = Config::default();
        config.transcription.engine = "whisper".into();
        config.live_transcript.backend = "apple-speech".into();

        assert!(!parakeet_warmup_selected(&config));
    }
}
