#[derive(Default)]
struct UvcIntegritySnapshot {
    status: String,
    detail: String,
    rejected_oversize: u64,
    rejected_invalid: u64,
    fallback_idle: u64,
    held_last_good: u64,
    strict_validation_failures: u64,
    dqbuf_errors: u64,
    qbuf_errors: u64,
    kernel_errors: u64,
    stale_replay: u64,
    read_errors: u64,
    generated_unix_ms: Option<u64>,
}

fn read_uvc_integrity_snapshot() -> UvcIntegritySnapshot {
    let path = std::env::var("LESAVKA_UVC_STATS_PATH")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| std::path::PathBuf::from("/run/lesavka-uvc-video-stats.json"));
    match std::fs::read(&path)
        .map_err(anyhow::Error::from)
        .and_then(|bytes| serde_json::from_slice::<serde_json::Value>(&bytes).map_err(Into::into))
    {
        Ok(value) => parse_uvc_integrity_snapshot(&value),
        Err(err) => {
            UvcIntegritySnapshot {
                status: "unavailable".to_string(),
                detail: format!("{}: {err}", path.display()),
                ..Default::default()
            }
        }
    }
}

fn parse_uvc_integrity_snapshot(value: &serde_json::Value) -> UvcIntegritySnapshot {
    let number = |name: &str| value.get(name).and_then(serde_json::Value::as_u64).unwrap_or(0);
    let kernel_number = |name: &str| {
        value
            .pointer(&format!("/kernel/counters/{name}"))
            .and_then(serde_json::Value::as_u64)
            .unwrap_or(0)
    };
    let rejected_oversize = number("rejected_oversize");
    let rejected_invalid = number("rejected_invalid");
    let fallback_idle = number("fallback_idle");
    let held_last_good = number("held_last_good");
    let strict_validation_failures = number("strict_validation_failures");
    let dqbuf_errors = number("dqbuf_ioctl_errors").saturating_add(number("dqbuf_flag_errors"));
    let qbuf_errors = number("qbuf_ioctl_errors");
    let stale_replay = number("stale_replay");
    let read_errors = number("read_errors");
    let kernel_errors = [
        "dwc2_errors",
        "uvc_errors",
        "udc_errors",
        "urb_errors",
        "usb_resets",
    ]
    .into_iter()
    .map(kernel_number)
    .sum();
    let hard_faults = rejected_oversize
        .saturating_add(rejected_invalid)
        .saturating_add(fallback_idle)
        .saturating_add(strict_validation_failures)
        .saturating_add(dqbuf_errors)
        .saturating_add(qbuf_errors)
        .saturating_add(kernel_errors)
        .saturating_add(read_errors);
    let status = if hard_faults > 0 {
        "fault"
    } else if held_last_good > 0 || stale_replay > 0 {
        "holding"
    } else {
        "ready"
    };
    UvcIntegritySnapshot {
        status: status.to_string(),
        detail: format!(
            "oversize={rejected_oversize} invalid={rejected_invalid} idle={fallback_idle} held={held_last_good} stale={stale_replay} strict={strict_validation_failures} dq={dqbuf_errors} q={qbuf_errors} read={read_errors} kernel={kernel_errors}"
        ),
        rejected_oversize,
        rejected_invalid,
        fallback_idle,
        held_last_good,
        strict_validation_failures,
        dqbuf_errors,
        qbuf_errors,
        kernel_errors,
        stale_replay,
        read_errors,
        generated_unix_ms: value
            .get("generated_unix_ms")
            .and_then(serde_json::Value::as_u64),
    }
}

#[cfg(test)]
mod uvc_integrity_snapshot_tests {
    use super::parse_uvc_integrity_snapshot;

    #[test]
    fn helper_and_kernel_faults_drive_integrity_status() {
        let ready = parse_uvc_integrity_snapshot(&serde_json::json!({
            "generated_unix_ms": 123,
            "held_last_good": 0,
            "kernel": {"counters": {"dwc2_errors": 0}}
        }));
        assert_eq!(ready.status, "ready");
        assert_eq!(ready.generated_unix_ms, Some(123));

        let holding = parse_uvc_integrity_snapshot(&serde_json::json!({
            "held_last_good": 2,
            "stale_replay": 1
        }));
        assert_eq!(holding.status, "holding");

        let fault = parse_uvc_integrity_snapshot(&serde_json::json!({
            "rejected_oversize": 1,
            "dqbuf_flag_errors": 2,
            "kernel": {"counters": {"dwc2_errors": 3, "usb_resets": 4}}
        }));
        assert_eq!(fault.status, "fault");
        assert_eq!(fault.dqbuf_errors, 2);
        assert_eq!(fault.kernel_errors, 7);
        assert!(fault.detail.contains("oversize=1"));
    }
}

