const DEFAULT_EYE_RECORD_FPS: u32 = 30;

#[derive(Default)]
struct EyeRecordState {
    save_dir_override: Option<PathBuf>,
    recording_active: bool,
    frame_dir: Option<PathBuf>,
    recording_control_tx: Option<std::sync::mpsc::Sender<RecordFrameTask>>,
    finalize_rx: Option<std::sync::mpsc::Receiver<Result<PathBuf, String>>>,
    audio_mode: EyeRecordAudioMode,
    audio_recording: Option<EyeAudioRecording>,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum EyeRecordAudioMode {
    #[default]
    NoAudio,
    UpstreamOnly,
    DownstreamOnly,
    Both,
}

impl EyeRecordAudioMode {
    fn next(self) -> Self {
        match self {
            Self::NoAudio => Self::UpstreamOnly,
            Self::UpstreamOnly => Self::DownstreamOnly,
            Self::DownstreamOnly => Self::Both,
            Self::Both => Self::NoAudio,
        }
    }

    fn button_label(self) -> &'static str {
        match self {
            Self::NoAudio => "🔇",
            Self::UpstreamOnly => "👂↑",
            Self::DownstreamOnly => "👂↓",
            Self::Both => "👂↕",
        }
    }

    fn status_label(self) -> &'static str {
        match self {
            Self::NoAudio => "no audio",
            Self::UpstreamOnly => "upstream audio only",
            Self::DownstreamOnly => "downstream audio only",
            Self::Both => "upstream and downstream audio",
        }
    }

    fn tooltip(self) -> &'static str {
        match self {
            Self::NoAudio => "Next recording will be video-only.",
            Self::UpstreamOnly => {
                "Next recording will include only the local upstream microphone source."
            }
            Self::DownstreamOnly => {
                "Next recording will include only the downstream speaker monitor."
            }
            Self::Both => "Next recording will mix upstream microphone and downstream speaker monitor audio.",
        }
    }

    fn capture_kinds(self) -> &'static [EyeRecordAudioKind] {
        match self {
            Self::NoAudio => &[],
            Self::UpstreamOnly => &[EyeRecordAudioKind::Upstream],
            Self::DownstreamOnly => &[EyeRecordAudioKind::Downstream],
            Self::Both => &[EyeRecordAudioKind::Upstream, EyeRecordAudioKind::Downstream],
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum EyeRecordAudioKind {
    Upstream,
    Downstream,
}

impl EyeRecordAudioKind {
    fn env_var(self) -> &'static str {
        match self {
            Self::Upstream => "LESAVKA_EYE_RECORD_UPSTREAM_AUDIO_SOURCE",
            Self::Downstream => "LESAVKA_EYE_RECORD_DOWNSTREAM_AUDIO_SOURCE",
        }
    }

    fn file_slug(self) -> &'static str {
        match self {
            Self::Upstream => "upstream",
            Self::Downstream => "downstream",
        }
    }

    fn display_name(self) -> &'static str {
        match self {
            Self::Upstream => "upstream audio",
            Self::Downstream => "downstream audio",
        }
    }
}

struct EyeAudioCapture {
    path: PathBuf,
    child: std::process::Child,
}

struct EyeAudioRecording {
    captures: Vec<EyeAudioCapture>,
}

impl EyeAudioRecording {
    fn audio_paths(&self) -> Vec<PathBuf> {
        self.captures
            .iter()
            .map(|capture| capture.path.clone())
            .collect()
    }
}

enum RecordFrameTask {
    Finish,
}

fn eye_slug(title: &str) -> &'static str {
    if title.to_ascii_lowercase().contains("left") {
        "left-eye"
    } else {
        "right-eye"
    }
}

fn timestamp_slug() -> String {
    if let Ok(now) = glib::DateTime::now_local()
        && let Ok(stamp) = now.format("%Y%m%d-%H%M%S")
    {
        return stamp.to_string();
    }
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    format!("{}-{:03}", now.as_secs(), now.subsec_millis())
}

fn capture_day_slug() -> String {
    if let Ok(now) = glib::DateTime::now_local()
        && let Ok(stamp) = now.format("%Y-%m-%d")
    {
        return stamp.to_string();
    }
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    format!("{}", now.as_secs() / 86_400)
}

