use anyhow::{anyhow, Context, Result};
use blake3;
use clap::{Parser, Subcommand};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::PathBuf;

#[derive(Parser, Debug)]
#[command(name = "every.channel")]
#[command(about = "CLI for the every.channel mesh", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Discover HDHomeRun devices on the network.
    Discover,
    /// Fetch channel lineup for a device.
    Lineup {
        /// Hostname or IP (e.g. 192.168.1.10 or hdhomerun.local).
        #[arg(long)]
        host: Option<String>,
        /// Device ID (used as <deviceid>.local).
        #[arg(long)]
        device_id: Option<String>,
    },
    /// Parse lineup JSON from a file on disk.
    LineupFile { path: String },
    /// Open an HDHomeRun stream and dump MPEG-TS to a file.
    StreamDump {
        /// Hostname or IP (e.g. 192.168.1.10).
        #[arg(long)]
        host: Option<String>,
        /// Device ID (used as <deviceid>.local).
        #[arg(long)]
        device_id: Option<String>,
        /// Guide number (e.g. 8.1).
        #[arg(long)]
        channel: Option<String>,
        /// Guide name (e.g. KQED).
        #[arg(long)]
        name: Option<String>,
        /// Optional duration in seconds (if supported by the tuner URL).
        #[arg(long)]
        duration: Option<u32>,
        /// Output path for the transport stream.
        #[arg(long, default_value = "stream.ts")]
        output: PathBuf,
    },
    /// Chunk an input stream using ffmpeg.
    Chunk {
        /// Input URL or file path.
        input: String,
        /// Output directory for segments.
        output_dir: PathBuf,
    },
    /// Probe a media file using ac-ffmpeg.
    Probe {
        /// Input file path.
        input: String,
    },
    /// Analyze TS timing and chunk boundaries.
    TsSync {
        /// Input TS file.
        input: String,
        /// Chunk duration in ms.
        #[arg(long, default_value_t = 2000)]
        chunk_ms: u64,
        /// Maximum number of events to print.
        #[arg(long, default_value_t = 50)]
        max_events: usize,
    },
    /// Re-encode the same input multiple times and compare segment hashes.
    DeterminismTest {
        /// Input file path (TS or other supported by ffmpeg).
        input: String,
        /// Output directory root (runs will be placed under run-*/).
        output_dir: PathBuf,
        /// Number of runs to compare.
        #[arg(long, default_value_t = 2)]
        runs: usize,
    },
    /// Open a Linux DVB DVR device and dump MPEG-TS to a file.
    LinuxDvbDump {
        /// DVB adapter index.
        #[arg(long, default_value_t = 0)]
        adapter: u32,
        /// DVR device index.
        #[arg(long, default_value_t = 0)]
        dvr: u32,
        /// Optional tune command (repeat for each arg).
        #[arg(long, allow_hyphen_values = true)]
        tune_cmd: Vec<String>,
        /// Optional tune wait (ms).
        #[arg(long)]
        tune_wait_ms: Option<u64>,
        /// Output path for the transport stream.
        #[arg(long, default_value = "linux-dvb.ts")]
        output: PathBuf,
    },
}

