//! Explicit request / handler / video-processing wrappers backed by Vision.
//!
//! This module exposes the generic Vision base classes that the rest of the
//! crate used internally until v0.15.1: [`Request`] (`VNRequest`),
//! [`Observation`] (`VNObservation`), [`ImageRequestHandler`]
//! (`VNImageRequestHandler`), [`SequenceRequestHandler`]
//! (`VNSequenceRequestHandler`), and [`VideoProcessor`] (`VNVideoProcessor`).
//! The initial safe surface focuses on text recognition, which is already part
//! of the crate's default feature set.

use core::{
    ffi::{c_char, c_void},
    ptr,
};
use std::{
    ffi::{CStr, CString},
    path::{Path, PathBuf},
};

use crate::{
    error::{from_swift, VisionError},
    ffi,
    recognize_text::{BoundingBox, RecognitionLevel, RecognizedText, RecognizedTextCandidate},
    request_base::RequestRevisionProviding,
    sdk::{ComputeStage, ImageOption},
};

const VIDEO_CADENCE_DEFAULT: i32 = 0;
const VIDEO_CADENCE_FRAME_RATE: i32 = 1;
const VIDEO_CADENCE_TIME_INTERVAL: i32 = 2;

/// The high-level Vision request kind carried by [`Request`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RequestKind {
    /// `VNRecognizeTextRequest`
    RecognizeText,
}

/// Shared `VNRequest` configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
    kind: RequestKind,
    recognition_level: RecognitionLevel,
    uses_language_correction: bool,
    prefer_background_processing: bool,
    uses_cpu_only: bool,
    revision: Option<usize>,
}

impl Default for Request {
    fn default() -> Self {
        Self::recognize_text()
    }
}

impl RequestRevisionProviding for Request {
    fn request_revision(&self) -> Option<usize> {
        self.revision()
    }
}

impl Request {
    /// Build a text-recognition request backed by `VNRecognizeTextRequest`.
    #[must_use]
    pub const fn recognize_text() -> Self {
        Self {
            kind: RequestKind::RecognizeText,
            recognition_level: RecognitionLevel::Accurate,
            uses_language_correction: true,
            prefer_background_processing: false,
            uses_cpu_only: false,
            revision: None,
        }
    }

    /// Return the underlying request kind.
    #[must_use]
    pub const fn kind(&self) -> RequestKind {
        self.kind
    }

    /// Select the OCR recognition strategy.
    #[must_use]
    pub const fn with_recognition_level(mut self, recognition_level: RecognitionLevel) -> Self {
        self.recognition_level = recognition_level;
        self
    }

    /// Enable or disable language correction.
    #[must_use]
    pub const fn with_language_correction(mut self, enabled: bool) -> Self {
        self.uses_language_correction = enabled;
        self
    }

    /// Mirror `VNRequest.preferBackgroundProcessing`.
    #[must_use]
    pub const fn with_prefer_background_processing(mut self, enabled: bool) -> Self {
        self.prefer_background_processing = enabled;
        self
    }

    /// Mirror `VNRequest.usesCPUOnly`.
    #[must_use]
    pub const fn with_uses_cpu_only(mut self, enabled: bool) -> Self {
        self.uses_cpu_only = enabled;
        self
    }

    /// Override the request revision.
    #[must_use]
    pub const fn with_revision(mut self, revision: usize) -> Self {
        self.revision = Some(revision);
        self
    }

    #[must_use]
    pub const fn recognition_level(&self) -> RecognitionLevel {
        self.recognition_level
    }

    #[must_use]
    pub const fn uses_language_correction(&self) -> bool {
        self.uses_language_correction
    }

    #[must_use]
    pub const fn prefer_background_processing(&self) -> bool {
        self.prefer_background_processing
    }

    #[must_use]
    pub const fn uses_cpu_only(&self) -> bool {
        self.uses_cpu_only
    }

    #[must_use]
    pub const fn revision(&self) -> Option<usize> {
        self.revision
    }