fn expand_home_token(raw: &str) -> PathBuf {
    if raw.contains("$HOME")
        && let Some(home) = std::env::var_os("HOME")
    {
        return PathBuf::from(raw.replace("$HOME", &home.to_string_lossy()));
    }
    if let Some(rest) = raw.strip_prefix("~/")
        && let Some(home) = std::env::var_os("HOME")
    {
        return PathBuf::from(home).join(rest);
    }
    PathBuf::from(raw)
}

fn default_eye_capture_root() -> PathBuf {
    if let Some(raw) = std::env::var_os("XDG_PICTURES_DIR") {
        let path = expand_home_token(&raw.to_string_lossy());
        if !path.as_os_str().is_empty() {
            return path.join("lesavka");
        }
    }
    if let Some(home) = std::env::var_os("HOME") {
        return PathBuf::from(home).join("Pictures").join("lesavka");
    }
    if let Some(profile) = std::env::var_os("USERPROFILE") {
        return PathBuf::from(profile).join("Pictures").join("lesavka");
    }
    std::env::current_dir()
        .unwrap_or_else(|_| PathBuf::from("."))
        .join("lesavka")
}

fn ensure_eye_capture_root(override_dir: Option<&Path>) -> Result<PathBuf, String> {
    let root = override_dir
        .map(Path::to_path_buf)
        .unwrap_or_else(default_eye_capture_root);
    std::fs::create_dir_all(&root)
        .map_err(|err| format!("could not create {}: {err}", root.display()))?;
    Ok(root)
}

fn ensure_eye_capture_dir(override_dir: Option<&Path>, kind: &str) -> Result<PathBuf, String> {
    let root = ensure_eye_capture_root(override_dir)?;
    let dir = root.join(capture_day_slug()).join(kind);
    std::fs::create_dir_all(&dir)
        .map_err(|err| format!("could not create {}: {err}", dir.display()))?;
    Ok(dir)
}

fn unique_capture_path(root: &Path, stem: &str, ext: &str) -> PathBuf {
    let mut candidate = root.join(format!("{stem}.{ext}"));
    if !candidate.exists() {
        return candidate;
    }
    for idx in 1..1000 {
        candidate = root.join(format!("{stem}-{idx}.{ext}"));
        if !candidate.exists() {
            break;
        }
    }
    candidate
}

fn current_eye_texture(picture: &gtk::Picture) -> Result<gtk::gdk::Texture, String> {
    let paintable = picture
        .paintable()
        .ok_or_else(|| "no live frame is available yet".to_string())?;
    paintable
        .downcast::<gtk::gdk::Texture>()
        .map_err(|_| "the current frame is not directly exportable".to_string())
}

fn save_texture_png(texture: &gtk::gdk::Texture, output_path: &Path) -> Result<(), String> {
    texture
        .save_to_png(output_path)
        .map_err(|err| format!("could not write {}: {err}", output_path.display()))
}

fn sync_record_audio_button(
    button: &gtk::Button,
    mode: EyeRecordAudioMode,
    recording_active: bool,
) {
    button.set_label(mode.button_label());
    button.set_tooltip_text(Some(if recording_active {
        "Audio mode is locked for the active recording."
    } else {
        mode.tooltip()
    }));
    button.set_sensitive(!recording_active);
}

fn env_audio_source(kind: EyeRecordAudioKind) -> Option<String> {
    std::env::var(kind.env_var())
        .ok()
        .map(|source| source.trim().to_string())
        .filter(|source| !source.is_empty())
}

