use anyhow::{Context, Result};
use lesavka_common::lesavka::{
    CalibrationAction, CalibrationRequest, Empty, relay_client::RelayClient,
};
use tonic::{Request, transport::Channel};

use super::state::{CalibrationStatus, UpstreamSyncStatus};
use crate::relay_transport;

pub fn fetch_calibration(server_addr: &str) -> Result<CalibrationStatus> {
    with_runtime(async move {
        let mut client = connect(server_addr).await?;
        let reply = client
            .get_calibration(Request::new(Empty {}))
            .await
            .context("querying upstream A/V calibration")?
            .into_inner();
        Ok(CalibrationStatus::from_proto(reply))
    })
}

pub fn fetch_upstream_sync(server_addr: &str) -> Result<UpstreamSyncStatus> {
    with_runtime(async move {
        let mut client = connect(server_addr).await?;
        let reply = client
            .get_upstream_sync(Request::new(Empty {}))
            .await
            .context("querying upstream A/V planner state")?
            .into_inner();
        Ok(UpstreamSyncStatus::from_proto(reply))
    })
}

pub fn restore_default_calibration(server_addr: &str) -> Result<CalibrationStatus> {
    send_calibration_request(
        server_addr,
        CalibrationAction::RestoreDefault,
        0,
        0,
        0.0,
        0.0,
        "restore saved default upstream A/V calibration",
    )
}

pub fn restore_factory_calibration(server_addr: &str) -> Result<CalibrationStatus> {
    send_calibration_request(
        server_addr,
        CalibrationAction::RestoreFactory,
        0,
        0,
        0.0,
        0.0,
        "restore factory MJPEG upstream A/V calibration",
    )
}

pub fn nudge_audio_calibration(server_addr: &str, delta_us: i64) -> Result<CalibrationStatus> {
    send_calibration_request(
        server_addr,
        CalibrationAction::AdjustActive,
        delta_us,
        0,
        0.0,
        0.0,
        "manual upstream A/V calibration nudge",
    )
}

pub fn blind_calibration_estimate(
    server_addr: &str,
    audio_delta_us: i64,
    delivery_skew_ms: f32,
    enqueue_skew_ms: f32,
    note: &str,
) -> Result<CalibrationStatus> {
    send_calibration_request(
        server_addr,
        CalibrationAction::BlindEstimate,
        audio_delta_us,
        0,
        delivery_skew_ms,
        enqueue_skew_ms,
        note,
    )
}

fn send_calibration_request(
    server_addr: &str,
    action: CalibrationAction,
    audio_delta_us: i64,
    video_delta_us: i64,
    observed_delivery_skew_ms: f32,
    observed_enqueue_skew_ms: f32,
    note: &str,
) -> Result<CalibrationStatus> {
    with_runtime(async move {
        let mut client = connect(server_addr).await?;
        let reply = client
            .calibrate(Request::new(CalibrationRequest {
                action: action as i32,
                audio_delta_us,
                video_delta_us,
                observed_delivery_skew_ms,
                observed_enqueue_skew_ms,
                note: note.to_string(),
            }))
            .await
            .context("applying upstream A/V calibration")?
            .into_inner();
        Ok(CalibrationStatus::from_proto(reply))
    })
}

fn with_runtime<F, T>(future: F) -> Result<T>
where
    F: std::future::Future<Output = Result<T>>,
{
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .context("building launcher calibration runtime")?
        .block_on(future)
}

async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> {
    let channel = relay_transport::endpoint(server_addr)?
        .tcp_nodelay(true)
        .connect()
        .await
        .context("connecting launcher to relay host")?;
    Ok(RelayClient::new(channel))
}