    /// The compute-stage keys exposed by the current Vision SDK.
    #[must_use]
    pub const fn supported_compute_stages() -> &'static [ComputeStage] {
        ComputeStage::ALL
    }

    const fn recognition_level_raw(&self) -> i32 {
        match self.recognition_level {
            RecognitionLevel::Fast => 0,
            RecognitionLevel::Accurate => 1,
        }
    }
}

/// Shared `VNObservation` metadata surfaced by Vision results.
#[derive(Debug, Clone, PartialEq)]
pub struct Observation {
    /// Stable UUID generated by Vision for this observation.
    pub uuid: String,
    /// Base confidence score in `0.0..=1.0`.
    pub confidence: f32,
    /// Optional media time range in seconds.
    pub time_range: Option<TimeRange>,
}

/// Media time range in seconds.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TimeRange {
    pub start_seconds: f64,
    pub duration_seconds: f64,
}

/// One recognized-text observation plus shared [`Observation`] metadata.
#[derive(Debug, Clone, PartialEq)]
pub struct RecognizedTextObservation {
    pub observation: Observation,
    pub text: String,
    pub bounding_box: BoundingBox,
}

impl RecognizedTextObservation {
    /// Drop the generic observation metadata and keep the existing text result
    /// shape used elsewhere in the crate.
    #[must_use]
    pub fn into_recognized_text(self) -> RecognizedText {
        self.into()
    }

    /// Clone into the existing [`RecognizedText`] shape.
    #[must_use]
    pub fn as_recognized_text(&self) -> RecognizedText {
        self.clone().into()
    }

    /// Clone into the dedicated `VNRecognizedText` wrapper.
    #[must_use]
    pub fn candidate(&self) -> RecognizedTextCandidate {
        self.as_recognized_text().into()
    }
}

impl From<RecognizedTextObservation> for RecognizedText {
    fn from(value: RecognizedTextObservation) -> Self {
        Self {
            text: value.text,
            confidence: value.observation.confidence,
            bounding_box: value.bounding_box,
        }
    }
}

/// Safe wrapper around `VNImageRequestHandler`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageRequestHandler {
    image_path: PathBuf,
}

impl ImageRequestHandler {
    /// Bind the handler to an image path.
    #[must_use]
    pub fn new(image_path: impl AsRef<Path>) -> Self {
        Self {
            image_path: image_path.as_ref().to_path_buf(),
        }
    }

    /// Image-option keys accepted by `VNImageRequestHandler`.
    #[must_use]
    pub const fn supported_image_options() -> &'static [ImageOption] {
        ImageOption::ALL
    }

    /// Perform `request` against the bound image.
    ///
    /// # Errors
    ///
    /// Returns [`VisionError`] if the path is invalid, the image cannot be
    /// loaded, or Vision rejects the request.
    pub fn perform(
        &self,
        request: &Request,
    ) -> Result<Vec<RecognizedTextObservation>, VisionError> {
        let image_c = path_to_cstring(&self.image_path, "image path")?;
        let mut out_array: *mut c_void = ptr::null_mut();
        let mut out_count: usize = 0;
        let mut err_msg: *mut c_char = ptr::null_mut();
        // SAFETY: all pointer arguments are valid stack locations or bridge-owned handles; strings are valid C strings for the duration of the call.
        let status = unsafe {
            ffi::vn_image_request_handler_perform_text_request(
                image_c.as_ptr(),
                request.recognition_level_raw(),
                request.uses_language_correction,
                request.prefer_background_processing,
                request.uses_cpu_only,
                request.revision.unwrap_or_default(),
                request.revision.is_some(),
                &mut out_array,
                &mut out_count,
                &mut err_msg,
            )
        };
        if status != ffi::status::OK {
            // SAFETY: the error pointer is either null or a bridge-allocated C string; `from_swift` frees it.
            return Err(unsafe { from_swift(status, err_msg) });
        }
        Ok(collect_request_observations(out_array, out_count))
    }
}

/// Safe wrapper around a retained `VNSequenceRequestHandler`.
pub struct SequenceRequestHandler {
    handle: *mut c_void,
}

