#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CalibrationStatus {
    pub available: bool,
    pub profile: String,
    pub factory_audio_offset_us: i64,
    pub factory_video_offset_us: i64,
    pub default_audio_offset_us: i64,
    pub default_video_offset_us: i64,
    pub active_audio_offset_us: i64,
    pub active_video_offset_us: i64,
    pub source: String,
    pub confidence: String,
    pub updated_at: String,
    pub detail: String,
}

/// Convert relay calibration payloads into visible launcher state.
impl CalibrationStatus {
    /// Convert the relay calibration RPC payload into launcher state.
    #[must_use]
    pub fn from_proto(reply: lesavka_common::lesavka::CalibrationState) -> Self {
        Self {
            available: true,
            profile: reply.profile,
            factory_audio_offset_us: reply.factory_audio_offset_us,
            factory_video_offset_us: reply.factory_video_offset_us,
            default_audio_offset_us: reply.default_audio_offset_us,
            default_video_offset_us: reply.default_video_offset_us,
            active_audio_offset_us: reply.active_audio_offset_us,
            active_video_offset_us: reply.active_video_offset_us,
            source: reply.source,
            confidence: reply.confidence,
            updated_at: reply.updated_at,
            detail: reply.detail,
        }
    }
    #[must_use]
    /// Build an unavailable calibration placeholder when the relay status RPC cannot answer.
    ///
    /// Inputs: human-readable failure detail for the launcher. Output: a
    /// disabled status that keeps factory/default fields predictable for the UI.
    pub fn unavailable(detail: impl Into<String>) -> Self {
        Self {
            detail: detail.into(),
            ..Self::default()
        }
    }
}

