//! LLM-only iteration: skip Whisper, use the production-bereinigten
//! transcripts that already live next to the audio files (`<stem>.json`
//! contains the gazetteer-cleaned text the server uses as LLM input).
//!
//! Usage:
//!   run_llm_only \
//!     --case-dir case_c414cf52 \
//!     --transcripts-dir /home/brummel/dev/doctate/tmpdata/dr_mueller/c414cf52-1ca8-440d-844f-a7d19ed584fd \
//!     --llm-prompt prompts/llm/v1_keep_headings.txt \
//!     --runs 3

use std::path::{Path, PathBuf};

fn data_runs_root(case_dir: &Path) -> anyhow::Result<PathBuf> {
    let case_name = case_dir
        .file_name()
        .and_then(|s| s.to_str())
        .ok_or_else(|| anyhow::anyhow!("case-dir has no file name"))?;
    let experiments_root = case_dir
        .parent()
        .ok_or_else(|| anyhow::anyhow!("case-dir has no parent"))?;
    let runs = experiments_root.join("_data").join(case_name).join("runs");
    std::fs::create_dir_all(&runs)?;
    Ok(runs)
}
use std::time::{Duration, Instant};

use anyhow::{Context, Result};
use clap::Parser;
use doctate_experiments::{
    GazetteerDictMode, RecordingMeta, RunMeta, RunTimings, atomic_write, load_prompt, run_id,
};
use doctate_server::analyze::llm::{self as srv_llm, LlmSettings as LlmCallSettings};
use doctate_server::analyze::prompt::render_prompt;
use doctate_server::analyze::{AnalysisInput, RecordingInput};
use doctate_server::gazetteer::Gazetteer;
use doctate_server::settings::Settings;
use serde::Deserialize;
use tracing::info;

#[derive(Parser, Debug)]
#[command(name = "run_llm_only")]
struct Args {
    #[arg(long)]
    case_dir: PathBuf,

    /// Directory containing the `<recorded_at>.json` transcript files.
    /// Typically `tmpdata/<user>/<case-uuid>/`.
    #[arg(long)]
    transcripts_dir: PathBuf,

    #[arg(long)]
    llm_prompt: PathBuf,

    #[arg(long, default_value_t = 1)]
    runs: u32,

    #[arg(long, default_value = "../server/settings.toml")]
    settings: PathBuf,

    /// Vocab dir for the Post-LLM gazetteer pass. Mirrors the server's
    /// `analyze/worker.rs:150` post-AI normalization. Pass `none` to skip.
    #[arg(long, default_value = "../server/vocab")]
    vocab_dir: PathBuf,
}

#[derive(Debug, Deserialize)]
struct StoredTranscript {
    transcript: TranscriptInner,
}

#[derive(Debug, Deserialize)]
struct TranscriptInner {
    text: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    init_tracing();
    let args = Args::parse();

    let case_dir = args
        .case_dir
        .canonicalize()
        .with_context(|| format!("case-dir not found: {}", args.case_dir.display()))?;
    let settings = Settings::load_or_default(&args.settings.to_string_lossy());
    if !settings.llm_configured() {
        anyhow::bail!("LLM not configured in {}", args.settings.display());
    }

    let llm_system_prompt = load_prompt(&args.llm_prompt)?;
    let mut recordings = load_transcripts(&args.transcripts_dir)?;
    info!(count = recordings.len(), "transcripts loaded");

    let gazetteer = Gazetteer::load_dir(&args.vocab_dir)
        .await
        .with_context(|| format!("loading vocab dir {}", args.vocab_dir.display()))?;
    info!(loaded = gazetteer.len(), vocab_dir = %args.vocab_dir.display(), "gazetteer ready");

    // Pre-LLM gazetteer pass — mirrors server/src/transcribe/worker.rs:138.
    // The tmpdata transcripts were already cleaned with an earlier vocabulary
    // snapshot; re-running with the current dir is idempotent for old entries
    // and applies any newly added entries (e.g. Enoxaparin, Pumpfunktion).
    for r in recordings.iter_mut() {
        r.text = gazetteer.replace(&r.text);
    }