fn main() -> Result<()> {
    tracing_subscriber::fmt().init();
    let cli = Cli::parse();

    match cli.command {
        Commands::Discover => {
            let devices = ec_hdhomerun::discover()?;
            println!("{}", serde_json::to_string_pretty(&devices)?);
        }
        Commands::Lineup { host, device_id } => {
            let device = resolve_device(host, device_id)?;
            let lineup = ec_hdhomerun::fetch_lineup(&device)?;
            println!("{}", serde_json::to_string_pretty(&lineup)?);
        }
        Commands::LineupFile { path } => {
            let bytes = fs::read(&path)?;
            let lineup = ec_hdhomerun::lineup_from_json_bytes(&bytes, None)?;
            println!("{}", serde_json::to_string_pretty(&lineup)?);
        }
        Commands::StreamDump {
            host,
            device_id,
            channel,
            name,
            duration,
            output,
        } => {
            let device = resolve_device(host, device_id)?;
            let lineup = ec_hdhomerun::fetch_lineup(&device)?;
            let entry = if let Some(channel) = channel {
                ec_hdhomerun::find_lineup_entry_by_number(&lineup, &channel)
                    .or_else(|| ec_hdhomerun::find_lineup_entry_by_name(&lineup, &channel))
                    .ok_or_else(|| anyhow!("channel not found: {channel}"))?
            } else if let Some(name) = name {
                ec_hdhomerun::find_lineup_entry_by_name(&lineup, &name)
                    .ok_or_else(|| anyhow!("channel not found: {name}"))?
            } else {
                return Err(anyhow!("--channel or --name required"));
            };

            let mut stream = ec_hdhomerun::open_stream_entry(entry, duration)?;
            let mut file = File::create(&output)
                .with_context(|| format!("failed to create {}", output.display()))?;

            let mut buf = [0u8; 8192];
            loop {
                let read = stream.read(&mut buf)?;
                if read == 0 {
                    break;
                }
                file.write_all(&buf[..read])?;
            }
        }
        Commands::Chunk { input, output_dir } => {
            let profile = ec_chopper::deterministic_h264_profile();
            let config = ec_chopper::ChunkerConfig {
                output_dir,
                segment_duration_ms: profile.chunk_duration_ms,
                segment_template: ec_chopper::ChunkerConfig::default_segment_template(),
                format: ec_chopper::ChunkFormat::Fmp4,
                profile,
            };

            let input = if input.starts_with("http://") || input.starts_with("https://") {
                ec_chopper::ChunkerInput::Url(input)
            } else {
                ec_chopper::ChunkerInput::File(PathBuf::from(input))
            };

            let segmenter = ec_chopper::FfmpegCliSegmenter::default();
            let mut process = segmenter.spawn(input, &config)?;
            let status = process.child.wait()?;
            if !status.success() {
                return Err(anyhow!("ffmpeg exited with status {status}"));
            }

            let manifest = ec_chopper::collect_segments(&process.output_dir)?;
            println!("{}", serde_json::to_string_pretty(&manifest)?);
        }
        Commands::Probe { input } => {
            let file = File::open(&input).with_context(|| format!("failed to open {}", input))?;
            let probes = ec_chopper::probe_read_stream(file)?;
            println!("{}", serde_json::to_string_pretty(&probes)?);
        }
        Commands::TsSync {
            input,
            chunk_ms,
            max_events,
        } => {
            let file = File::open(&input).with_context(|| format!("failed to open {}", input))?;
            let events = ec_chopper::analyze_ts_time(file, chunk_ms, max_events)?;
            println!("{}", serde_json::to_string_pretty(&events)?);
        }
        Commands::DeterminismTest {
            input,
            output_dir,
            runs,
        } => {
            if runs < 1 {
                return Err(anyhow!("runs must be >= 1"));
            }

            let profile = ec_chopper::deterministic_h264_profile();
            let format = ec_chopper::ChunkFormat::Fmp4;
            let template = ec_chopper::ChunkerConfig::default_segment_template();

            let mut baseline: Option<Vec<String>> = None;
            for run in 0..runs {
                let run_dir = output_dir.join(format!("run-{}", run + 1));
                let _ = fs::remove_dir_all(&run_dir);

                let config = ec_chopper::ChunkerConfig {
                    output_dir: run_dir.clone(),
                    segment_duration_ms: profile.chunk_duration_ms,
                    segment_template: template.clone(),
                    format: format.clone(),
                    profile: profile.clone(),
                };

                let input_spec = if input.starts_with("http://") || input.starts_with("https://") {
                    ec_chopper::ChunkerInput::Url(input.clone())
                } else {
                    ec_chopper::ChunkerInput::File(PathBuf::from(&input))
                };

                let segmenter = ec_chopper::FfmpegCliSegmenter::default();
                let mut process = segmenter.spawn(input_spec, &config)?;
                let status = process.child.wait()?;
                if !status.success() {
                    return Err(anyhow!("ffmpeg exited with status {status}"));
                }

                let hashes = hash_segments(&process.output_dir)?;
                match baseline.as_ref() {
                    None => {
                        baseline = Some(hashes);
                        println!(
                            "run {}: baseline ({}) segments",
                            run + 1,
                            baseline.as_ref().unwrap().len()
                        );
                    }
                    Some(base) => {
                        let mismatches = compare_hashes(base, &hashes);
                        if mismatches > 0 {
                            return Err(anyhow!(
                                "determinism mismatch on run {} ({} mismatches)",
                                run + 1,
                                mismatches
                            ));
                        }
                        println!("run {}: matched baseline", run + 1);
                    }
                }
            }
        }
        Commands::LinuxDvbDump {
            adapter,
            dvr,
            tune_cmd,
            tune_wait_ms,
            output,
        } => {
            let config = ec_linux_iptv::LinuxDvbConfig {
                adapter,
                frontend: 0,
                dvr,
                tune_command: if tune_cmd.is_empty() {
                    None
                } else {
                    Some(tune_cmd)
                },
                tune_timeout_ms: tune_wait_ms,
            };

            let mut stream = ec_linux_iptv::open_stream(&config)?;
            let mut file = File::create(&output)
                .with_context(|| format!("failed to create {}", output.display()))?;

            let mut buf = [0u8; 8192];
            loop {
                let read = stream.read(&mut buf)?;
                if read == 0 {
                    break;
                }
                file.write_all(&buf[..read])?;
            }
        }
    }

    Ok(())
}

