//! Voxtype - Push-to-talk voice-to-text for Linux
//!
//! Run with `voxtype` or `voxtype daemon` to start the daemon.
//! Use `voxtype setup` to check dependencies and download models.
//! Use `voxtype transcribe <file>` to transcribe an audio file.

mod audio;
mod config;
mod daemon;
mod error;
mod hotkey;
mod output;
mod setup;
mod state;
mod text;
mod transcribe;

use clap::{Parser, Subcommand};
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;

#[derive(Parser)]
#[command(name = "voxtype")]
#[command(author, version, about = "Push-to-talk voice-to-text for Linux")]
#[command(long_about = "
Voxtype is a push-to-talk voice-to-text tool for Linux.
Optimized for Wayland, works on X11 too.
Hold a hotkey to record, release to transcribe and output the text.

SETUP:
  1. Add yourself to the input group: sudo usermod -aG input $USER
  2. Log out and back in
  3. Install wtype (Wayland) or ydotool (X11) for typing support
  4. Run: voxtype setup (to check dependencies and download whisper model)
  5. Run: voxtype (to start the daemon)

USAGE:
  Hold ScrollLock (default) while speaking, release to transcribe.
  Text will be typed at cursor position, or copied to clipboard as fallback.
")]
struct Cli {
    /// Path to config file
    #[arg(short, long, value_name = "FILE")]
    config: Option<PathBuf>,

    /// Increase verbosity (-v = debug, -vv = trace)
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,

    /// Quiet mode (errors only)
    #[arg(short, long)]
    quiet: bool,

    /// Force clipboard mode (don't try to type)
    #[arg(long)]
    clipboard: bool,

    /// Force paste mode (clipboard + Ctrl+V)
    #[arg(long)]
    paste: bool,

    /// Override whisper model (tiny, base, small, medium, large-v3, large-v3-turbo)
    #[arg(long, value_name = "MODEL")]
    model: Option<String>,

    /// Override hotkey (e.g., SCROLLLOCK, PAUSE, F13)
    #[arg(long, value_name = "KEY")]
    hotkey: Option<String>,

    /// Use toggle mode (press to start/stop) instead of push-to-talk (hold to record)
    #[arg(long)]
    toggle: bool,

    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Run as daemon (default if no command specified)
    Daemon,

    /// Transcribe an audio file (WAV, 16kHz, mono)
    Transcribe {
        /// Path to audio file
        file: PathBuf,
    },

    /// Setup and installation utilities
    Setup {
        #[command(subcommand)]
        action: Option<SetupAction>,

        /// Download model if missing (shorthand for basic setup)
        #[arg(long)]
        download: bool,
    },

    /// Show current configuration
    Config,

    /// Show daemon status (for Waybar/polybar integration)
    Status {
        /// Continuously output status changes as JSON (for Waybar exec)
        #[arg(long)]
        follow: bool,

        /// Output format: "text" (default) or "json" (for Waybar)
        #[arg(long, default_value = "text")]
        format: String,

        /// Include extended info in JSON (model, device, backend)
        #[arg(long)]
        extended: bool,
    },

    /// Control recording from external sources (compositor keybindings, scripts)
    Record {
        #[command(subcommand)]
        action: RecordAction,
    },
}

#[derive(Subcommand)]
enum RecordAction {
    /// Start recording (send SIGUSR1 to daemon)
    Start,
    /// Stop recording and transcribe (send SIGUSR2 to daemon)
    Stop,
    /// Toggle recording state
    Toggle,
}

#[derive(Subcommand)]
enum SetupAction {
    /// Install voxtype as a systemd user service
    Systemd {
        /// Uninstall the service instead of installing
        #[arg(long)]
        uninstall: bool,

        /// Show service status
        #[arg(long)]
        status: bool,
    },