fn command_output_trim(command: &str, args: &[&str]) -> Option<String> {
    let output = Command::new(command).args(args).output().ok()?;
    if !output.status.success() {
        return None;
    }
    String::from_utf8(output.stdout)
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn resolve_audio_source(kind: EyeRecordAudioKind) -> Result<String, String> {
    if let Some(source) = env_audio_source(kind) {
        return Ok(source);
    }
    match kind {
        EyeRecordAudioKind::Upstream => Ok(command_output_trim("pactl", &["get-default-source"])
            .unwrap_or_else(|| "default".to_string())),
        EyeRecordAudioKind::Downstream => command_output_trim("pactl", &["get-default-sink"])
            .map(|sink| format!("{sink}.monitor"))
            .ok_or_else(|| {
                "could not resolve downstream audio monitor; set LESAVKA_EYE_RECORD_DOWNSTREAM_AUDIO_SOURCE"
                    .to_string()
            }),
    }
}

fn spawn_audio_capture(source: &str, output_path: &Path) -> Result<std::process::Child, String> {
    let output_arg = output_path.to_string_lossy().into_owned();
    let mut child = Command::new("ffmpeg")
        .args([
            "-hide_banner",
            "-loglevel",
            "error",
            "-y",
            "-f",
            "pulse",
            "-i",
            source,
            "-ac",
            "2",
            "-ar",
            "48000",
            "-c:a",
            "pcm_s16le",
        ])
        .arg(&output_arg)
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .map_err(|err| format!("ffmpeg audio capture is unavailable: {err}"))?;

    std::thread::sleep(Duration::from_millis(150));
    if let Some(status) = child
        .try_wait()
        .map_err(|err| format!("could not check ffmpeg audio capture: {err}"))?
    {
        return Err(format!(
            "ffmpeg audio capture exited immediately for source {source:?} with {status}"
        ));
    }
    Ok(child)
}

fn start_eye_audio_recording(
    mode: EyeRecordAudioMode,
    frame_dir: &Path,
    stem: &str,
) -> Result<Option<EyeAudioRecording>, String> {
    if mode == EyeRecordAudioMode::NoAudio {
        return Ok(None);
    }

    let mut captures = Vec::new();
    for kind in mode.capture_kinds() {
        let source = resolve_audio_source(*kind)?;
        let path = frame_dir.join(format!("{}-{}.wav", stem, kind.file_slug()));
        let child = spawn_audio_capture(&source, &path).map_err(|err| {
            format!(
                "could not start {} recording from {source:?}: {err}",
                kind.display_name()
            )
        })?;
        captures.push(EyeAudioCapture { path, child });
    }
    Ok(Some(EyeAudioRecording { captures }))
}

fn stop_audio_child(child: &mut std::process::Child) {
    if let Some(stdin) = child.stdin.as_mut() {
        let _ = std::io::Write::write_all(stdin, b"q\n");
    }
    let deadline = Instant::now() + Duration::from_secs(2);
    loop {
        match child.try_wait() {
            Ok(Some(_)) => return,
            Ok(None) if Instant::now() < deadline => {
                std::thread::sleep(Duration::from_millis(50));
            }
            _ => break,
        }
    }
    let _ = child.kill();
    let _ = child.wait();
}

fn stop_audio_recording(recording: &mut Option<EyeAudioRecording>) {
    let Some(mut recording) = recording.take() else {
        return;
    };
    for capture in &mut recording.captures {
        stop_audio_child(&mut capture.child);
    }
}

fn best_effort_recording_profile(
    state: &LauncherState,
    preview: Option<&LauncherPreview>,
    monitor_id: usize,
) -> (u32, u32) {
    let choice = state
        .display_capture_size_choice(monitor_id)
        .unwrap_or_else(|| state.capture_size_choice(monitor_id));
    let mut fps = if choice.fps == 0 {
        DEFAULT_EYE_RECORD_FPS
    } else {
        choice.fps.max(1)
    };
    if let Some(snapshot) =
        preview.and_then(|feed| feed.snapshot_metrics(monitor_id, PreviewSurface::Inline))
        && snapshot.server_fps.is_finite()
        && snapshot.server_fps >= 1.0
    {
        fps = snapshot.server_fps.round().clamp(1.0, 120.0) as u32;
    }
    let bitrate_kbit = choice.max_bitrate_kbit.max(800);
    (fps, bitrate_kbit)
}

fn validated_audio_paths(
    audio_mode: EyeRecordAudioMode,
    audio_paths: &[PathBuf],
) -> Result<Vec<PathBuf>, String> {
    if audio_mode == EyeRecordAudioMode::NoAudio {
        return Ok(Vec::new());
    }
    let usable = audio_paths
        .iter()
        .filter(|path| {
            path.metadata()
                .map(|metadata| metadata.len() > 44)
                .unwrap_or(false)
        })
        .cloned()
        .collect::<Vec<_>>();
    let expected = audio_mode.capture_kinds().len();
    if usable.len() != expected {
        return Err(format!(
            "recording requested {}, but only {} usable audio track(s) were captured",
            audio_mode.status_label(),
            usable.len()
        ));
    }
    Ok(usable)
}