fn hash_segments(output_dir: &PathBuf) -> Result<Vec<String>> {
    let manifest = ec_chopper::collect_segments(output_dir)?;
    let mut hashes = Vec::new();
    for segment in manifest.segments {
        let bytes = fs::read(&segment.path)
            .with_context(|| format!("failed to read {}", segment.path.display()))?;
        let hash = blake3::hash(&bytes);
        hashes.push(hash.to_hex().to_string());
    }
    Ok(hashes)
}

fn compare_hashes(base: &[String], candidate: &[String]) -> usize {
    let mut mismatches = 0usize;
    let max_len = base.len().max(candidate.len());
    for idx in 0..max_len {
        let base_hash = base.get(idx);
        let candidate_hash = candidate.get(idx);
        if base_hash != candidate_hash {
            mismatches += 1;
        }
    }
    mismatches
}

fn resolve_device(
    host: Option<String>,
    device_id: Option<String>,
) -> Result<ec_hdhomerun::HdhomerunDevice> {
    if let Some(host) = host {
        ec_hdhomerun::discover_from_host(&host)
    } else if let Some(device_id) = device_id {
        let host = format!("{device_id}.local");
        ec_hdhomerun::discover_from_host(&host)
    } else {
        let mut devices = ec_hdhomerun::discover()?;
        devices
            .pop()
            .ok_or_else(|| anyhow!("no HDHomeRun devices found"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::Parser;

    #[test]
    fn clap_parses_common_subcommands() {
        let cli = Cli::try_parse_from(["every.channel", "discover"]).unwrap();
        matches!(cli.command, Commands::Discover);

        let cli = Cli::try_parse_from([
            "every.channel",
            "ts-sync",
            "input.ts",
            "--chunk-ms",
            "1000",
            "--max-events",
            "5",
        ])
        .unwrap();
        matches!(cli.command, Commands::TsSync { .. });

        let cli = Cli::try_parse_from([
            "every.channel",
            "linux-dvb-dump",
            "--adapter",
            "0",
            "--dvr",
            "0",
            "--tune-cmd",
            "dvbv5-zap",
            "--tune-cmd",
            "-r",
            "--tune-cmd",
            "KQED",
        ])
        .unwrap();
        matches!(cli.command, Commands::LinuxDvbDump { .. });
    }
}