    /// Show Waybar configuration snippets
    Waybar {
        /// Output only the JSON config (for scripting)
        #[arg(long)]
        json: bool,

        /// Output only the CSS config (for scripting)
        #[arg(long)]
        css: bool,

        /// Install waybar integration (inject config and CSS)
        #[arg(long)]
        install: bool,

        /// Uninstall waybar integration (remove config and CSS)
        #[arg(long)]
        uninstall: bool,
    },

    /// Interactive model selection and download
    Model {
        /// List installed models instead of interactive selection
        #[arg(long)]
        list: bool,
    },

    /// Manage GPU acceleration (switch between CPU and Vulkan backends)
    Gpu {
        /// Enable GPU (Vulkan) acceleration
        #[arg(long)]
        enable: bool,

        /// Disable GPU acceleration (switch back to CPU)
        #[arg(long)]
        disable: bool,

        /// Show current backend status
        #[arg(long)]
        status: bool,
    },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    // Initialize logging
    let log_level = if cli.quiet {
        "error"
    } else {
        match cli.verbose {
            0 => "info",
            1 => "debug",
            _ => "trace",
        }
    };

    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| EnvFilter::new(format!("voxtype={},warn", log_level))),
        )
        .with_target(false)
        .init();

    // Load configuration
    let mut config = config::load_config(cli.config.as_deref())?;

    // Apply CLI overrides
    if cli.clipboard {
        config.output.mode = config::OutputMode::Clipboard;
    }
    if cli.paste {
        config.output.mode = config::OutputMode::Paste;
    }
    if let Some(model) = cli.model {
        config.whisper.model = model;
    }
    if let Some(hotkey) = cli.hotkey {
        config.hotkey.key = hotkey;
    }
    if cli.toggle {
        config.hotkey.mode = config::ActivationMode::Toggle;
    }

    // Run the appropriate command
    match cli.command.unwrap_or(Commands::Daemon) {
        Commands::Daemon => {
            let mut daemon = daemon::Daemon::new(config);
            daemon.run().await?;
        }

        Commands::Transcribe { file } => {
            transcribe_file(&config, &file)?;
        }

        Commands::Setup { action, download } => {
            match action {
                Some(SetupAction::Systemd { uninstall, status }) => {
                    if status {
                        setup::systemd::status().await?;
                    } else if uninstall {
                        setup::systemd::uninstall().await?;
                    } else {
                        setup::systemd::install().await?;
                    }
                }
                Some(SetupAction::Waybar {
                    json,
                    css,
                    install,
                    uninstall,
                }) => {
                    if install {
                        setup::waybar::install()?;
                    } else if uninstall {
                        setup::waybar::uninstall()?;
                    } else if json {
                        println!("{}", setup::waybar::get_json_config());
                    } else if css {
                        println!("{}", setup::waybar::get_css_config());
                    } else {
                        setup::waybar::print_config();
                    }
                }
                Some(SetupAction::Model { list }) => {
                    if list {
                        setup::model::list_installed();
                    } else {
                        setup::model::interactive_select().await?;
                    }
                }
                Some(SetupAction::Gpu { enable, disable, status }) => {
                    if status {
                        setup::gpu::show_status();
                    } else if enable {
                        setup::gpu::enable()?;
                    } else if disable {
                        setup::gpu::disable()?;
                    } else {
                        // Default: show status
                        setup::gpu::show_status();
                    }
                }
                None => {
                    // Default: run basic setup (backwards compatible)
                    setup::run_basic_setup(&config, download).await?;
                }
            }
        }

        Commands::Config => {
            show_config(&config).await?;
        }

        Commands::Status { follow, format, extended } => {
            run_status(&config, follow, &format, extended).await?;
        }

        Commands::Record { action } => {
            send_record_command(&config, action)?;
        }
    }

    Ok(())
}