impl SequenceRequestHandler {
    /// Create a fresh sequence handler.
    ///
    /// # Errors
    ///
    /// Returns [`VisionError`] if the Swift bridge fails to allocate the
    /// backing handler.
    pub fn new() -> Result<Self, VisionError> {
        let mut handle: *mut c_void = ptr::null_mut();
        let mut err_msg: *mut c_char = ptr::null_mut();
        // SAFETY: all pointer arguments are valid stack locations or bridge-owned handles; strings are valid C strings for the duration of the call.
        let status = unsafe { ffi::vn_sequence_request_handler_create(&mut handle, &mut err_msg) };
        if status != ffi::status::OK {
            // SAFETY: the error pointer is either null or a bridge-allocated C string; `from_swift` frees it.
            return Err(unsafe { from_swift(status, err_msg) });
        }
        if handle.is_null() {
            return Err(VisionError::Unknown {
                code: ffi::status::UNKNOWN,
                message: "sequence request handler bridge returned a null handle".into(),
            });
        }
        Ok(Self { handle })
    }

    /// Perform `request` on `image_path`, preserving Vision's sequence state
    /// across calls.
    ///
    /// # Errors
    ///
    /// Returns [`VisionError`] if the path is invalid, the image cannot be
    /// loaded, or Vision rejects the request.
    pub fn perform(
        &mut self,
        image_path: impl AsRef<Path>,
        request: &Request,
    ) -> Result<Vec<RecognizedTextObservation>, VisionError> {
        let image_c = path_to_cstring(image_path.as_ref(), "image path")?;
        let mut out_array: *mut c_void = ptr::null_mut();
        let mut out_count: usize = 0;
        let mut err_msg: *mut c_char = ptr::null_mut();
        // SAFETY: all pointer arguments are valid stack locations or bridge-owned handles; strings are valid C strings for the duration of the call.
        let status = unsafe {
            ffi::vn_sequence_request_handler_perform_text_request(
                self.handle,
                image_c.as_ptr(),
                request.recognition_level_raw(),
                request.uses_language_correction,
                request.prefer_background_processing,
                request.uses_cpu_only,
                request.revision.unwrap_or_default(),
                request.revision.is_some(),
                &mut out_array,
                &mut out_count,
                &mut err_msg,
            )
        };
        if status != ffi::status::OK {
            // SAFETY: the error pointer is either null or a bridge-allocated C string; `from_swift` frees it.
            return Err(unsafe { from_swift(status, err_msg) });
        }
        Ok(collect_request_observations(out_array, out_count))
    }
}

impl Drop for SequenceRequestHandler {
    fn drop(&mut self) {
        if !self.handle.is_null() {
            // SAFETY: `self.handle` is a live bridge-owned handle and this drop path releases it exactly once.
            unsafe { ffi::vn_sequence_request_handler_free(self.handle) };
        }
    }
}

/// Cadence override for [`VideoProcessor`].
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum VideoCadence {
    /// Let Vision process every frame.
    EveryFrame,
    /// Sample the video at the given frame rate.
    FrameRate(usize),
    /// Sample the video at the given time interval, in seconds.
    TimeIntervalSeconds(f64),
}

/// Base `VNVideoProcessorCadence` wrapper.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VideoProcessorCadence {
    pub cadence: VideoCadence,
}

impl VideoProcessorCadence {
    #[must_use]
    pub const fn every_frame() -> Self {
        Self {
            cadence: VideoCadence::EveryFrame,
        }
    }

    #[must_use]
    pub const fn frame_rate(frames_per_second: usize) -> Self {
        Self {
            cadence: VideoCadence::FrameRate(frames_per_second),
        }
    }

    #[must_use]
    pub const fn time_interval_seconds(seconds: f64) -> Self {
        Self {
            cadence: VideoCadence::TimeIntervalSeconds(seconds),
        }
    }
}

/// Dedicated `VNVideoProcessorFrameRateCadence` wrapper.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VideoProcessorFrameRateCadence {
    pub frames_per_second: usize,
}

