//! History store for the TTS playground webview.
//!
//! One directory per user under `~/.adom/tts/history/`. Each clip is two
//! files: `<id>.mp3` and `<id>.json` (the metadata sidecar). The webview
//! UI scans this dir every few seconds so clips dropped in externally
//! (via `adom-tts push` or `adom-tts say --history`) show up without
//! restarting the server.
//!
//! ID format: `YYYYMMDD-HHMMSS-<sha8>`. Sortable by timestamp, unique
//! on collision via the first 8 hex chars of the audio hash.

use anyhow::{Context, Result};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Clip {
    pub id: String,
    pub text: String,
    pub voice: String,
    pub rate: String,
    pub created_at: String,
    pub size_bytes: u64,
    pub source: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tts_hash: Option<String>,
}

pub fn history_dir() -> Result<PathBuf> {
    let home = std::env::var("HOME").context("no $HOME")?;
    let dir = PathBuf::from(home).join(".adom").join("tts").join("history");
    std::fs::create_dir_all(&dir)?;
    Ok(dir)
}

pub fn save(
    audio: &[u8],
    text: &str,
    voice: &str,
    rate: &str,
    source: &str,
    tts_hash: Option<String>,
) -> Result<Clip> {
    let dir = history_dir()?;
    let mut hasher = Sha256::new();
    hasher.update(audio);
    let hash = hex8(&hasher.finalize());
    let id = format!("{}-{}", Utc::now().format("%Y%m%d-%H%M%S"), hash);

    let mp3 = dir.join(format!("{id}.mp3"));
    let meta = dir.join(format!("{id}.json"));
    std::fs::write(&mp3, audio)?;

    let clip = Clip {
        id: id.clone(),
        text: text.to_string(),
        voice: voice.to_string(),
        rate: rate.to_string(),
        created_at: Utc::now().to_rfc3339(),
        size_bytes: audio.len() as u64,
        source: source.to_string(),
        tts_hash,
    };
    std::fs::write(&meta, serde_json::to_vec_pretty(&clip)?)?;
    Ok(clip)
}

pub fn list() -> Result<Vec<Clip>> {
    let dir = history_dir()?;
    let mut clips = Vec::new();
    for entry in std::fs::read_dir(&dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }
        let bytes = match std::fs::read(&path) {
            Ok(b) => b,
            Err(_) => continue,
        };
        let clip: Clip = match serde_json::from_slice(&bytes) {
            Ok(c) => c,
            Err(_) => continue,
        };
        // Skip orphans (json with no matching mp3).
        if !dir.join(format!("{}.mp3", clip.id)).exists() {
            continue;
        }
        clips.push(clip);
    }
    clips.sort_by(|a, b| b.created_at.cmp(&a.created_at));
    Ok(clips)
}

/// Newest mp3 in the history dir (by mtime). Used by `adom-tts play last`.
pub fn latest_mp3() -> Result<Option<PathBuf>> {
    let dir = history_dir()?;
    let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
    for entry in std::fs::read_dir(&dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("mp3") {
            continue;
        }
        let meta = match entry.metadata() { Ok(m) => m, Err(_) => continue };
        let mtime = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
        match &best {
            Some((t, _)) if *t >= mtime => {}
            _ => best = Some((mtime, path)),
        }
    }
    Ok(best.map(|(_, p)| p))
}

pub fn audio_path(id: &str) -> Result<PathBuf> {
    let dir = history_dir()?;
    let p = dir.join(format!("{id}.mp3"));
    if !p.exists() {
        anyhow::bail!("clip not found: {id}");
    }
    // Reject path traversal — id must not escape history dir.
    let canon = p.canonicalize()?;
    if !canon.starts_with(dir.canonicalize()?) {
        anyhow::bail!("clip id out of history dir");
    }
    Ok(p)
}

pub fn delete(id: &str) -> Result<()> {
    let dir = history_dir()?;
    let mp3 = dir.join(format!("{id}.mp3"));
    let meta = dir.join(format!("{id}.json"));
    // Path-traversal guard (same as audio_path).
    if let Ok(c) = mp3.canonicalize() {
        if !c.starts_with(dir.canonicalize()?) {
            anyhow::bail!("clip id out of history dir");
        }
    }
    let _ = std::fs::remove_file(&mp3);
    let _ = std::fs::remove_file(&meta);
    Ok(())
}

/// Adopt an arbitrary .mp3 from disk into the history store. Used by
/// `adom-tts push <path>` so clips synthesized by other tools show up
/// in the playground.
pub fn push_file(src: &Path, text: &str, voice: &str, rate: &str) -> Result<Clip> {
    let audio = std::fs::read(src).with_context(|| format!("read {}", src.display()))?;
    save(&audio, text, voice, rate, "external", None)
}

fn hex8(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut s = String::with_capacity(8);
    for b in bytes.iter().take(4) {
        s.push(HEX[(b >> 4) as usize] as char);
        s.push(HEX[(b & 0xf) as usize] as char);
    }
    s
}