/// Send a record command to the running daemon via Unix signals
fn send_record_command(config: &config::Config, action: RecordAction) -> anyhow::Result<()> {
    use nix::sys::signal::{kill, Signal};
    use nix::unistd::Pid;

    // Read PID from the pid file
    let pid_file = config::Config::runtime_dir().join("pid");

    if !pid_file.exists() {
        eprintln!("Error: Voxtype daemon is not running.");
        eprintln!("Start it with: voxtype daemon");
        std::process::exit(1);
    }

    let pid_str = std::fs::read_to_string(&pid_file)
        .map_err(|e| anyhow::anyhow!("Failed to read PID file: {}", e))?;

    let pid: i32 = pid_str
        .trim()
        .parse()
        .map_err(|e| anyhow::anyhow!("Invalid PID in file: {}", e))?;

    // Check if the process is actually running
    if kill(Pid::from_raw(pid), None).is_err() {
        // Process doesn't exist, clean up stale PID file
        let _ = std::fs::remove_file(&pid_file);
        eprintln!("Error: Voxtype daemon is not running (stale PID file removed).");
        eprintln!("Start it with: voxtype daemon");
        std::process::exit(1);
    }

    // For toggle, we need to read current state to decide which signal to send
    let signal = match action {
        RecordAction::Start => Signal::SIGUSR1,
        RecordAction::Stop => Signal::SIGUSR2,
        RecordAction::Toggle => {
            // Read current state to determine action
            let state_file = config.resolve_state_file();
            let current_state = state_file
                .and_then(|p| std::fs::read_to_string(p).ok())
                .unwrap_or_else(|| "idle".to_string());

            if current_state.trim() == "recording" {
                Signal::SIGUSR2 // Stop
            } else {
                Signal::SIGUSR1 // Start
            }
        }
    };

    kill(Pid::from_raw(pid), signal)
        .map_err(|e| anyhow::anyhow!("Failed to send signal to daemon: {}", e))?;

    Ok(())
}

/// Transcribe an audio file
fn transcribe_file(config: &config::Config, path: &PathBuf) -> anyhow::Result<()> {
    use hound::WavReader;

    println!("Loading audio file: {:?}", path);

    let reader = WavReader::open(path)?;
    let spec = reader.spec();

    println!(
        "Audio format: {} Hz, {} channel(s), {:?}",
        spec.sample_rate, spec.channels, spec.sample_format
    );

    // Convert samples to f32 mono at 16kHz
    let samples: Vec<f32> = match spec.sample_format {
        hound::SampleFormat::Int => {
            let max_val = (1 << (spec.bits_per_sample - 1)) as f32;
            reader
                .into_samples::<i32>()
                .filter_map(|s| s.ok())
                .map(|s| s as f32 / max_val)
                .collect()
        }
        hound::SampleFormat::Float => reader
            .into_samples::<f32>()
            .filter_map(|s| s.ok())
            .collect(),
    };

    // Mix to mono if stereo
    let mono_samples: Vec<f32> = if spec.channels > 1 {
        samples
            .chunks(spec.channels as usize)
            .map(|chunk| chunk.iter().sum::<f32>() / chunk.len() as f32)
            .collect()
    } else {
        samples
    };

    // Resample to 16kHz if needed
    let final_samples = if spec.sample_rate != 16000 {
        println!(
            "Resampling from {} Hz to 16000 Hz...",
            spec.sample_rate
        );
        resample(&mono_samples, spec.sample_rate, 16000)
    } else {
        mono_samples
    };

    println!(
        "Processing {} samples ({:.2}s)...",
        final_samples.len(),
        final_samples.len() as f32 / 16000.0
    );

    // Create transcriber and transcribe
    let transcriber = transcribe::create_transcriber(&config.whisper)?;
    let text = transcriber.transcribe(&final_samples)?;

    println!("\n{}", text);
    Ok(())
}

/// Simple linear resampling
fn resample(samples: &[f32], from_rate: u32, to_rate: u32) -> Vec<f32> {
    if from_rate == to_rate {
        return samples.to_vec();
    }

    let ratio = to_rate as f64 / from_rate as f64;
    let new_len = (samples.len() as f64 * ratio).ceil() as usize;
    let mut output = Vec::with_capacity(new_len);

    for i in 0..new_len {
        let src_idx = i as f64 / ratio;
        let idx = src_idx.floor() as usize;
        let frac = (src_idx - idx as f64) as f32;

        let sample = if idx + 1 < samples.len() {
            samples[idx] * (1.0 - frac) + samples[idx + 1] * frac
        } else {
            samples.get(idx).copied().unwrap_or(0.0)
        };

        output.push(sample);
    }

    output
}

