mod config;
mod eq_discovery;
mod events;
mod kokoro;
mod speakers;
mod tui;
mod workers;

use anyhow::{Context, Result};
use clap::Parser;
use config::Config;
use crossterm::{
    event::{self, Event},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use events::LogEvent;
use ratatui::{Terminal, backend::CrosstermBackend};
use std::{
    io::{self, stdout},
    path::PathBuf,
    sync::{
        Arc,
        atomic::{AtomicUsize, Ordering},
        mpsc,
    },
    time::{Duration, Instant},
};
use tokio::sync::mpsc as tokio_mpsc;
use tui::{App, UiAction};
use workers::{
    AudioCommand, SynthesisJob, WorkerEvent, spawn_audio_worker, spawn_kokoro_worker,
    spawn_log_tailer,
};

#[derive(Debug, Parser)]
#[command(name = "mudmouth", version, about = "Give your EverQuest log a voice.")]
struct Cli {
    /// Override the EverQuest log location for this run.
    #[arg(long)]
    log: Option<PathBuf>,

    /// Override the Kokoro-FastAPI endpoint for this run.
    #[arg(long)]
    kokoro_url: Option<String>,
}

struct RuntimeChannels {
    worker_receiver: mpsc::Receiver<WorkerEvent>,
    worker_sender: mpsc::Sender<WorkerEvent>,
    synthesis_sender: tokio_mpsc::Sender<SynthesisJob>,
    worker_control: tokio_mpsc::Sender<Config>,
    audio_control: mpsc::Sender<AudioCommand>,
    tailer_generation: Arc<AtomicUsize>,
}

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();
    let mut config = Config::load()?;
    if let Some(path) = cli.log {
        config.log_path = Some(path);
    }
    if let Some(url) = cli.kokoro_url {
        config.kokoro_url = url;
    }

    let speakers = speakers::SpeakerStore::load()?;
    let (worker_sender, worker_receiver) = mpsc::channel::<WorkerEvent>();
    let audio_sender = spawn_audio_worker(config.volume, worker_sender.clone());
    let (synthesis_sender, synthesis_receiver) = tokio_mpsc::channel(32);
    let worker_control = spawn_kokoro_worker(
        config.clone(),
        synthesis_receiver,
        audio_sender.clone(),
        worker_sender.clone(),
    );

    let tailer_generation = Arc::new(AtomicUsize::new(0));
    if config.log_path.as_ref().is_some_and(|path| path.is_file()) {
        let path = config.log_path.clone().expect("log path was checked");
        spawn_log_tailer(path, worker_sender.clone(), tailer_generation.clone(), 0);
    }

    let mut app = App::new(config, speakers);
    let channels = RuntimeChannels {
        worker_receiver,
        worker_sender,
        synthesis_sender,
        worker_control,
        audio_control: audio_sender,
        tailer_generation,
    };
    run_terminal(&mut app, channels).await
}

async fn run_terminal(app: &mut App, channels: RuntimeChannels) -> Result<()> {
    let mut terminal = setup_terminal()?;
    let result = run_loop(app, &mut terminal, channels).await;
    restore_terminal(&mut terminal)?;
    result
}

async fn run_loop(
    app: &mut App,
    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
    channels: RuntimeChannels,
) -> Result<()> {
    loop {
        while let Ok(event) = channels.worker_receiver.try_recv() {
            process_worker_event(app, event, &channels.synthesis_sender)?;
        }

        terminal.draw(|frame| tui::draw(frame, app))?;
        if event::poll(Duration::from_millis(80))?
            && let Event::Key(key) = event::read()?
            && let Some(action) = app.handle_key(key)
        {
            match action {
                UiAction::Quit => return Ok(()),
                UiAction::ConfigChanged => {
                    app.config.save()?;
                    app.speakers.save()?;
                    let _ = channels.worker_control.try_send(app.config.clone());
                    let _ = channels
                        .audio_control
                        .send(AudioCommand::SetVolume(app.config.volume));
                    let generation = channels.tailer_generation.fetch_add(1, Ordering::Relaxed) + 1;
                    if let Some(path) = app.config.log_path.clone().filter(|path| path.is_file()) {
                        spawn_log_tailer(
                            path,
                            channels.worker_sender.clone(),
                            channels.tailer_generation.clone(),
                            generation,
                        );
                    }
                    app.activity.push_front("settings saved".into());
                }
            }
        }
    }
}

fn process_worker_event(
    app: &mut App,
    event: WorkerEvent,
    synthesis_sender: &tokio_mpsc::Sender<SynthesisJob>,
) -> Result<()> {
    match &event {
        WorkerEvent::Log(LogEvent::ZoneTransition { zone, .. }) => {
            app.current_zone = Some(zone.clone());
            if app.config.events.zone_transitions {
                enqueue_random_event(app, synthesis_sender, format!("You have entered {zone}."))?;
            }
        }
        WorkerEvent::Log(LogEvent::NpcDialogue { speaker, text, .. })
            if app.config.events.npc_dialogue =>
        {
            if app.voices.is_empty() {
                app.activity
                    .push_front("NPC line seen, waiting for Kokoro voice catalog…".into());
            } else {
                let key = events::speaker_key(speaker);
                if let Some(voice) = app.speakers.resolve_voice(
                    key.clone(),
                    speaker.clone(),
                    &app.voices,
                    app.current_zone.clone(),
                ) {
                    app.speakers.save()?;
                    if synthesis_sender
                        .try_send(SynthesisJob {
                            text: text.clone(),
                            voice,
                            speed: 1.0,
                            repeat_key: format!("{key}\u{1f}{text}"),
                            observed_at: Instant::now(),
                        })
                        .is_err()
                    {
                        app.activity
                            .push_front("speech queue is busy; line dropped".into());
                    }
                }
            }
        }
        _ => {}
    }
    app.receive(event);
    Ok(())
}

fn enqueue_random_event(
    app: &mut App,
    synthesis_sender: &tokio_mpsc::Sender<SynthesisJob>,
    text: String,
) -> Result<()> {
    use rand::prelude::IndexedRandom;
    if let Some(voice) = app.voices.choose(&mut rand::rng()).cloned() {
        synthesis_sender
            .try_send(SynthesisJob {
                repeat_key: format!("event:\u{1f}{text}"),
                text,
                voice,
                speed: 1.0,
                observed_at: Instant::now(),
            })
            .context("speech queue is busy")?;
    }
    Ok(())
}

fn setup_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
    enable_raw_mode()?;
    let mut output = stdout();
    execute!(output, EnterAlternateScreen)?;
    Terminal::new(CrosstermBackend::new(output)).context("could not start terminal UI")
}

fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;
    Ok(())
}