impl VideoProcessorFrameRateCadence {
    #[must_use]
    pub const fn as_video_processor_cadence(self) -> VideoProcessorCadence {
        VideoProcessorCadence::frame_rate(self.frames_per_second)
    }
}

/// Dedicated `VNVideoProcessorTimeIntervalCadence` wrapper.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VideoProcessorTimeIntervalCadence {
    pub seconds: f64,
}

impl VideoProcessorTimeIntervalCadence {
    #[must_use]
    pub const fn as_video_processor_cadence(self) -> VideoProcessorCadence {
        VideoProcessorCadence::time_interval_seconds(self.seconds)
    }
}

/// `VNVideoProcessor` request options.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VideoProcessingOptions {
    pub cadence: Option<VideoCadence>,
}

/// Dedicated `VNVideoProcessorRequestProcessingOptions` wrapper.
pub type VideoProcessorRequestProcessingOptions = VideoProcessingOptions;

impl From<VideoProcessorCadence> for VideoCadence {
    fn from(value: VideoProcessorCadence) -> Self {
        value.cadence
    }
}

impl Default for VideoProcessingOptions {
    fn default() -> Self {
        Self::new()
    }
}

impl VideoProcessingOptions {
    #[must_use]
    pub const fn new() -> Self {
        Self { cadence: None }
    }

    #[must_use]
    pub const fn with_cadence(mut self, cadence: VideoCadence) -> Self {
        self.cadence = Some(cadence);
        self
    }

    #[must_use]
    pub const fn with_video_processor_cadence(mut self, cadence: VideoProcessorCadence) -> Self {
        self.cadence = Some(cadence.cadence);
        self
    }
}

/// Safe wrapper around `VNVideoProcessor`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VideoProcessor {
    video_path: PathBuf,
}

impl VideoProcessor {
    /// Bind the processor to a video file.
    #[must_use]
    pub fn new(video_path: impl AsRef<Path>) -> Self {
        Self {
            video_path: video_path.as_ref().to_path_buf(),
        }
    }

    /// Analyze the bound video with `request`.
    ///
    /// # Errors
    ///
    /// Returns [`VisionError`] if the path is invalid, the video cannot be
    /// opened, the cadence is invalid, or Vision rejects the request.
    pub fn analyze(
        &self,
        request: &Request,
        options: VideoProcessingOptions,
    ) -> Result<Vec<RecognizedTextObservation>, VisionError> {
        let video_c = path_to_cstring(&self.video_path, "video path")?;
        let (cadence_kind, cadence_value) = cadence_to_ffi(options.cadence)?;
        let mut out_array: *mut c_void = ptr::null_mut();
        let mut out_count: usize = 0;
        let mut err_msg: *mut c_char = ptr::null_mut();
        // SAFETY: all pointer arguments are valid stack locations or bridge-owned handles; strings are valid C strings for the duration of the call.
        let status = unsafe {
            ffi::vn_video_processor_analyze_text_request(
                video_c.as_ptr(),
                request.recognition_level_raw(),
                request.uses_language_correction,
                request.prefer_background_processing,
                request.uses_cpu_only,
                request.revision.unwrap_or_default(),
                request.revision.is_some(),
                cadence_kind,
                cadence_value,
                &mut out_array,
                &mut out_count,
                &mut err_msg,
            )
        };
        if status != ffi::status::OK {
            // SAFETY: the error pointer is either null or a bridge-allocated C string; `from_swift` frees it.
            return Err(unsafe { from_swift(status, err_msg) });
        }
        Ok(collect_request_observations(out_array, out_count))
    }
}