impl Handler {
    /// Reopen HID handles and verify enumeration without cycling the USB gadget.
    async fn recover_usb_reply(&self) -> Result<Response<ResetUsbReply>, Status> {
        #[cfg(not(coverage))]
        info!("🛟 RecoverUsb() soft recovery called");

        self.reopen_hid()
            .await
            .map_err(|e| Status::internal(format!("soft USB HID reopen failed: {e:#}")))?;

        match current_controller_state_after_recovery() {
            Ok((ctrl, state)) if UsbGadget::host_enumerated_state(&state) => {
                #[cfg(not(coverage))]
                info!("✅ soft USB recovery verified host enumeration ctrl={ctrl} state={state}");
                Ok(Response::new(ResetUsbReply { ok: true }))
            }
            Ok((ctrl, state)) => Err(Status::failed_precondition(format!(
                "soft USB recovery refused hard reset: UDC {ctrl} is {state}; use explicit hard ResetUsb only after closing remote UVC/audio consumers"
            ))),
            Err(err) => Err(Status::failed_precondition(format!(
                "soft USB recovery could not read UDC state: {err:#}"
            ))),
        }
    }

    async fn recover_uac_reply(&self) -> Result<Response<ResetUsbReply>, Status> {
        #[cfg(not(coverage))]
        info!("🛟 RecoverUac() soft recovery called");

        self.upstream_media_rt.soft_recover_microphone();
        Ok(Response::new(ResetUsbReply { ok: true }))
    }

    /// Retire the current webcam relay sink without detaching the UVC gadget.
    async fn recover_uvc_reply(&self) -> Result<Response<ResetUsbReply>, Status> {
        #[cfg(not(coverage))]
        info!("🛟 RecoverUvc() soft recovery called");

        let cfg = camera::current_camera_config();
        if cfg.output != camera::CameraOutput::Uvc {
            return Err(Status::failed_precondition(format!(
                "soft UVC recovery skipped because active camera output is {}",
                cfg.output.as_str()
            )));
        }

        self.camera_rt.soft_recover().await;
        Ok(Response::new(ResetUsbReply { ok: true }))
    }

    /// Keeps `paste_text_reply` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
    /// Inputs are the typed parameters; output is the return value or side effect.
    async fn paste_text_reply(
        &self,
        req: Request<PasteRequest>,
    ) -> Result<Response<PasteReply>, Status> {
        let req = req.into_inner();
        let text = paste::decrypt(&req).map_err(|e| Status::unauthenticated(format!("{e}")))?;
        if let Err(e) = paste::type_text(&self.kb, &hid_endpoint(0), &text).await {
            return Ok(Response::new(PasteReply {
                ok: false,
                error: format!("{e}"),
            }));
        }
        Ok(Response::new(PasteReply {
            ok: true,
            error: String::new(),
        }))
    }

    /// Keeps `reset_usb_reply` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
    /// Inputs are the typed parameters; output is the return value or side effect.
    async fn reset_usb_reply(&self) -> Result<Response<ResetUsbReply>, Status> {
        #[cfg(not(coverage))]
        info!("🔴 explicit ResetUsb() called");

        match self.gadget.recover_enumeration() {
            Ok(_) => {
                if let Err(e) = self.reopen_hid().await {
                    #[cfg(not(coverage))]
                    error!("💥 reopen HID failed: {e:#}");
                    return Err(Status::internal(e.to_string()));
                }
                if let Err(e) = restart_uvc_helper() {
                    #[cfg(not(coverage))]
                    error!("💥 restart UVC helper failed: {e:#}");
                    return Err(Status::internal(e.to_string()));
                }
                match current_controller_state_after_recovery() {
                    Ok((ctrl, state)) if UsbGadget::host_enumerated_state(&state) => {
                        #[cfg(not(coverage))]
                        info!(
                            "✅ USB host enumerated gadget after recovery ctrl={ctrl} state={state}"
                        );
                    }
                    Ok((ctrl, state)) => {
                        let message = format!(
                            "USB gadget recovery ran, but UDC {ctrl} is still {state}; the controlled host has not enumerated the relay HID/audio/video gadget"
                        );
                        #[cfg(not(coverage))]
                        warn!("⚠️ {message}");
                        return Err(Status::failed_precondition(message));
                    }
                    Err(err) => {
                        let message = format!(
                            "USB gadget recovery ran, but the relay cannot read UDC state: {err:#}"
                        );
                        #[cfg(not(coverage))]
                        warn!("⚠️ {message}");
                        return Err(Status::failed_precondition(message));
                    }
                }
                Ok(Response::new(ResetUsbReply { ok: true }))
            }
            Err(e) => {
                #[cfg(not(coverage))]
                error!("💥 USB recovery failed: {e:#}");
                let message = format!("{e:#}");
                if message.contains("still not attached") || message.contains("not attached") {
                    Err(Status::failed_precondition(message))
                } else {
                    Err(Status::internal(message))
                }
            }
        }
    }