    let analysis_input = AnalysisInput {
        last_recording_mtime: recordings
            .last()
            .map(|r| r.recorded_at.clone())
            .unwrap_or_default(),
        recordings,
    };
    let user_content = render_prompt(&analysis_input);

    let http = reqwest::Client::builder().build()?;
    let llm_variant_id = stem(&args.llm_prompt);
    let runs_root = data_runs_root(&case_dir)?;

    for run_index in 1..=args.runs {
        let id = run_id(&llm_variant_id, run_index);
        let run_dir = runs_root.join(&id);
        std::fs::create_dir_all(&run_dir)?;

        atomic_write(&run_dir.join("llm_input.txt"), &user_content)?;

        let wallclock = Instant::now();
        let t_llm = Instant::now();
        let document_raw = call_llm(&http, &settings, &llm_system_prompt, &user_content).await?;
        // Post-LLM gazetteer pass — mirrors server/src/analyze/worker.rs:150
        let document = gazetteer.replace(&document_raw);
        let timings = RunTimings {
            whisper_total_ms: 0,
            gazetteer_total_ms: 0,
            llm_ms: t_llm.elapsed().as_millis() as u64,
            wallclock_ms: wallclock.elapsed().as_millis() as u64,
        };
        atomic_write(&run_dir.join("document.md"), &document)?;

        let meta = RunMeta {
            run_id: id.clone(),
            created_at_utc: time::OffsetDateTime::now_utc()
                .format(&time::format_description::well_known::Iso8601::DEFAULT)
                .unwrap_or_default(),
            case: case_dir
                .file_name()
                .map(|s| s.to_string_lossy().into_owned())
                .unwrap_or_default(),
            llm_variant: llm_variant_id.clone(),
            whisper_url: String::new(),
            llm_url: settings.llm.url.clone(),
            llm_model: settings.llm.model.clone(),
            llm_temperature: settings.llm.temperature,
            vocab_dir: format!("{} (pre+post-LLM)", args.vocab_dir.display()),
            vocab_loaded: gazetteer.len(),
            gazetteer_dict: GazetteerDictMode::NoDict,
            timings_ms: timings,
            recordings: Vec::<RecordingMeta>::new(),
        };
        atomic_write(
            &run_dir.join("meta.json"),
            &serde_json::to_string_pretty(&meta)?,
        )?;
        info!(run = %id, "llm-only run done");
    }

    Ok(())
}

fn init_tracing() {
    use tracing_subscriber::EnvFilter;
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
    tracing_subscriber::fmt().with_env_filter(filter).init();
}

fn load_transcripts(dir: &Path) -> Result<Vec<RecordingInput>> {
    let mut out = 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 recorded_at = path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("")
            .to_string();
        if recorded_at.is_empty() || recorded_at == "oneliner" {
            continue;
        }
        let raw = std::fs::read_to_string(&path)?;
        let parsed: StoredTranscript = match serde_json::from_str(&raw) {
            Ok(p) => p,
            Err(_) => continue,
        };
        out.push(RecordingInput {
            recorded_at,
            text: parsed.transcript.text,
        });
    }
    out.sort_by(|a, b| a.recorded_at.cmp(&b.recorded_at));
    Ok(out)
}

fn stem<P: AsRef<Path>>(p: P) -> String {
    p.as_ref()
        .file_stem()
        .map(|s| s.to_string_lossy().into_owned())
        .unwrap_or_else(|| "unknown".to_string())
}

async fn call_llm(
    http: &reqwest::Client,
    settings: &Settings,
    system_prompt: &str,
    user_content: &str,
) -> Result<String> {
    let call = LlmCallSettings {
        base_url: &settings.llm.url,
        api_key: &settings.llm.api_key,
        model: &settings.llm.model,
        temperature: settings.llm.temperature,
    };
    let timeout = Duration::from_secs(settings.llm.timeout_seconds);
    srv_llm::chat_once(http, &call, system_prompt, user_content, timeout)
        .await
        .map_err(|e| anyhow::anyhow!("llm chat failed: {e}"))
}