/// Extended status info for JSON output
struct ExtendedStatusInfo {
    model: String,
    device: String,
    backend: String,
}

impl ExtendedStatusInfo {
    fn from_config(config: &config::Config) -> Self {
        let backend = setup::gpu::detect_current_backend()
            .map(|b| match b {
                setup::gpu::Backend::Cpu => "CPU (native)",
                setup::gpu::Backend::Avx2 => "CPU (AVX2)",
                setup::gpu::Backend::Avx512 => "CPU (AVX-512)",
                setup::gpu::Backend::Vulkan => "GPU (Vulkan)",
            })
            .unwrap_or("unknown")
            .to_string();

        Self {
            model: config.whisper.model.clone(),
            device: config.audio.device.clone(),
            backend,
        }
    }
}

/// Run the status command - show current daemon state
async fn run_status(
    config: &config::Config,
    follow: bool,
    format: &str,
    extended: bool,
) -> anyhow::Result<()> {
    let state_file = config.resolve_state_file();

    if state_file.is_none() {
        eprintln!("Error: state_file is not configured.");
        eprintln!();
        eprintln!("To enable status monitoring, add to your config.toml:");
        eprintln!();
        eprintln!("  state_file = \"auto\"");
        eprintln!();
        eprintln!("This enables external integrations like Waybar to monitor voxtype state.");
        std::process::exit(1);
    }

    let state_path = state_file.unwrap();
    let ext_info = if extended {
        Some(ExtendedStatusInfo::from_config(config))
    } else {
        None
    };

    if !follow {
        // One-shot: just read and print current state
        let state = std::fs::read_to_string(&state_path).unwrap_or_else(|_| "stopped".to_string());
        let state = state.trim();

        if format == "json" {
            println!("{}", format_state_json(state, ext_info.as_ref()));
        } else {
            println!("{}", state);
        }
        return Ok(());
    }

    // Follow mode: watch for changes using inotify
    use notify::{Config as NotifyConfig, RecommendedWatcher, RecursiveMode, Watcher};
    use std::sync::mpsc::channel;
    use std::time::Duration;

    // Print initial state
    let state = std::fs::read_to_string(&state_path).unwrap_or_else(|_| "stopped".to_string());
    let state = state.trim();
    if format == "json" {
        println!("{}", format_state_json(state, ext_info.as_ref()));
    } else {
        println!("{}", state);
    }

    // Set up file watcher
    let (tx, rx) = channel();
    let mut watcher = RecommendedWatcher::new(
        move |res| {
            let _ = tx.send(res);
        },
        NotifyConfig::default().with_poll_interval(Duration::from_millis(100)),
    )?;

    // Watch the state file's parent directory (file may not exist yet)
    if let Some(parent) = state_path.parent() {
        std::fs::create_dir_all(parent)?;
        watcher.watch(parent, RecursiveMode::NonRecursive)?;
    }

    // Also try to watch the file directly if it exists
    if state_path.exists() {
        let _ = watcher.watch(&state_path, RecursiveMode::NonRecursive);
    }

    let mut last_state = state.to_string();

    loop {
        match rx.recv_timeout(Duration::from_millis(500)) {
            Ok(Ok(_event)) => {
                // File changed, read new state
                if let Ok(new_state) = std::fs::read_to_string(&state_path) {
                    let new_state = new_state.trim().to_string();
                    if new_state != last_state {
                        if format == "json" {
                            println!("{}", format_state_json(&new_state, ext_info.as_ref()));
                        } else {
                            println!("{}", new_state);
                        }
                        last_state = new_state;
                    }
                }
            }
            Ok(Err(e)) => {
                tracing::warn!("Watch error: {:?}", e);
            }
            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
                // Check if file was deleted (daemon stopped)
                if !state_path.exists() && last_state != "stopped" {
                    if format == "json" {
                        println!("{}", format_state_json("stopped", ext_info.as_ref()));
                    } else {
                        println!("stopped");
                    }
                    last_state = "stopped".to_string();
                }
            }
            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
                break;
            }
        }
    }

    Ok(())
}