    async fn get_capture_power_reply(&self) -> Result<Response<CapturePowerState>, Status> {
        let state = self
            .capture_power
            .snapshot()
            .await
            .map_err(|e| Status::internal(format!("{e:#}")))?;
        Ok(Response::new(
            self.with_detected_capture_devices(state).await,
        ))
    }

    /// Keeps `set_capture_power_reply` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
    /// Inputs are the typed parameters; output is the return value or side effect.
    async fn set_capture_power_reply(
        &self,
        req: Request<SetCapturePowerRequest>,
    ) -> Result<Response<CapturePowerState>, Status> {
        let req = req.into_inner();
        let result = match CapturePowerCommand::try_from(req.command)
            .unwrap_or(CapturePowerCommand::Unspecified)
        {
            CapturePowerCommand::Auto => self.capture_power.set_auto().await,
            CapturePowerCommand::ForceOn => self.capture_power.set_manual(true).await,
            CapturePowerCommand::ForceOff => self.capture_power.set_manual(false).await,
            CapturePowerCommand::Unspecified => self.capture_power.set_manual(req.enabled).await,
        };
        let state = result.map_err(|e| Status::internal(format!("{e:#}")))?;
        Ok(Response::new(
            self.with_detected_capture_devices(state).await,
        ))
    }

    async fn get_calibration_reply(&self) -> Result<Response<CalibrationState>, Status> {
        Ok(Response::new(self.calibration.current()))
    }

    async fn calibrate_reply(
        &self,
        req: Request<CalibrationRequest>,
    ) -> Result<Response<CalibrationState>, Status> {
        self.calibration
            .apply(req.into_inner())
            .map(Response::new)
            .map_err(|e| Status::internal(format!("{e:#}")))
    }

