use anyhow::{anyhow, Result};
use clap::ValueEnum;
use ec_chopper::{deterministic_h264_profile, ffmpeg_profile_args};
use ec_core::{BroadcastId, SourceId};
use ec_hdhomerun::{find_lineup_entry_by_name, find_lineup_entry_by_number, LineupEntry};
use ec_linux_iptv::LinuxDvbConfig;
use ec_ts::probe_transport_stream_identity;
use std::io::Read;
use std::process::{Child, Command, Stdio};
use std::thread;

pub trait StreamSource: Send {
    fn open_stream(&self) -> Result<Box<dyn Read + Send>>;
    fn source_id(&self) -> SourceId;
    fn broadcast_id(&self) -> Result<Option<BroadcastId>> {
        Ok(None)
    }
}

#[derive(Debug, Clone)]
pub struct HdhrSource {
    pub host: Option<String>,
    pub device_id: Option<String>,
    pub channel: Option<String>,
    pub name: Option<String>,
    pub prefer_mdns: bool,
}

impl StreamSource for HdhrSource {
    fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
        let entry = resolve_hdhr_lineup_entry(self)?;
        Ok(Box::new(ec_hdhomerun::open_stream_entry(&entry, None)?))
    }

    fn source_id(&self) -> SourceId {
        let device_id = self.device_id.clone().or_else(|| self.host.clone());
        SourceId {
            kind: "hdhr".to_string(),
            device_id,
            channel: self.channel.clone().or_else(|| self.name.clone()),
        }
    }

    fn broadcast_id(&self) -> Result<Option<BroadcastId>> {
        let entry = resolve_hdhr_lineup_entry(self)?;
        let mut broadcast = entry.channel.broadcast_id("atsc");
        if broadcast.as_ref().is_none_or(|identity| {
            identity.transport_stream_id.is_none() || identity.program_number.is_none()
        }) {
            let probe = ec_hdhomerun::open_stream_entry(&entry, Some(2))?;
            broadcast = merge_broadcast_identity(
                broadcast,
                probe_transport_stream_broadcast(Box::new(probe), Some("atsc"))?,
            );
        }
        Ok(broadcast)
    }
}

fn resolve_hdhr_device(source: &HdhrSource) -> Result<ec_hdhomerun::HdhomerunDevice> {
    if let Some(host) = &source.host {
        return ec_hdhomerun::discover_from_host(host);
    }

    if let Some(device_id) = &source.device_id {
        let host = format!("{device_id}.local");
        return ec_hdhomerun::discover_from_host(&host);
    }

    if source.prefer_mdns {
        if let Ok(device) = ec_hdhomerun::discover_from_host("hdhomerun.local") {
            return Ok(device);
        }
    }

    let mut devices = ec_hdhomerun::discover()?;
    devices
        .pop()
        .ok_or_else(|| anyhow!("no HDHomeRun devices found"))
}

fn resolve_hdhr_lineup_entry(source: &HdhrSource) -> Result<LineupEntry> {
    let device = resolve_hdhr_device(source)?;
    let lineup = ec_hdhomerun::fetch_lineup(&device)?;
    let entry = if let Some(channel) = &source.channel {
        find_lineup_entry_by_number(&lineup, channel)
            .or_else(|| find_lineup_entry_by_name(&lineup, channel))
            .ok_or_else(|| anyhow!("channel not found: {channel}"))?
    } else if let Some(name) = &source.name {
        find_lineup_entry_by_name(&lineup, name)
            .ok_or_else(|| anyhow!("channel not found: {name}"))?
    } else {
        return Err(anyhow!("--channel or --name required for hdhr"));
    };

    Ok(entry.clone())
}

#[derive(Debug, Clone)]
pub struct LinuxDvbSource {
    pub adapter: u32,
    pub dvr: u32,
    pub tune_cmd: Vec<String>,
    pub tune_wait_ms: Option<u64>,
}

impl StreamSource for LinuxDvbSource {
    fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
        let config = LinuxDvbConfig {
            adapter: self.adapter,
            frontend: 0,
            dvr: self.dvr,
            tune_command: if self.tune_cmd.is_empty() {
                None
            } else {
                Some(self.tune_cmd.clone())
            },
            tune_timeout_ms: self.tune_wait_ms,
        };
        Ok(Box::new(ec_linux_iptv::open_stream(&config)?))
    }

    fn source_id(&self) -> SourceId {
        SourceId {
            kind: "linux-dvb".to_string(),
            device_id: Some(format!("adapter{}:dvr{}", self.adapter, self.dvr)),
            channel: None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct TsSource {
    pub input: String,
}

impl StreamSource for TsSource {
    fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
        if self.input.starts_with("http://") || self.input.starts_with("https://") {
            Ok(Box::new(ec_hdhomerun::open_stream_url(&self.input, None)?))
        } else {
            Ok(Box::new(std::fs::File::open(&self.input)?))
        }
    }

    fn source_id(&self) -> SourceId {
        SourceId {
            kind: "ts".to_string(),
            device_id: None,
            channel: None,
        }
    }

    fn broadcast_id(&self) -> Result<Option<BroadcastId>> {
        if self.input.starts_with("http://") || self.input.starts_with("https://") {
            let reader = ec_hdhomerun::open_stream_url(&self.input, Some(2))?;
            probe_transport_stream_broadcast(Box::new(reader), None)
        } else {
            let reader = std::fs::File::open(&self.input)?;
            probe_transport_stream_broadcast(Box::new(reader), None)
        }
    }
}

#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum HlsMode {
    Passthrough,
    Remux,
    Transcode,
}