fn cadence_to_ffi(cadence: Option<VideoCadence>) -> Result<(i32, f64), VisionError> {
    match cadence.unwrap_or(VideoCadence::EveryFrame) {
        VideoCadence::EveryFrame => Ok((VIDEO_CADENCE_DEFAULT, 0.0)),
        VideoCadence::FrameRate(frame_rate) => {
            if frame_rate == 0 {
                return Err(VisionError::InvalidArgument(
                    "video cadence frame rate must be greater than zero".into(),
                ));
            }
            let frame_rate = u32::try_from(frame_rate).map_err(|_| {
                VisionError::InvalidArgument(
                    "video cadence frame rate exceeds the supported range".into(),
                )
            })?;
            Ok((VIDEO_CADENCE_FRAME_RATE, f64::from(frame_rate)))
        }
        VideoCadence::TimeIntervalSeconds(seconds) => {
            if !seconds.is_finite() || seconds <= 0.0 {
                return Err(VisionError::InvalidArgument(
                    "video cadence time interval must be a finite positive number".into(),
                ));
            }
            Ok((VIDEO_CADENCE_TIME_INTERVAL, seconds))
        }
    }
}

fn collect_request_observations(
    array: *mut c_void,
    count: usize,
) -> Vec<RecognizedTextObservation> {
    if array.is_null() || count == 0 {
        return Vec::new();
    }

    let typed = array.cast::<ffi::RequestObservationRaw>();
    let mut observations = Vec::with_capacity(count);
    for index in 0..count {
        // SAFETY: the pointer is valid for the reported element count; the index is in bounds.
        let raw = unsafe { &*typed.add(index) };
        let uuid = c_string_or_empty(raw.uuid);
        let text = c_string_or_empty(raw.text);
        let time_range = raw.has_time_range.then_some(TimeRange {
            start_seconds: raw.time_range_start_seconds,
            duration_seconds: raw.time_range_duration_seconds,
        });
        observations.push(RecognizedTextObservation {
            observation: Observation {
                uuid,
                confidence: raw.confidence,
                time_range,
            },
            text,
            bounding_box: BoundingBox {
                x: raw.bbox_x,
                y: raw.bbox_y,
                width: raw.bbox_w,
                height: raw.bbox_h,
            },
        });
    }

    // SAFETY: the pointer/count pair was allocated by the bridge and is freed exactly once here.
    unsafe { ffi::vn_request_observations_free(array, count) };
    observations
}

fn c_string_or_empty(ptr: *mut c_char) -> String {
    if ptr.is_null() {
        String::new()
    } else {
        // SAFETY: the C string pointer is non-null (checked above) and valid for the duration of this borrow.
        unsafe { CStr::from_ptr(ptr) }
            .to_string_lossy()
            .into_owned()
    }
}

fn path_to_cstring(path: &Path, label: &str) -> Result<CString, VisionError> {
    let path_str = path
        .to_str()
        .ok_or_else(|| VisionError::InvalidArgument(format!("non-UTF-8 {label}")))?;
    CString::new(path_str)
        .map_err(|err| VisionError::InvalidArgument(format!("{label} NUL byte: {err}")))
}

#[doc(hidden)]
/// Test helper used by the smoke test + example — renders a tiny MOV file with
/// two text segments so `VNVideoProcessor` can be exercised without fixture
/// assets checked into git.
pub fn _test_helper_render_text_video(
    first_text: &str,
    second_text: &str,
    width: i32,
    height: i32,
    fps: i32,
    frames_per_text: i32,
    path: &Path,
) -> Result<(), VisionError> {
    let first_c =
        CString::new(first_text).map_err(|err| VisionError::InvalidArgument(err.to_string()))?;
    let second_c =
        CString::new(second_text).map_err(|err| VisionError::InvalidArgument(err.to_string()))?;
    let path_c = CString::new(path.to_string_lossy().as_ref())
        .map_err(|err| VisionError::InvalidArgument(err.to_string()))?;
    // SAFETY: all pointer arguments are valid stack locations or bridge-owned handles; strings are valid C strings for the duration of the call.
    let status = unsafe {
        ffi::vn_test_helper_render_text_video(
            first_c.as_ptr(),
            second_c.as_ptr(),
            width,
            height,
            fps,
            frames_per_text,
            path_c.as_ptr(),
        )
    };
    if status != ffi::status::OK {
        return Err(VisionError::Unknown {
            code: status,
            message: "video render helper failed".into(),
        });
    }
    Ok(())
}