    /// Keeps `get_upstream_sync_reply` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
    /// Inputs are the typed parameters; output is the return value or side effect.
    async fn get_upstream_sync_reply(&self) -> Result<Response<UpstreamSyncState>, Status> {
        let snapshot = self.upstream_media_rt.snapshot();
        let contract_path = uvc_contract::contract_path();
        let contract = uvc_contract::read_contract(&contract_path);
        let (
            uvc_contract_status,
            uvc_transport,
            uvc_enforced_frame_cap_bytes,
            uvc_advertised_frame_bytes,
            uvc_committed_frame_bytes,
            uvc_committed_payload_bytes,
            uvc_contract_divergent,
            uvc_contract_detail,
            uvc_contract_generated_unix_ms,
        ) = match contract {
            Ok(contract) => (
                if !contract.configfs_available {
                    "missing_configfs"
                } else if !contract.commit_observed {
                    "unverified"
                } else if contract.divergent {
                    "divergent"
                } else {
                    "ready"
                }
                .to_string(),
                contract.transport,
                Some(contract.enforced_frame_cap_bytes),
                Some(contract.advertised_frame_bytes),
                Some(contract.committed_frame_bytes),
                Some(contract.committed_payload_bytes),
                contract.divergent,
                contract.detail,
                Some(contract.generated_unix_ms),
            ),
            Err(err) => (
                "missing_or_invalid".to_string(),
                "unknown".to_string(),
                None,
                None,
                None,
                None,
                true,
                format!("{}: {err}", contract_path.display()),
                None,
            ),
        };
        let uvc_contract_read_stats = uvc_contract::contract_read_stats();
        let uvc_integrity = read_uvc_integrity_snapshot();
        Ok(Response::new(UpstreamSyncState {
            session_id: snapshot.session_id,
            phase: snapshot.phase.to_string(),
            latest_camera_remote_pts_us: snapshot.latest_camera_remote_pts_us,
            latest_microphone_remote_pts_us: snapshot.latest_microphone_remote_pts_us,
            last_video_presented_pts_us: snapshot.last_video_presented_pts_us,
            last_audio_presented_pts_us: snapshot.last_audio_presented_pts_us,
            live_lag_ms: snapshot.live_lag_ms.map(|value| value as f32),
            planner_skew_ms: snapshot.planner_skew_ms.map(|value| value as f32),
            stale_audio_drops: snapshot.stale_audio_drops,
            stale_video_drops: snapshot.stale_video_drops,
            skew_video_drops: snapshot.skew_video_drops,
            freshness_reanchors: snapshot.freshness_reanchors,
            startup_timeouts: snapshot.startup_timeouts,
            video_freezes: snapshot.video_freezes,
            last_reason: snapshot.last_reason,
            client_capture_skew_ms: snapshot.client_capture_skew_ms.map(|value| value as f32),
            client_send_skew_ms: snapshot.client_send_skew_ms.map(|value| value as f32),
            server_receive_skew_ms: snapshot.server_receive_skew_ms.map(|value| value as f32),
            camera_client_queue_age_ms: snapshot
                .camera_client_queue_age_ms
                .map(|value| value as f32),
            microphone_client_queue_age_ms: snapshot
                .microphone_client_queue_age_ms
                .map(|value| value as f32),
            camera_server_receive_age_ms: snapshot
                .camera_server_receive_age_ms
                .map(|value| value as f32),
            microphone_server_receive_age_ms: snapshot
                .microphone_server_receive_age_ms
                .map(|value| value as f32),
            client_capture_abs_skew_p95_ms: snapshot
                .client_capture_abs_skew_p95_ms
                .map(|value| value as f32),
            client_send_abs_skew_p95_ms: snapshot
                .client_send_abs_skew_p95_ms
                .map(|value| value as f32),
            server_receive_abs_skew_p95_ms: snapshot
                .server_receive_abs_skew_p95_ms
                .map(|value| value as f32),
            camera_client_queue_age_p95_ms: snapshot
                .camera_client_queue_age_p95_ms
                .map(|value| value as f32),
            microphone_client_queue_age_p95_ms: snapshot
                .microphone_client_queue_age_p95_ms
                .map(|value| value as f32),
            sink_handoff_skew_ms: snapshot.sink_handoff_skew_ms.map(|value| value as f32),
            sink_handoff_abs_skew_p95_ms: snapshot
                .sink_handoff_abs_skew_p95_ms
                .map(|value| value as f32),
            camera_sink_late_ms: snapshot.camera_sink_late_ms.map(|value| value as f32),
            microphone_sink_late_ms: snapshot
                .microphone_sink_late_ms
                .map(|value| value as f32),
            camera_sink_late_p95_ms: snapshot
                .camera_sink_late_p95_ms
                .map(|value| value as f32),
            microphone_sink_late_p95_ms: snapshot
                .microphone_sink_late_p95_ms
                .map(|value| value as f32),
            client_timing_window_samples: snapshot.client_timing_window_samples,
            sink_handoff_window_samples: snapshot.sink_handoff_window_samples,
            uvc_contract_status,
            uvc_transport,
            uvc_enforced_frame_cap_bytes,
            uvc_advertised_frame_bytes,
            uvc_committed_frame_bytes,
            uvc_committed_payload_bytes,
            uvc_contract_divergent,
            uvc_contract_detail,
            uvc_contract_generated_unix_ms,
            uvc_contract_missing_reads: uvc_contract_read_stats.missing_or_invalid,
            uvc_contract_divergent_reads: uvc_contract_read_stats.divergent,
            uvc_integrity_status: uvc_integrity.status,
            uvc_integrity_detail: uvc_integrity.detail,
            uvc_rejected_oversize: uvc_integrity.rejected_oversize,
            uvc_rejected_invalid: uvc_integrity.rejected_invalid,
            uvc_fallback_idle: uvc_integrity.fallback_idle,
            uvc_held_last_good: uvc_integrity.held_last_good,
            uvc_strict_validation_failures: uvc_integrity.strict_validation_failures,
            uvc_dqbuf_errors: uvc_integrity.dqbuf_errors,
            uvc_qbuf_errors: uvc_integrity.qbuf_errors,
            uvc_kernel_errors: uvc_integrity.kernel_errors,
            uvc_integrity_generated_unix_ms: uvc_integrity.generated_unix_ms,
            uvc_stale_replay: uvc_integrity.stale_replay,
            uvc_read_errors: uvc_integrity.read_errors,
        }))
    }
}
