//! Live media feed controls shared by the launcher and relay child.

use std::{
    fs,
    path::{Path, PathBuf},
    sync::{Arc, Mutex},
    time::{SystemTime, UNIX_EPOCH},
};

use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use lesavka_common::audio_transport::{UpstreamAudioCodec, parse_upstream_audio_codec};

pub const MEDIA_CONTROL_ENV: &str = "LESAVKA_MEDIA_CONTROL";
pub const DEFAULT_MEDIA_CONTROL_PATH: &str = "/tmp/lesavka-media.control";

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum MediaDeviceChoice {
    Inherit,
    Auto,
    Selected(String),
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum MediaAudioCodecChoice {
    Inherit,
    Selected(UpstreamAudioCodec),
}

impl MediaAudioCodecChoice {
    #[must_use]
    pub fn selected(codec: UpstreamAudioCodec) -> Self {
        Self::Selected(codec)
    }

    #[must_use]
    pub fn resolve(&self, fallback: UpstreamAudioCodec) -> UpstreamAudioCodec {
        match self {
            Self::Inherit => fallback,
            Self::Selected(codec) => *codec,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum MediaCameraCodecChoice {
    Inherit,
    Selected(String),
}

impl MediaCameraCodecChoice {
    #[must_use]
    pub fn selected(codec: Option<String>) -> Self {
        codec
            .filter(|value| !value.trim().is_empty())
            .map(|value| Self::Selected(value.trim().to_ascii_lowercase()))
            .unwrap_or(Self::Inherit)
    }

    #[must_use]
    pub fn resolve(&self, fallback: Option<&str>) -> Option<String> {
        match self {
            Self::Inherit => fallback.map(str::to_string),
            Self::Selected(codec) => Some(codec.clone()),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum MediaNoiseSuppressionChoice {
    Inherit,
    Enabled,
    Disabled,
}

impl MediaNoiseSuppressionChoice {
    #[must_use]
    pub fn selected(enabled: bool) -> Self {
        if enabled {
            Self::Enabled
        } else {
            Self::Disabled
        }
    }

    #[must_use]
    pub fn resolve(&self, fallback: bool) -> bool {
        match self {
            Self::Inherit => fallback,
            Self::Enabled => true,
            Self::Disabled => false,
        }
    }
}

impl MediaDeviceChoice {
    #[must_use]
    pub fn from_selection(selection: Option<String>) -> Self {
        selection
            .filter(|value| !value.trim().is_empty())
            .map(Self::Selected)
            .unwrap_or(Self::Auto)
    }

    #[must_use]
    /// Keeps `resolve` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
    /// Inputs are the typed parameters; output is the return value or side effect.
    pub fn resolve(&self, fallback: Option<&str>) -> Option<String> {
        match self {
            Self::Inherit => fallback.map(str::to_string),
            Self::Auto => None,
            Self::Selected(value) => Some(value.clone()),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct MediaControlState {
    pub camera: bool,
    pub microphone: bool,
    pub audio: bool,
    pub camera_source: MediaDeviceChoice,
    pub camera_profile: MediaDeviceChoice,
    pub microphone_source: MediaDeviceChoice,
    pub audio_sink: MediaDeviceChoice,
    pub camera_codec: MediaCameraCodecChoice,
    pub audio_codec: MediaAudioCodecChoice,
    pub noise_suppression: MediaNoiseSuppressionChoice,
}

impl MediaControlState {
    #[must_use]
    pub fn new(camera: bool, microphone: bool, audio: bool) -> Self {
        Self {
            camera,
            microphone,
            audio,
            camera_source: MediaDeviceChoice::Inherit,
            camera_profile: MediaDeviceChoice::Inherit,
            microphone_source: MediaDeviceChoice::Inherit,
            audio_sink: MediaDeviceChoice::Inherit,
            camera_codec: MediaCameraCodecChoice::Inherit,
            audio_codec: MediaAudioCodecChoice::Inherit,
            noise_suppression: MediaNoiseSuppressionChoice::Inherit,
        }
    }

    #[must_use]
    pub fn with_devices(
        camera: bool,
        microphone: bool,
        audio: bool,
        camera_source: Option<String>,
        camera_profile: Option<String>,
        microphone_source: Option<String>,
        audio_sink: Option<String>,
    ) -> Self {
        Self {
            camera,
            microphone,
            audio,
            camera_source: MediaDeviceChoice::from_selection(camera_source),
            camera_profile: MediaDeviceChoice::from_selection(camera_profile),
            microphone_source: MediaDeviceChoice::from_selection(microphone_source),
            audio_sink: MediaDeviceChoice::from_selection(audio_sink),
            camera_codec: MediaCameraCodecChoice::Inherit,
            audio_codec: MediaAudioCodecChoice::Inherit,
            noise_suppression: MediaNoiseSuppressionChoice::Inherit,
        }
    }

    #[must_use]
    #[allow(clippy::too_many_arguments)]
    pub fn with_devices_and_audio(
        camera: bool,
        microphone: bool,
        audio: bool,
        camera_source: Option<String>,
        camera_profile: Option<String>,
        microphone_source: Option<String>,
        audio_sink: Option<String>,
        audio_codec: UpstreamAudioCodec,
        noise_suppression: bool,
    ) -> Self {
        Self {
            audio_codec: MediaAudioCodecChoice::selected(audio_codec),
            noise_suppression: MediaNoiseSuppressionChoice::selected(noise_suppression),
            ..Self::with_devices(
                camera,
                microphone,
                audio,
                camera_source,
                camera_profile,
                microphone_source,
                audio_sink,
            )
        }
    }

    #[must_use]
    #[allow(clippy::too_many_arguments)]
    pub fn with_devices_and_codecs(
        camera: bool,
        microphone: bool,
        audio: bool,
        camera_source: Option<String>,
        camera_profile: Option<String>,
        microphone_source: Option<String>,
        audio_sink: Option<String>,
        camera_codec: Option<String>,
        audio_codec: UpstreamAudioCodec,
        noise_suppression: bool,
    ) -> Self {
        Self {
            camera_codec: MediaCameraCodecChoice::selected(camera_codec),
            audio_codec: MediaAudioCodecChoice::selected(audio_codec),
            noise_suppression: MediaNoiseSuppressionChoice::selected(noise_suppression),
            ..Self::with_devices(
                camera,
                microphone,
                audio,
                camera_source,
                camera_profile,
                microphone_source,
                audio_sink,
            )
        }
    }
}

#[derive(Clone, Debug)]
pub(crate) struct LiveMediaControls {
    path: PathBuf,
    inner: Arc<Mutex<LiveMediaControlsInner>>,
}

#[derive(Debug)]
struct LiveMediaControlsInner {
    state: MediaControlState,
}

impl LiveMediaControls {
    #[must_use]
    pub fn from_env(initial: MediaControlState) -> Self {
        let path = std::env::var(MEDIA_CONTROL_ENV)
            .map(PathBuf::from)
            .unwrap_or_else(|_| PathBuf::from(DEFAULT_MEDIA_CONTROL_PATH));
        let controls = Self {
            path,
            inner: Arc::new(Mutex::new(LiveMediaControlsInner { state: initial })),
        };
        let _ = controls.refresh();
        controls
    }

    /// Reloads the launcher-written soft-pause switches, falling back safely on read errors.
    pub fn refresh(&self) -> MediaControlState {
        let Ok(mut inner) = self.inner.lock() else {
            return MediaControlState::new(true, true, true);
        };
        if let Ok(raw) = fs::read_to_string(&self.path)
            && let Some(state) = parse_media_control_state(&raw)
        {
            inner.state = state;
        }
        inner.state.clone()
    }
}

/// Writes one atomic-ish soft-pause/device request for the running relay child to poll.
pub(crate) fn write_media_control_request(
    path: &Path,
    state: MediaControlState,
) -> std::io::Result<()> {
    fs::write(
        path,
        format!(
            "camera={} microphone={} audio={} camera_source={} camera_profile={} microphone_source={} audio_sink={} camera_codec={} audio_codec={} noise_suppression={} nonce={}\n",
            bool_flag(state.camera),
            bool_flag(state.microphone),
            bool_flag(state.audio),
            encode_choice(&state.camera_source),
            encode_choice(&state.camera_profile),
            encode_choice(&state.microphone_source),
            encode_choice(&state.audio_sink),
            encode_camera_codec_choice(&state.camera_codec),
            encode_audio_codec_choice(&state.audio_codec),
            encode_noise_suppression_choice(&state.noise_suppression),
            control_request_nonce(),
        ),
    )
}

/// Parses the small launcher control-file grammar used across process boundaries.
fn parse_media_control_state(raw: &str) -> Option<MediaControlState> {
    let mut camera = None;
    let mut microphone = None;
    let mut audio = None;
    let mut camera_source = MediaDeviceChoice::Inherit;
    let mut camera_profile = MediaDeviceChoice::Inherit;
    let mut microphone_source = MediaDeviceChoice::Inherit;
    let mut audio_sink = MediaDeviceChoice::Inherit;
    let mut camera_codec = MediaCameraCodecChoice::Inherit;
    let mut audio_codec = MediaAudioCodecChoice::Inherit;
    let mut noise_suppression = MediaNoiseSuppressionChoice::Inherit;
    for token in raw.split_ascii_whitespace() {
        let Some((key, value)) = token.split_once('=') else {
            continue;
        };
        match key {
            "camera" => camera = Some(parse_bool_flag(value)?),
            "microphone" | "mic" => microphone = Some(parse_bool_flag(value)?),
            "audio" | "speaker" => audio = Some(parse_bool_flag(value)?),
            "camera_source" | "camera_source_b64" => camera_source = parse_choice(value)?,
            "camera_profile" | "camera_quality" => camera_profile = parse_choice(value)?,
            "microphone_source" | "mic_source" | "microphone_source_b64" => {
                microphone_source = parse_choice(value)?;
            }
            "audio_sink" | "speaker_sink" | "audio_sink_b64" => audio_sink = parse_choice(value)?,
            "camera_codec" | "uplink_camera_codec" | "webcam_transport" => {
                camera_codec = parse_camera_codec_choice(value)?;
            }
            "audio_codec" | "uplink_audio_codec" => {
                audio_codec = parse_audio_codec_choice(value)?;
            }
            "noise_suppression" | "mic_noise_suppression" => {
                noise_suppression = parse_noise_suppression_choice(value)?;
            }
            _ => {}
        }
    }
    Some(MediaControlState {
        camera: camera?,
        microphone: microphone?,
        audio: audio?,
        camera_source,
        camera_profile,
        microphone_source,
        audio_sink,
        camera_codec,
        audio_codec,
        noise_suppression,
    })
}

/// Keeps `encode_choice` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
fn encode_choice(choice: &MediaDeviceChoice) -> String {
    match choice {
        MediaDeviceChoice::Inherit => "inherit".to_string(),
        MediaDeviceChoice::Auto => "auto".to_string(),
        MediaDeviceChoice::Selected(value) => format!("b64:{}", B64.encode(value.as_bytes())),
    }
}

/// Keeps `parse_choice` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_choice(value: &str) -> Option<MediaDeviceChoice> {
    let value = value.trim();
    if value.is_empty() || value.eq_ignore_ascii_case("auto") {
        return Some(MediaDeviceChoice::Auto);
    }
    if value.eq_ignore_ascii_case("inherit") {
        return Some(MediaDeviceChoice::Inherit);
    }
    if let Some(encoded) = value.strip_prefix("b64:") {
        let decoded = B64.decode(encoded).ok()?;
        let decoded = String::from_utf8(decoded).ok()?;
        return Some(MediaDeviceChoice::from_selection(Some(decoded)));
    }
    Some(MediaDeviceChoice::from_selection(Some(value.to_string())))
}

fn encode_camera_codec_choice(choice: &MediaCameraCodecChoice) -> &str {
    match choice {
        MediaCameraCodecChoice::Inherit => "inherit",
        MediaCameraCodecChoice::Selected(codec) => codec.as_str(),
    }
}

fn parse_camera_codec_choice(value: &str) -> Option<MediaCameraCodecChoice> {
    let value = value.trim();
    if value.eq_ignore_ascii_case("inherit") || value.is_empty() {
        return Some(MediaCameraCodecChoice::Inherit);
    }
    match value.to_ascii_lowercase().as_str() {
        "mjpeg" | "mjpg" | "jpeg" => Some(MediaCameraCodecChoice::Selected("mjpeg".to_string())),
        "hevc" | "h265" | "h.265" => Some(MediaCameraCodecChoice::Selected("hevc".to_string())),
        "h264" => Some(MediaCameraCodecChoice::Selected("h264".to_string())),
        _ => None,
    }
}

fn encode_audio_codec_choice(choice: &MediaAudioCodecChoice) -> &'static str {
    match choice {
        MediaAudioCodecChoice::Inherit => "inherit",
        MediaAudioCodecChoice::Selected(codec) => codec.as_id(),
    }
}

fn parse_audio_codec_choice(value: &str) -> Option<MediaAudioCodecChoice> {
    let value = value.trim();
    if value.eq_ignore_ascii_case("inherit") || value.is_empty() {
        return Some(MediaAudioCodecChoice::Inherit);
    }
    parse_upstream_audio_codec(value).map(MediaAudioCodecChoice::Selected)
}

fn encode_noise_suppression_choice(choice: &MediaNoiseSuppressionChoice) -> &'static str {
    match choice {
        MediaNoiseSuppressionChoice::Inherit => "inherit",
        MediaNoiseSuppressionChoice::Enabled => "1",
        MediaNoiseSuppressionChoice::Disabled => "0",
    }
}

fn parse_noise_suppression_choice(value: &str) -> Option<MediaNoiseSuppressionChoice> {
    if value.trim().eq_ignore_ascii_case("inherit") || value.trim().is_empty() {
        return Some(MediaNoiseSuppressionChoice::Inherit);
    }
    parse_bool_flag(value).map(MediaNoiseSuppressionChoice::selected)
}

/// Keeps `parse_bool_flag` explicit because it sits on this module contract, where hidden behavior would make regressions difficult to diagnose.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_bool_flag(value: &str) -> Option<bool> {
    match value.trim().to_ascii_lowercase().as_str() {
        "1" | "true" | "on" | "yes" => Some(true),
        "0" | "false" | "off" | "no" => Some(false),
        _ => None,
    }
}

const fn bool_flag(enabled: bool) -> &'static str {
    if enabled { "1" } else { "0" }
}

fn control_request_nonce() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or_default()
}

#[cfg(test)]
#[path = "live_media_control/tests/mod.rs"]
mod tests;