/// Format state as JSON for Waybar consumption
fn format_state_json(state: &str, extended: Option<&ExtendedStatusInfo>) -> String {
    let (text, class, base_tooltip) = match state {
        "recording" => ("🎤", "recording", "Recording..."),
        "transcribing" => ("⏳", "transcribing", "Transcribing..."),
        "idle" => ("🎙️", "idle", "Voxtype ready - hold hotkey to record"),
        "stopped" => ("", "stopped", "Voxtype not running"),
        _ => ("?", "unknown", "Unknown state"),
    };

    match extended {
        Some(info) => {
            // Extended format includes model, device, backend
            let tooltip = format!(
                "{}\\nModel: {}\\nDevice: {}\\nBackend: {}",
                base_tooltip, info.model, info.device, info.backend
            );
            format!(
                r#"{{"text": "{}", "class": "{}", "tooltip": "{}", "model": "{}", "device": "{}", "backend": "{}"}}"#,
                text, class, tooltip, info.model, info.device, info.backend
            )
        }
        None => {
            format!(
                r#"{{"text": "{}", "class": "{}", "tooltip": "{}"}}"#,
                text, class, base_tooltip
            )
        }
    }
}

/// Show current configuration
async fn show_config(config: &config::Config) -> anyhow::Result<()> {
    println!("Current Configuration\n");
    println!("=====================\n");

    println!("[hotkey]");
    println!("  key = {:?}", config.hotkey.key);
    println!("  modifiers = {:?}", config.hotkey.modifiers);
    println!("  mode = {:?}", config.hotkey.mode);

    println!("\n[audio]");
    println!("  device = {:?}", config.audio.device);
    println!("  sample_rate = {}", config.audio.sample_rate);
    println!("  max_duration_secs = {}", config.audio.max_duration_secs);

    println!("\n[audio.feedback]");
    println!("  enabled = {}", config.audio.feedback.enabled);
    println!("  theme = {:?}", config.audio.feedback.theme);
    println!("  volume = {}", config.audio.feedback.volume);

    println!("\n[whisper]");
    println!("  model = {:?}", config.whisper.model);
    println!("  language = {:?}", config.whisper.language);
    println!("  translate = {}", config.whisper.translate);
    if let Some(threads) = config.whisper.threads {
        println!("  threads = {}", threads);
    }

    println!("\n[output]");
    println!("  mode = {:?}", config.output.mode);
    println!(
        "  fallback_to_clipboard = {}",
        config.output.fallback_to_clipboard
    );
    println!("  type_delay_ms = {}", config.output.type_delay_ms);

    println!("\n[output.notification]");
    println!(
        "  on_recording_start = {}",
        config.output.notification.on_recording_start
    );
    println!(
        "  on_recording_stop = {}",
        config.output.notification.on_recording_stop
    );
    println!(
        "  on_transcription = {}",
        config.output.notification.on_transcription
    );

    if let Some(ref state_file) = config.state_file {
        println!("\n[integration]");
        println!("  state_file = {:?}", state_file);
        if let Some(resolved) = config.resolve_state_file() {
            println!("  (resolves to: {:?})", resolved);
        }
    }

    // Show output chain status
    let output_status = setup::detect_output_chain().await;
    setup::print_output_chain_status(&output_status);

    println!("\n---");
    println!(
        "Config file: {:?}",
        config::Config::default_path().unwrap_or_else(|| PathBuf::from("(not found)"))
    );
    println!("Models dir: {:?}", config::Config::models_dir());

    Ok(())
}