impl Default for HlsMode {
    fn default() -> Self {
        HlsMode::Passthrough
    }
}

#[derive(Debug, Clone)]
pub struct HlsSource {
    pub url: String,
    pub mode: HlsMode,
}

impl StreamSource for HlsSource {
    fn open_stream(&self) -> Result<Box<dyn Read + Send>> {
        let mut cmd = Command::new("ffmpeg");
        cmd.arg("-hide_banner")
            .arg("-loglevel")
            .arg("error")
            .arg("-nostdin")
            .arg("-i")
            .arg(&self.url);

        match self.mode {
            HlsMode::Passthrough => {
                cmd.arg("-c").arg("copy");
            }
            HlsMode::Remux => {
                cmd.arg("-fflags").arg("+genpts").arg("-c").arg("copy");
            }
            HlsMode::Transcode => {
                let profile = deterministic_h264_profile();
                for arg in ffmpeg_profile_args(&profile) {
                    cmd.arg(arg);
                }
            }
        }

        cmd.arg("-f")
            .arg("mpegts")
            .arg("pipe:1")
            .stdout(Stdio::piped())
            .stderr(Stdio::inherit());

        let mut child = cmd
            .spawn()
            .map_err(|err| anyhow!("failed to spawn ffmpeg: {err}"))?;
        let stdout = child
            .stdout
            .take()
            .ok_or_else(|| anyhow!("ffmpeg stdout unavailable"))?;
        Ok(Box::new(FfmpegChildStream { child, stdout }))
    }

    fn source_id(&self) -> SourceId {
        SourceId {
            kind: "hls".to_string(),
            device_id: None,
            channel: Some(self.url.clone()),
        }
    }
}

fn probe_transport_stream_broadcast(
    reader: Box<dyn Read + Send>,
    fallback_standard: Option<&str>,
) -> Result<Option<BroadcastId>> {
    let Some(identity) = probe_transport_stream_identity(reader, 256)? else {
        return Ok(None);
    };

    let virtual_channel = match (identity.major_channel_number, identity.minor_channel_number) {
        (Some(major), Some(minor)) => Some(format!("{major}.{minor}")),
        _ => None,
    };

    Ok(Some(BroadcastId {
        standard: identity
            .standard
            .or_else(|| fallback_standard.map(|value| value.to_string()))
            .unwrap_or_else(|| "mpegts".to_string()),
        transport_stream_id: Some(identity.transport_stream_id),
        program_number: identity.program_number,
        virtual_channel,
        callsign: identity.short_name,
        region: None,
        frequency: None,
    }))
}

fn merge_broadcast_identity(
    base: Option<BroadcastId>,
    probed: Option<BroadcastId>,
) -> Option<BroadcastId> {
    match (base, probed) {
        (None, other) => other,
        (some, None) => some,
        (Some(mut base), Some(probed)) => {
            if base.standard.trim().is_empty() {
                base.standard = probed.standard;
            }
            if base.transport_stream_id.is_none() {
                base.transport_stream_id = probed.transport_stream_id;
            }
            if base.program_number.is_none() {
                base.program_number = probed.program_number;
            }
            if base.virtual_channel.is_none() {
                base.virtual_channel = probed.virtual_channel;
            }
            if base.callsign.is_none() {
                base.callsign = probed.callsign;
            }
            if base.region.is_none() {
                base.region = probed.region;
            }
            if base.frequency.is_none() {
                base.frequency = probed.frequency;
            }
            Some(base)
        }
    }
}

struct FfmpegChildStream {
    child: Child,
    stdout: std::process::ChildStdout,
}

impl Read for FfmpegChildStream {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.stdout.read(buf)
    }
}

impl Drop for FfmpegChildStream {
    fn drop(&mut self) {
        let _ = self.child.kill();
    }
}

pub fn deterministic_transcode_stream(
    reader: Box<dyn Read + Send>,
) -> Result<Box<dyn Read + Send>> {
    let profile = deterministic_h264_profile();
    let mut cmd = Command::new("ffmpeg");
    cmd.arg("-hide_banner")
        .arg("-loglevel")
        .arg("error")
        .arg("-nostdin")
        .arg("-i")
        .arg("pipe:0");

    for arg in ffmpeg_profile_args(&profile) {
        cmd.arg(arg);
    }

    cmd.arg("-f")
        .arg("mpegts")
        .arg("pipe:1")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::inherit());

    let mut child = cmd
        .spawn()
        .map_err(|err| anyhow!("failed to spawn ffmpeg: {err}"))?;
    let mut stdin = child
        .stdin
        .take()
        .ok_or_else(|| anyhow!("ffmpeg stdin unavailable"))?;
    let stdout = child
        .stdout
        .take()
        .ok_or_else(|| anyhow!("ffmpeg stdout unavailable"))?;

    let writer = thread::spawn(move || {
        let mut reader = reader;
        let _ = std::io::copy(&mut reader, &mut stdin);
    });

    Ok(Box::new(FfmpegTranscodeStream {
        child,
        stdout,
        writer: Some(writer),
    }))
}

struct FfmpegTranscodeStream {
    child: Child,
    stdout: std::process::ChildStdout,
    writer: Option<thread::JoinHandle<()>>,
}

impl Read for FfmpegTranscodeStream {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.stdout.read(buf)
    }
}

impl Drop for FfmpegTranscodeStream {
    fn drop(&mut self) {
        let _ = self.child.kill();
        if let Some(writer) = self.writer.take() {
            let _ = writer.join();
        }
    }
}