/// Provide factory MJPEG offsets until the relay reports saved calibration.
impl Default for CalibrationStatus {
    /// Start with the current lab-validated MJPEG baseline.
    fn default() -> Self {
        Self {
            available: false,
            profile: "mjpeg".to_string(),
            factory_audio_offset_us: 0,
            factory_video_offset_us: 0,
            default_audio_offset_us: 0,
            default_video_offset_us: 0,
            active_audio_offset_us: 0,
            active_video_offset_us: 0,
            source: "unknown".to_string(),
            confidence: "unknown".to_string(),
            updated_at: String::new(),
            detail: "calibration status unavailable".to_string(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UpstreamSyncStatus {
    pub available: bool,
    pub session_id: u64,
    pub phase: String,
    pub latest_camera_remote_pts_us: Option<u64>,
    pub latest_microphone_remote_pts_us: Option<u64>,
    pub last_video_presented_pts_us: Option<u64>,
    pub last_audio_presented_pts_us: Option<u64>,
    pub live_lag_ms: Option<f32>,
    pub planner_skew_ms: Option<f32>,
    pub stale_audio_drops: u64,
    pub stale_video_drops: u64,
    pub skew_video_drops: u64,
    pub freshness_reanchors: u64,
    pub startup_timeouts: u64,
    pub video_freezes: u64,
    pub uvc_contract_status: String,
    pub uvc_transport: String,
    pub uvc_enforced_frame_cap_bytes: Option<u32>,
    pub uvc_contract_divergent: bool,
    pub uvc_contract_detail: String,
    pub uvc_integrity_status: String,
    pub uvc_integrity_detail: String,
    pub detail: String,
}

impl UpstreamSyncStatus {
    #[must_use]
    /// Keeps `from_proto` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
    /// Inputs are the typed parameters; output is the return value or side effect.
    pub fn from_proto(reply: lesavka_common::lesavka::UpstreamSyncState) -> Self {
        Self {
            available: true,
            session_id: reply.session_id,
            phase: reply.phase,
            latest_camera_remote_pts_us: reply.latest_camera_remote_pts_us,
            latest_microphone_remote_pts_us: reply.latest_microphone_remote_pts_us,
            last_video_presented_pts_us: reply.last_video_presented_pts_us,
            last_audio_presented_pts_us: reply.last_audio_presented_pts_us,
            live_lag_ms: reply.live_lag_ms,
            planner_skew_ms: reply.planner_skew_ms,
            stale_audio_drops: reply.stale_audio_drops,
            stale_video_drops: reply.stale_video_drops,
            skew_video_drops: reply.skew_video_drops,
            freshness_reanchors: reply.freshness_reanchors,
            startup_timeouts: reply.startup_timeouts,
            video_freezes: reply.video_freezes,
            uvc_contract_status: reply.uvc_contract_status,
            uvc_transport: reply.uvc_transport,
            uvc_enforced_frame_cap_bytes: reply.uvc_enforced_frame_cap_bytes,
            uvc_contract_divergent: reply.uvc_contract_divergent,
            uvc_contract_detail: reply.uvc_contract_detail,
            uvc_integrity_status: reply.uvc_integrity_status,
            uvc_integrity_detail: reply.uvc_integrity_detail,
            detail: reply.last_reason,
        }
    }

    #[must_use]
    /// Build an unavailable upstream-sync placeholder when the relay planner cannot be queried.
    ///
    /// Inputs: human-readable failure detail for the launcher. Output: a
    /// disabled status that keeps stale/drop counters at their safe zero-state.
    pub fn unavailable(detail: impl Into<String>) -> Self {
        Self {
            detail: detail.into(),
            ..Self::default()
        }
    }
}

impl Default for UpstreamSyncStatus {
    /// Keeps `default` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn default() -> Self {
        Self {
            available: false,
            session_id: 0,
            phase: "unknown".to_string(),
            latest_camera_remote_pts_us: None,
            latest_microphone_remote_pts_us: None,
            last_video_presented_pts_us: None,
            last_audio_presented_pts_us: None,
            live_lag_ms: None,
            planner_skew_ms: None,
            stale_audio_drops: 0,
            stale_video_drops: 0,
            skew_video_drops: 0,
            freshness_reanchors: 0,
            startup_timeouts: 0,
            video_freezes: 0,
            uvc_contract_status: "unavailable".to_string(),
            uvc_transport: "unknown".to_string(),
            uvc_enforced_frame_cap_bytes: None,
            uvc_contract_divergent: true,
            uvc_contract_detail: "UVC payload contract unavailable".to_string(),
            uvc_integrity_status: "unavailable".to_string(),
            uvc_integrity_detail: "UVC helper integrity telemetry unavailable".to_string(),
            detail: "upstream sync planner unavailable".to_string(),
        }
    }
}

#[cfg(test)]
mod sync_status_tests {
    use super::{CalibrationStatus, UpstreamSyncStatus};
    use lesavka_common::lesavka::{CalibrationState, UpstreamSyncState};

    #[test]
    /// Confirm launcher calibration state remains useful when relay calibration is live or unavailable.
    ///
    /// Inputs: representative relay calibration payloads and failure details.
    /// Output: assertions that visible launcher fields preserve both active
    /// offsets and safe fallback text.
    fn calibration_status_maps_proto_and_unavailable_detail() {
        let status = CalibrationStatus::from_proto(CalibrationState {
            profile: "hevc/1920x1080@30".to_string(),
            factory_audio_offset_us: 1,
            factory_video_offset_us: 2,
            default_audio_offset_us: 3,
            default_video_offset_us: 4,
            active_audio_offset_us: 5,
            active_video_offset_us: 6,
            source: "factory".to_string(),
            confidence: "bench".to_string(),
            updated_at: "now".to_string(),
            detail: "ready".to_string(),
        });

        assert!(status.available);
        assert_eq!(status.profile, "hevc/1920x1080@30");
        assert_eq!(status.active_video_offset_us, 6);

        let unavailable = CalibrationStatus::unavailable("relay offline");
        assert!(!unavailable.available);
        assert_eq!(unavailable.detail, "relay offline");
        assert_eq!(CalibrationStatus::default().detail, "calibration status unavailable");
    }

    #[test]
    /// Confirm upstream sync status exposes planner health without hiding fallback states.
    ///
    /// Inputs: representative upstream planner payloads and failure details.
    /// Output: assertions for live counters, phase labels, and unavailable
    /// zero-state values used by the launcher.
    fn upstream_sync_status_maps_proto_and_unavailable_detail() {
        let status = UpstreamSyncStatus::from_proto(UpstreamSyncState {
            session_id: 42,
            phase: "live".to_string(),
            latest_camera_remote_pts_us: Some(1_000),
            latest_microphone_remote_pts_us: Some(1_010),
            last_video_presented_pts_us: Some(2_000),
            last_audio_presented_pts_us: Some(2_010),
            live_lag_ms: Some(12.5),
            planner_skew_ms: Some(1.0),
            stale_audio_drops: 1,
            stale_video_drops: 2,
            skew_video_drops: 3,
            freshness_reanchors: 4,
            startup_timeouts: 5,
            video_freezes: 6,
            last_reason: "healthy".to_string(),
            ..Default::default()
        });

        assert!(status.available);
        assert_eq!(status.session_id, 42);
        assert_eq!(status.phase, "live");
        assert_eq!(status.detail, "healthy");
        assert_eq!(status.video_freezes, 6);

        let unavailable = UpstreamSyncStatus::unavailable("sync rpc unavailable");
        assert!(!unavailable.available);
        assert_eq!(unavailable.detail, "sync rpc unavailable");
        assert_eq!(
            UpstreamSyncStatus::default().detail,
            "upstream sync planner unavailable"
        );
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct DeviceSelection {
    pub camera: Option<String>,
    pub microphone: Option<String>,
    pub speaker: Option<String>,
    pub keyboard: Option<String>,
    pub mouse: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelSelection {
    pub camera: bool,
    pub microphone: bool,
    pub audio: bool,
}

impl Default for ChannelSelection {
    fn default() -> Self {
        Self {
            camera: false,
            microphone: false,
            audio: true,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LauncherState {
    pub server_available: bool,
    pub server_version: Option<String>,
    pub server_camera: Option<bool>,
    pub server_microphone: Option<bool>,
    pub server_camera_output: Option<String>,
    pub server_camera_codec: Option<String>,
    pub routing: InputRouting,
    pub view_mode: ViewMode,
    pub displays: [DisplaySurface; 2],
    pub feed_sources: [FeedSourcePreset; 2],
    pub preview_source: PreviewSourceSize,
    pub breakout_limit: PreviewSourceSize,
    pub breakout_display: PreviewSourceSize,
    pub capture_sizes: [CaptureSizePreset; 2],
    pub capture_fps: [u32; 2],
    pub capture_bitrates_kbit: [u32; 2],
    pub breakout_sizes: [BreakoutSizePreset; 2],
    pub devices: DeviceSelection,
    pub camera_quality: Option<CameraMode>,
    pub webcam_transport: WebcamTransport,
    pub upstream_audio_transport: UpstreamAudioTransport,
    pub mic_noise_suppression: bool,
    pub channels: ChannelSelection,
    pub audio_gain_percent: u32,
    pub mic_gain_percent: u32,
    pub swap_key: String,
    pub swap_key_binding: bool,
    pub swap_key_binding_token: u64,
    pub capture_power: CapturePowerStatus,
    pub calibration: CalibrationStatus,
    pub upstream_sync: UpstreamSyncStatus,
    pub remote_active: bool,
    pub notes: Vec<String>,
}

impl Default for LauncherState {
    /// Keeps `default` explicit because it sits on launcher state/UI wiring, where device choices should remain explainable across refreshes.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn default() -> Self {
        Self {
            server_available: false,
            server_version: None,
            server_camera: None,
            server_microphone: None,
            server_camera_output: None,
            server_camera_codec: None,
            routing: InputRouting::Remote,
            view_mode: ViewMode::Unified,
            displays: [DisplaySurface::Preview, DisplaySurface::Preview],
            feed_sources: [FeedSourcePreset::ThisEye, FeedSourcePreset::ThisEye],
            preview_source: PreviewSourceSize::default(),
            breakout_limit: PreviewSourceSize::default(),
            breakout_display: PreviewSourceSize::default(),
            capture_sizes: [CaptureSizePreset::P1080, CaptureSizePreset::P1080],
            capture_fps: [60, 60],
            capture_bitrates_kbit: [18_000, 18_000],
            breakout_sizes: [BreakoutSizePreset::Source, BreakoutSizePreset::Source],
            devices: DeviceSelection::default(),
            camera_quality: None,
            webcam_transport: WebcamTransport::default(),
            upstream_audio_transport: UpstreamAudioTransport::default(),
            mic_noise_suppression: false,
            channels: ChannelSelection::default(),
            audio_gain_percent: DEFAULT_AUDIO_GAIN_PERCENT,
            mic_gain_percent: DEFAULT_MIC_GAIN_PERCENT,
            swap_key: "pause".to_string(),
            swap_key_binding: false,
            swap_key_binding_token: 0,
            capture_power: CapturePowerStatus::default(),
            calibration: CalibrationStatus::default(),
            upstream_sync: UpstreamSyncStatus::default(),
            remote_active: false,
            notes: Vec::new(),
        }
    }
}
