// This file is auto-generated by alef. DO NOT EDIT.
// alef:hash:4e15143f4af1ae8bafbdb1506ef057da924484c66a19483966333558ad437e75
// Re-generate with: alef generate
#![allow(dead_code, unused_imports, unused_variables)]
#![allow(
    clippy::too_many_arguments,
    clippy::let_unit_value,
    clippy::needless_borrow,
    clippy::map_identity,
    clippy::just_underscores_and_digits,
    clippy::unused_unit,
    clippy::unnecessary_cast,
    clippy::unwrap_or_default,
    clippy::derivable_impls,
    clippy::needless_borrows_for_generic_args,
    clippy::unnecessary_fallible_conversions
)]

use extendr_api::Result;
use extendr_api::prelude::*;
use std::collections::HashMap;
use std::sync::Arc;

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CacheStats {
    pub total_files: f64,
    pub total_size_mb: f64,
    pub available_space_mb: f64,
    pub oldest_file_age_days: f64,
    pub newest_file_age_days: f64,
}

#[extendr]
impl CacheStats {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct AccelerationConfig {
    /// Execution provider to use for ONNX inference.
    pub provider: ExecutionProviderType,
    /// GPU device ID (for CUDA/TensorRT). Ignored for CPU/CoreML/Auto.
    pub device_id: i32,
}

#[extendr]
impl AccelerationConfig {}

#[extendr]
pub fn new_accelerationconfig(provider: Option<String>, device_id: Option<i32>) -> AccelerationConfig {
    let mut __out = <AccelerationConfig>::default();
    if let Some(v) = provider {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.provider = parsed;
        }
    }
    if let Some(v) = device_id {
        __out.device_id = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ContentFilterConfig {
    /// Include running headers in extraction output.
    ///
    /// - PDF: Disables top-margin furniture stripping and prevents the layout
    ///   model from treating `PageHeader`-classified regions as furniture.
    /// - DOCX: Includes document headers in text output.
    /// - RTF/ODT: Headers already included; this is a no-op when true.
    /// - HTML/EPUB: Keeps `<header>` element content.
    ///
    /// Default: `false` (headers are stripped or excluded).
    pub include_headers: bool,
    /// Include running footers in extraction output.
    ///
    /// - PDF: Disables bottom-margin furniture stripping and prevents the layout
    ///   model from treating `PageFooter`-classified regions as furniture.
    /// - DOCX: Includes document footers in text output.
    /// - RTF/ODT: Footers already included; this is a no-op when true.
    /// - HTML/EPUB: Keeps `<footer>` element content.
    ///
    /// Default: `false` (footers are stripped or excluded).
    pub include_footers: bool,
    /// Enable the heuristic cross-page repeating text detector.
    ///
    /// When `true` (default), text that repeats verbatim across a supermajority
    /// of pages is classified as furniture and stripped.  Disable this if brand
    /// names or repeated headings are being incorrectly removed by the heuristic.
    ///
    /// Note: when a layout-detection model is active, the model may independently
    /// classify page-header / page-footer regions as furniture on a per-page basis.
    /// To preserve those regions, set `include_headers = true`, `include_footers = true`,
    /// or both, in addition to disabling this flag.
    ///
    /// Primarily affects PDF extraction.
    ///
    /// Default: `true`.
    pub strip_repeating_text: bool,
    /// Include watermark text in extraction output.
    ///
    /// - PDF: Keeps watermark artifacts and arXiv identifiers.
    /// - Other formats: No effect currently.
    ///
    /// Default: `false` (watermarks are stripped).
    pub include_watermarks: bool,
}

#[extendr]
impl ContentFilterConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> ContentFilterConfig {
        kreuzberg::ContentFilterConfig::default().into()
    }
}

#[extendr]
pub fn new_contentfilterconfig(
    include_headers: Option<bool>,
    include_footers: Option<bool>,
    strip_repeating_text: Option<bool>,
    include_watermarks: Option<bool>,
) -> ContentFilterConfig {
    let mut __out = <ContentFilterConfig>::default();
    if let Some(v) = include_headers {
        __out.include_headers = v;
    }
    if let Some(v) = include_footers {
        __out.include_footers = v;
    }
    if let Some(v) = strip_repeating_text {
        __out.strip_repeating_text = v;
    }
    if let Some(v) = include_watermarks {
        __out.include_watermarks = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EmailConfig {
    /// Windows codepage number to use when an MSG file contains no codepage property.
    /// Defaults to `None`, which falls back to windows-1252.
    ///
    /// If an unrecognized or invalid codepage number is supplied (including 0),
    /// the behavior silently falls back to windows-1252 — the same as when the
    /// MSG file itself contains an unrecognized codepage. No error or warning is
    /// emitted. Users should verify output when supplying unusual values.
    ///
    /// Common values:
    /// - 1250: Central European (Polish, Czech, Hungarian, etc.)
    /// - 1251: Cyrillic (Russian, Ukrainian, Bulgarian, etc.)
    /// - 1252: Western European (default)
    /// - 1253: Greek
    /// - 1254: Turkish
    /// - 1255: Hebrew
    /// - 1256: Arabic
    /// - 932:  Japanese (Shift-JIS)
    /// - 936:  Simplified Chinese (GBK)
    pub msg_fallback_codepage: Option<i32>,
}

#[extendr]
impl EmailConfig {}

#[extendr]
pub fn new_emailconfig(msg_fallback_codepage: Option<i32>) -> EmailConfig {
    let mut __out = <EmailConfig>::default();
    if let Some(v) = msg_fallback_codepage {
        __out.msg_fallback_codepage = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ExtractionConfig {
    /// Enable caching of extraction results
    pub use_cache: bool,
    /// Enable quality post-processing
    pub enable_quality_processing: bool,
    /// OCR configuration (None = OCR disabled)
    pub ocr: Option<OcrConfig>,
    /// Force OCR even for searchable PDFs
    pub force_ocr: bool,
    /// Force OCR on specific pages only (1-indexed page numbers, must be >= 1).
    ///
    /// When set, only the listed pages are OCR'd regardless of text layer quality.
    /// Unlisted pages use native text extraction. Ignored when `force_ocr` is `true`.
    /// Only applies to PDF documents. Duplicates are automatically deduplicated.
    /// An `ocr` config is recommended for backend/language selection; defaults are used if absent.
    pub force_ocr_pages: Option<Vec<i32>>,
    /// Disable OCR entirely, even for images.
    ///
    /// When `true`, OCR is skipped for all document types. Images return metadata
    /// only (dimensions, format, EXIF) without text extraction. PDFs use only
    /// native text extraction without OCR fallback.
    ///
    /// Cannot be `true` simultaneously with `force_ocr`.
    ///
    /// *Added in v4.7.0.*
    pub disable_ocr: bool,
    /// Text chunking configuration (None = chunking disabled)
    pub chunking: Option<ChunkingConfig>,
    /// Content filtering configuration (None = use extractor defaults).
    ///
    /// Controls whether document "furniture" (headers, footers, watermarks,
    /// repeating text) is included in or stripped from extraction results.
    /// See [`ContentFilterConfig`] for per-field documentation.
    pub content_filter: Option<ContentFilterConfig>,
    /// Image extraction configuration (None = no image extraction)
    pub images: Option<ImageExtractionConfig>,
    /// Token reduction configuration (None = no token reduction)
    pub token_reduction: Option<TokenReductionOptions>,
    /// Language detection configuration (None = no language detection)
    pub language_detection: Option<LanguageDetectionConfig>,
    /// Page extraction configuration (None = no page tracking)
    pub pages: Option<PageConfig>,
    /// Post-processor configuration (None = use defaults)
    pub postprocessor: Option<PostProcessorConfig>,
    /// Default per-file timeout in seconds for batch extraction.
    ///
    /// When set, each file in a batch will be canceled after this duration
    /// unless overridden by [`FileExtractionConfig::timeout_secs`].
    ///
    /// Defaults to `Some(60)` to prevent pathological files (e.g. deeply
    /// nested archives, documents with millions of cells) from running
    /// indefinitely and exhausting caller resources. Set to `None` to
    /// disable the timeout for trusted input or long-running workloads.
    pub extraction_timeout_secs: Option<f64>,
    /// Maximum concurrent extractions in batch operations (None = (num_cpus × 1.5).ceil()).
    ///
    /// Limits parallelism to prevent resource exhaustion when processing
    /// large batches. Defaults to (num_cpus × 1.5).ceil() when not set.
    pub max_concurrent_extractions: Option<f64>,
    /// Result structure format
    ///
    /// Controls whether results are returned in unified format (default) with all
    /// content in the `content` field, or element-based format with semantic
    /// elements (for Unstructured-compatible output).
    pub result_format: ResultFormat,
    /// Security limits for archive extraction.
    ///
    /// Controls maximum archive size, compression ratio, file count, and other
    /// security thresholds to prevent decompression bomb attacks. Also caps
    /// nesting depth, iteration count, entity / token length, total
    /// content size, and table cell count for every extraction path that
    /// ingests user-controlled bytes.
    /// When `None`, default limits are used.
    pub security_limits: Option<SecurityLimits>,
    /// Maximum uncompressed size in bytes for a single embedded file before
    /// recursive extraction is attempted (default: 50 MiB).
    ///
    /// Applies to embedded objects inside OOXML containers (DOCX, PPTX) and
    /// to email attachments processed via recursive extraction. Files that
    /// exceed this limit are skipped with a `ProcessingWarning` rather than
    /// passed to the extraction pipeline, preventing a single oversized
    /// embedded object from consuming unbounded memory or time.
    ///
    /// Set to `None` to disable the per-embedded-file cap (falls back to
    /// `security_limits.max_archive_size` as the only guard).
    pub max_embedded_file_bytes: Option<f64>,
    /// Content text format (default: Plain).
    ///
    /// Controls the format of the extracted content:
    /// - `Plain`: Raw extracted text (default)
    /// - `Markdown`: Markdown formatted output
    /// - `Djot`: Djot markup format (requires djot feature)
    /// - `Html`: HTML formatted output
    ///
    /// When set to a structured format, extraction results will include
    /// formatted output. The `formatted_content` field may be populated
    /// when format conversion is applied.
    pub output_format: OutputFormat,
    /// Run layout detection on the non-OCR PDF markdown path.
    ///
    /// When `true` and `layout` is `Some(_)`, layout regions inform heading,
    /// table, list, and figure detection in the structure pipeline that would
    /// otherwise rely on font-clustering heuristics alone. Significantly
    /// improves SF1 (structural F1) at the cost of inference latency
    /// (~150-300ms/page CPU, ~20-50ms/page GPU). Default: `false`.
    /// Requires the `layout-detection` feature.
    pub use_layout_for_markdown: bool,
    /// Enable structured document tree output.
    ///
    /// When true, populates the `document` field on `ExtractionResult` with a
    /// hierarchical `DocumentStructure` containing heading-driven section nesting,
    /// table grids, content layer classification, and inline annotations.
    ///
    /// Independent of `result_format` — can be combined with Unified or ElementBased.
    pub include_document_structure: bool,
    /// Hardware acceleration configuration for ONNX Runtime models.
    ///
    /// Controls execution provider selection for layout detection and embedding
    /// models. When `None`, uses platform defaults (CoreML on macOS, CUDA on
    /// Linux, CPU on Windows).
    pub acceleration: Option<AccelerationConfig>,
    /// Cache namespace for tenant isolation.
    ///
    /// When set, cache entries are stored under `{cache_dir}/{namespace}/`.
    /// Must be alphanumeric, hyphens, or underscores only (max 64 chars).
    /// Different namespaces have isolated cache spaces on the same filesystem.
    pub cache_namespace: Option<String>,
    /// Per-request cache TTL in seconds.
    ///
    /// Overrides the global `max_age_days` for this specific extraction.
    /// When `0`, caching is completely skipped (no read or write).
    /// When `None`, the global TTL applies.
    pub cache_ttl_secs: Option<f64>,
    /// Email extraction configuration (None = use defaults).
    ///
    /// Currently supports configuring the fallback codepage for MSG files
    /// that do not specify one. See `EmailConfig` for details.
    pub email: Option<EmailConfig>,
    /// Concurrency limits for constrained environments (None = use defaults).
    ///
    /// Controls Rayon thread pool size, ONNX Runtime intra-op threads, and
    /// (when `max_concurrent_extractions` is unset) the batch concurrency
    /// semaphore. See `ConcurrencyConfig` for details.
    pub concurrency: Option<String>,
    /// Maximum recursion depth for archive extraction (default: 3).
    /// Set to 0 to disable recursive extraction (legacy behavior).
    pub max_archive_depth: f64,
    /// Structured extraction via LLM (None = disabled).
    ///
    /// When set, the extracted document content is sent to an LLM with the
    /// provided JSON schema. The structured response is stored in
    /// `ExtractionResult::structured_output`.
    pub structured_extraction: Option<StructuredExtractionConfig>,
    /// Cancellation token for this extraction (None = no external cancellation).
    ///
    /// Pass a [`CancellationToken`] clone here and call [`CancellationToken::cancel`]
    /// from another thread / task to abort the extraction in progress. The extractor
    /// checks the token at safe checkpoints (before lock acquisition, between pages,
    /// between batch items) and returns [`KreuzbergError::Cancelled`] when set.
    ///
    /// The field is excluded from serialization because `CancellationToken` is a
    /// runtime handle, not a configuration value.
    pub cancel_token: Option<String>,
}

#[extendr]
impl ExtractionConfig {
    pub fn needs_image_processing(&self) -> bool {
        #[allow(clippy::needless_update)]
        let core_self = kreuzberg::ExtractionConfig {
            use_cache: self.use_cache,

            enable_quality_processing: self.enable_quality_processing,

            ocr: self.ocr.clone().map(Into::into),

            force_ocr: self.force_ocr,

            force_ocr_pages: self
                .force_ocr_pages
                .clone()
                .map(|v| v.into_iter().map(|x| x as u32).collect()),

            disable_ocr: self.disable_ocr,

            chunking: self.chunking.clone().map(Into::into),

            content_filter: self.content_filter.clone().map(Into::into),

            images: self.images.clone().map(Into::into),

            token_reduction: self.token_reduction.clone().map(Into::into),

            language_detection: self.language_detection.clone().map(Into::into),

            pages: self.pages.clone().map(Into::into),

            postprocessor: self.postprocessor.clone().map(Into::into),

            extraction_timeout_secs: self.extraction_timeout_secs.map(|v| v as u64),

            max_concurrent_extractions: self.max_concurrent_extractions.map(|v| v as usize),

            result_format: self.result_format.clone().into(),

            security_limits: self.security_limits.clone().map(Into::into),

            max_embedded_file_bytes: self.max_embedded_file_bytes.map(|v| v as u64),

            output_format: Default::default(),
            use_layout_for_markdown: self.use_layout_for_markdown,

            include_document_structure: self.include_document_structure,

            acceleration: self.acceleration.clone().map(Into::into),

            cache_namespace: self.cache_namespace.clone(),

            cache_ttl_secs: self.cache_ttl_secs.map(|v| v as u64),

            email: self.email.clone().map(Into::into),

            concurrency: Default::default(),

            max_archive_depth: self.max_archive_depth as usize,

            structured_extraction: self.structured_extraction.clone().map(Into::into),

            cancel_token: Default::default(),

            ..Default::default()
        };
        core_self.needs_image_processing()
    }

    #[allow(clippy::should_implement_trait)]
    pub fn default() -> ExtractionConfig {
        kreuzberg::ExtractionConfig::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<ExtractionConfig> {
        let core: kreuzberg::ExtractionConfig =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[extendr]
pub fn new_extractionconfig(
    use_cache: Option<bool>,
    enable_quality_processing: Option<bool>,
    force_ocr: Option<bool>,
    force_ocr_pages: Option<Vec<i32>>,
    disable_ocr: Option<bool>,
    extraction_timeout_secs: Option<f64>,
    max_concurrent_extractions: Option<f64>,
    result_format: Option<String>,
    max_embedded_file_bytes: Option<f64>,
    output_format: Option<String>,
    use_layout_for_markdown: Option<bool>,
    include_document_structure: Option<bool>,
    cache_namespace: Option<String>,
    cache_ttl_secs: Option<f64>,
    concurrency: Option<String>,
    max_archive_depth: Option<f64>,
    cancel_token: Option<String>,
) -> ExtractionConfig {
    let mut __out = <ExtractionConfig>::default();
    if let Some(v) = use_cache {
        __out.use_cache = v;
    }
    if let Some(v) = enable_quality_processing {
        __out.enable_quality_processing = v;
    }
    if let Some(v) = force_ocr {
        __out.force_ocr = v;
    }
    if let Some(v) = force_ocr_pages {
        __out.force_ocr_pages = Some(v);
    }
    if let Some(v) = disable_ocr {
        __out.disable_ocr = v;
    }
    if let Some(v) = extraction_timeout_secs {
        __out.extraction_timeout_secs = Some(v);
    }
    if let Some(v) = max_concurrent_extractions {
        __out.max_concurrent_extractions = Some(v);
    }
    if let Some(v) = result_format {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.result_format = parsed;
        }
    }
    if let Some(v) = max_embedded_file_bytes {
        __out.max_embedded_file_bytes = Some(v);
    }
    if let Some(v) = output_format {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.output_format = parsed;
        }
    }
    if let Some(v) = use_layout_for_markdown {
        __out.use_layout_for_markdown = v;
    }
    if let Some(v) = include_document_structure {
        __out.include_document_structure = v;
    }
    if let Some(v) = cache_namespace {
        __out.cache_namespace = Some(v);
    }
    if let Some(v) = cache_ttl_secs {
        __out.cache_ttl_secs = Some(v);
    }
    if let Some(v) = concurrency {
        __out.concurrency = Some(v);
    }
    if let Some(v) = max_archive_depth {
        __out.max_archive_depth = v;
    }
    if let Some(v) = cancel_token {
        __out.cancel_token = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct FileExtractionConfig {
    /// Override quality post-processing for this file.
    pub enable_quality_processing: Option<bool>,
    /// Override OCR configuration for this file (None in the Option = use batch default).
    pub ocr: Option<OcrConfig>,
    /// Override force OCR for this file.
    pub force_ocr: Option<bool>,
    /// Override force OCR pages for this file (1-indexed page numbers).
    pub force_ocr_pages: Option<Vec<i32>>,
    /// Override disable OCR for this file.
    pub disable_ocr: Option<bool>,
    /// Override chunking configuration for this file.
    pub chunking: Option<ChunkingConfig>,
    /// Override content filtering configuration for this file.
    pub content_filter: Option<ContentFilterConfig>,
    /// Override image extraction configuration for this file.
    pub images: Option<ImageExtractionConfig>,
    /// Override token reduction for this file.
    pub token_reduction: Option<TokenReductionOptions>,
    /// Override language detection for this file.
    pub language_detection: Option<LanguageDetectionConfig>,
    /// Override page extraction for this file.
    pub pages: Option<PageConfig>,
    /// Override post-processor for this file.
    pub postprocessor: Option<PostProcessorConfig>,
    /// Override result format for this file.
    pub result_format: Option<ResultFormat>,
    /// Override output content format for this file.
    pub output_format: Option<OutputFormat>,
    /// Override document structure output for this file.
    pub include_document_structure: Option<bool>,
    /// Override per-file extraction timeout in seconds.
    ///
    /// When set, the extraction for this file will be canceled after the
    /// specified duration. A timed-out file produces an error result without
    /// affecting other files in the batch.
    pub timeout_secs: Option<f64>,
    /// Override structured extraction configuration for this file.
    ///
    /// When set, enables LLM-based structured extraction with a JSON schema
    /// for this specific file. The extracted content is sent to a VLM/LLM
    /// and the response is parsed according to the provided schema.
    pub structured_extraction: Option<StructuredExtractionConfig>,
}

#[extendr]
impl FileExtractionConfig {}

#[extendr]
pub fn new_fileextractionconfig(
    enable_quality_processing: Option<bool>,
    force_ocr: Option<bool>,
    force_ocr_pages: Option<Vec<i32>>,
    disable_ocr: Option<bool>,
    result_format: Option<String>,
    output_format: Option<String>,
    include_document_structure: Option<bool>,
    timeout_secs: Option<f64>,
) -> FileExtractionConfig {
    let mut __out = <FileExtractionConfig>::default();
    if let Some(v) = enable_quality_processing {
        __out.enable_quality_processing = Some(v);
    }
    if let Some(v) = force_ocr {
        __out.force_ocr = Some(v);
    }
    if let Some(v) = force_ocr_pages {
        __out.force_ocr_pages = Some(v);
    }
    if let Some(v) = disable_ocr {
        __out.disable_ocr = Some(v);
    }
    if let Some(v) = result_format {
        __out.result_format = serde_json::from_str(&format!("\"{v}\"")).ok();
    }
    if let Some(v) = output_format {
        __out.output_format = serde_json::from_str(&format!("\"{v}\"")).ok();
    }
    if let Some(v) = include_document_structure {
        __out.include_document_structure = Some(v);
    }
    if let Some(v) = timeout_secs {
        __out.timeout_secs = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct BatchBytesItem {
    /// The content bytes to extract from
    pub content: Vec<u8>,
    /// MIME type of the content (e.g., "application/pdf", "text/html")
    pub mime_type: String,
    /// Per-item configuration overrides (None uses batch-level defaults)
    pub config: Option<FileExtractionConfig>,
}

#[extendr]
impl BatchBytesItem {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct BatchFileItem {
    /// Path to the file to extract from
    pub path: String,
    /// Per-file configuration overrides (None uses batch-level defaults)
    pub config: Option<FileExtractionConfig>,
}

#[extendr]
impl BatchFileItem {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct ImageExtractionConfig {
    /// Extract images from documents
    pub extract_images: bool,
    /// Target DPI for image normalization
    pub target_dpi: i32,
    /// Maximum dimension for images (width or height)
    pub max_image_dimension: i32,
    /// Whether to inject image reference placeholders into markdown output.
    /// When `true` (default), image references like `![Image 1](embedded:p1_i0)`
    /// are appended to the markdown. Set to `false` to extract images as data
    /// without polluting the markdown output.
    pub inject_placeholders: bool,
    /// Automatically adjust DPI based on image content
    pub auto_adjust_dpi: bool,
    /// Minimum DPI threshold
    pub min_dpi: i32,
    /// Maximum DPI threshold
    pub max_dpi: i32,
    /// Maximum number of image objects to extract per PDF page.
    ///
    /// Some PDFs (e.g. technical diagrams stored as thousands of raster fragments)
    /// can trigger extremely long or indefinite extraction times when every image
    /// object on a dense page is decoded individually via the PDF extractor. Setting this
    /// limit causes kreuzberg to stop collecting individual images once the count
    /// per page reaches the cap and emit a warning instead.
    ///
    /// `None` (default) means no limit — all images are extracted.
    pub max_images_per_page: Option<i32>,
    /// When `true` (default), extracted images are classified by kind and grouped
    /// into clusters where they appear to belong to one figure.
    pub classify: bool,
    /// When `true`, full-page renders produced during OCR preprocessing are captured
    /// and returned as `ImageKind::PageRaster` entries in `ExtractionResult.images`.
    ///
    /// **PDF + OCR only.** No rasters are captured for non-PDF inputs or when the
    /// document-level OCR bypass is active (whole-document backend). When OCR is
    /// enabled and this flag is set but the active backend skips per-page rendering,
    /// a `ProcessingWarning` is emitted in `ExtractionResult.processing_warnings`.
    ///
    /// Defaults to `false`. Enable when downstream consumers need page thumbnails
    /// (e.g. citation previews, visual grounding).
    pub include_page_rasters: bool,
    /// Run OCR on extracted images and include the recognized text in the document content.
    ///
    /// When `true` (default) and `ExtractionConfig.ocr` is configured, extracted images
    /// are processed with the configured OCR backend. Set to `false` to extract images
    /// without OCR processing, even when OCR is enabled.
    pub run_ocr_on_images: bool,
    /// When `true`, image OCR results are rendered as plain text without the
    /// `![...](...)` markdown placeholder. Only takes effect when `run_ocr_on_images`
    /// is also `true`.
    pub ocr_text_only: bool,
    /// When `true` and `ocr_text_only` is `false`, append the OCR text after
    /// the image placeholder in the rendered output.
    pub append_ocr_text: bool,
}

#[extendr]
impl ImageExtractionConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> ImageExtractionConfig {
        kreuzberg::ImageExtractionConfig::default().into()
    }
}

#[extendr]
pub fn new_imageextractionconfig(
    extract_images: Option<bool>,
    target_dpi: Option<i32>,
    max_image_dimension: Option<i32>,
    inject_placeholders: Option<bool>,
    auto_adjust_dpi: Option<bool>,
    min_dpi: Option<i32>,
    max_dpi: Option<i32>,
    max_images_per_page: Option<i32>,
    classify: Option<bool>,
    include_page_rasters: Option<bool>,
    run_ocr_on_images: Option<bool>,
    ocr_text_only: Option<bool>,
    append_ocr_text: Option<bool>,
) -> ImageExtractionConfig {
    let mut __out = <ImageExtractionConfig>::default();
    if let Some(v) = extract_images {
        __out.extract_images = v;
    }
    if let Some(v) = target_dpi {
        __out.target_dpi = v;
    }
    if let Some(v) = max_image_dimension {
        __out.max_image_dimension = v;
    }
    if let Some(v) = inject_placeholders {
        __out.inject_placeholders = v;
    }
    if let Some(v) = auto_adjust_dpi {
        __out.auto_adjust_dpi = v;
    }
    if let Some(v) = min_dpi {
        __out.min_dpi = v;
    }
    if let Some(v) = max_dpi {
        __out.max_dpi = v;
    }
    if let Some(v) = max_images_per_page {
        __out.max_images_per_page = Some(v);
    }
    if let Some(v) = classify {
        __out.classify = v;
    }
    if let Some(v) = include_page_rasters {
        __out.include_page_rasters = v;
    }
    if let Some(v) = run_ocr_on_images {
        __out.run_ocr_on_images = v;
    }
    if let Some(v) = ocr_text_only {
        __out.ocr_text_only = v;
    }
    if let Some(v) = append_ocr_text {
        __out.append_ocr_text = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct TokenReductionOptions {
    /// Reduction mode: "off", "light", "moderate", "aggressive", "maximum"
    pub mode: String,
    /// Preserve important words (capitalized, technical terms)
    pub preserve_important_words: bool,
}

#[extendr]
impl TokenReductionOptions {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> TokenReductionOptions {
        kreuzberg::TokenReductionOptions::default().into()
    }
}

#[extendr]
pub fn new_tokenreductionoptions(
    mode: Option<String>,
    preserve_important_words: Option<bool>,
) -> TokenReductionOptions {
    let mut __out = <TokenReductionOptions>::default();
    if let Some(v) = mode {
        __out.mode = v;
    }
    if let Some(v) = preserve_important_words {
        __out.preserve_important_words = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct LanguageDetectionConfig {
    /// Enable language detection
    pub enabled: bool,
    /// Minimum confidence threshold (0.0-1.0)
    pub min_confidence: f64,
    /// Detect multiple languages in the document
    pub detect_multiple: bool,
}

#[extendr]
impl LanguageDetectionConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> LanguageDetectionConfig {
        kreuzberg::LanguageDetectionConfig::default().into()
    }
}

#[extendr]
pub fn new_languagedetectionconfig(
    enabled: Option<bool>,
    min_confidence: Option<f64>,
    detect_multiple: Option<bool>,
) -> LanguageDetectionConfig {
    let mut __out = <LanguageDetectionConfig>::default();
    if let Some(v) = enabled {
        __out.enabled = v;
    }
    if let Some(v) = min_confidence {
        __out.min_confidence = v;
    }
    if let Some(v) = detect_multiple {
        __out.detect_multiple = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HtmlOutputConfig {
    /// Inline CSS string injected into the output after the theme stylesheet.
    /// Concatenated after `css_file` content when both are set.
    pub css: Option<String>,
    /// Path to a CSS file loaded once at renderer construction time.
    /// Concatenated before `css` when both are set.
    pub css_file: Option<String>,
    /// Built-in colour/typography theme. Default: [`HtmlTheme::Unstyled`].
    pub theme: HtmlTheme,
    /// CSS class prefix applied to every emitted class name.
    ///
    /// Default: `"kb-"`. Change this if your host application already uses
    /// classes that start with `kb-`.
    pub class_prefix: String,
    /// When `true` (default), write the resolved CSS into a `<style>` block
    /// immediately after the opening `<div class="{prefix}doc">`.
    ///
    /// Set to `false` to emit only the structural markup and wire up your
    /// own stylesheet targeting the `kb-*` class names.
    pub embed_css: bool,
}

#[extendr]
impl HtmlOutputConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> HtmlOutputConfig {
        kreuzberg::HtmlOutputConfig::default().into()
    }
}

#[extendr]
pub fn new_htmloutputconfig(
    css: Option<String>,
    css_file: Option<String>,
    theme: Option<String>,
    class_prefix: Option<String>,
    embed_css: Option<bool>,
) -> HtmlOutputConfig {
    let mut __out = <HtmlOutputConfig>::default();
    if let Some(v) = css {
        __out.css = Some(v);
    }
    if let Some(v) = css_file {
        __out.css_file = Some(v);
    }
    if let Some(v) = theme {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.theme = parsed;
        }
    }
    if let Some(v) = class_prefix {
        __out.class_prefix = v;
    }
    if let Some(v) = embed_css {
        __out.embed_css = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct LayoutDetectionConfig {
    /// Confidence threshold override (None = use model default).
    pub confidence_threshold: Option<f64>,
    /// Whether to apply postprocessing heuristics (default: true).
    pub apply_heuristics: bool,
    /// Table structure recognition model.
    ///
    /// Controls which model is used for table cell detection within layout-detected
    /// table regions. Defaults to [`TableModel::Tatr`].
    pub table_model: TableModel,
    /// Hardware acceleration for ONNX models (layout detection + table structure).
    ///
    /// When set, controls which execution provider (CPU, CUDA, CoreML, TensorRT)
    /// is used for inference. Defaults to `None` (auto-select per platform).
    pub acceleration: Option<AccelerationConfig>,
}

#[extendr]
impl LayoutDetectionConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> LayoutDetectionConfig {
        kreuzberg::LayoutDetectionConfig::default().into()
    }
}

#[extendr]
pub fn new_layoutdetectionconfig(
    confidence_threshold: Option<f64>,
    apply_heuristics: Option<bool>,
    table_model: Option<String>,
) -> LayoutDetectionConfig {
    let mut __out = <LayoutDetectionConfig>::default();
    if let Some(v) = confidence_threshold {
        __out.confidence_threshold = Some(v);
    }
    if let Some(v) = apply_heuristics {
        __out.apply_heuristics = v;
    }
    if let Some(v) = table_model {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.table_model = parsed;
        }
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct LlmConfig {
    /// Provider/model string using liter-llm routing format.
    ///
    /// Examples: `"openai/gpt-4o"`, `"anthropic/claude-sonnet-4-20250514"`,
    /// `"groq/llama-3.1-70b-versatile"`.
    pub model: String,
    /// API key for the provider. When `None`, liter-llm falls back to
    /// the provider's standard environment variable (e.g., `OPENAI_API_KEY`).
    pub api_key: Option<String>,
    /// Custom base URL override for the provider endpoint.
    pub base_url: Option<String>,
    /// Request timeout in seconds (default: 60).
    pub timeout_secs: Option<f64>,
    /// Maximum retry attempts (default: 3).
    pub max_retries: Option<i32>,
    /// Sampling temperature for generation tasks.
    pub temperature: Option<f64>,
    /// Maximum tokens to generate.
    pub max_tokens: Option<f64>,
}

#[extendr]
impl LlmConfig {}

#[extendr]
pub fn new_llmconfig(
    model: Option<String>,
    api_key: Option<String>,
    base_url: Option<String>,
    timeout_secs: Option<f64>,
    max_retries: Option<i32>,
    temperature: Option<f64>,
    max_tokens: Option<f64>,
) -> LlmConfig {
    let mut __out = <LlmConfig>::default();
    if let Some(v) = model {
        __out.model = v;
    }
    if let Some(v) = api_key {
        __out.api_key = Some(v);
    }
    if let Some(v) = base_url {
        __out.base_url = Some(v);
    }
    if let Some(v) = timeout_secs {
        __out.timeout_secs = Some(v);
    }
    if let Some(v) = max_retries {
        __out.max_retries = Some(v);
    }
    if let Some(v) = temperature {
        __out.temperature = Some(v);
    }
    if let Some(v) = max_tokens {
        __out.max_tokens = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct StructuredExtractionConfig {
    /// JSON Schema defining the desired output structure.
    pub schema: String,
    /// Schema name passed to the LLM's structured output mode.
    pub schema_name: String,
    /// Optional schema description for the LLM.
    pub schema_description: Option<String>,
    /// Enable strict mode — output must exactly match the schema.
    pub strict: bool,
    /// Custom Jinja2 extraction prompt template. When `None`, a default template is used.
    ///
    /// Available template variables:
    /// - `{{ content }}` — The extracted document text.
    /// - `{{ schema }}` — The JSON schema as a formatted string.
    /// - `{{ schema_name }}` — The schema name.
    /// - `{{ schema_description }}` — The schema description (may be empty).
    pub prompt: Option<String>,
    /// LLM configuration for the extraction.
    pub llm: LlmConfig,
}

#[extendr]
impl StructuredExtractionConfig {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrQualityThresholds {
    /// Minimum total non-whitespace characters to consider text substantive.
    pub min_total_non_whitespace: f64,
    /// Minimum non-whitespace characters per page on average.
    pub min_non_whitespace_per_page: f64,
    /// Minimum character count for a word to be "meaningful".
    pub min_meaningful_word_len: f64,
    /// Minimum count of meaningful words before text is accepted.
    pub min_meaningful_words: f64,
    /// Minimum alphanumeric ratio (non-whitespace chars that are alphanumeric).
    pub min_alnum_ratio: f64,
    /// Minimum Unicode replacement characters (U+FFFD) to trigger OCR fallback.
    pub min_garbage_chars: f64,
    /// Maximum fraction of short (1-2 char) words before text is considered fragmented.
    pub max_fragmented_word_ratio: f64,
    /// Critical fragmentation threshold — triggers OCR regardless of meaningful words.
    /// Normal English text has ~20-30% short words. 80%+ is definitive garbage.
    pub critical_fragmented_word_ratio: f64,
    /// Minimum average word length. Below this with enough words indicates garbled extraction.
    pub min_avg_word_length: f64,
    /// Minimum word count before average word length check applies.
    pub min_words_for_avg_length_check: f64,
    /// Minimum consecutive word repetition ratio to detect column scrambling.
    pub min_consecutive_repeat_ratio: f64,
    /// Minimum word count before consecutive repetition check is applied.
    pub min_words_for_repeat_check: f64,
    /// Minimum character count for "substantive markdown" OCR skip gate.
    pub substantive_min_chars: f64,
    /// Minimum character count for "non-text content" OCR skip gate.
    pub non_text_min_chars: f64,
    /// Alphanumeric+whitespace ratio threshold for skip decisions.
    pub alnum_ws_ratio_threshold: f64,
    /// Minimum quality score (0.0-1.0) for a pipeline stage result to be accepted.
    /// If the result from a backend scores below this, try the next backend.
    pub pipeline_min_quality: f64,
}

#[extendr]
impl OcrQualityThresholds {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> OcrQualityThresholds {
        kreuzberg::OcrQualityThresholds::default().into()
    }
}

#[extendr]
pub fn new_ocrqualitythresholds(
    min_total_non_whitespace: Option<f64>,
    min_non_whitespace_per_page: Option<f64>,
    min_meaningful_word_len: Option<f64>,
    min_meaningful_words: Option<f64>,
    min_alnum_ratio: Option<f64>,
    min_garbage_chars: Option<f64>,
    max_fragmented_word_ratio: Option<f64>,
    critical_fragmented_word_ratio: Option<f64>,
    min_avg_word_length: Option<f64>,
    min_words_for_avg_length_check: Option<f64>,
    min_consecutive_repeat_ratio: Option<f64>,
    min_words_for_repeat_check: Option<f64>,
    substantive_min_chars: Option<f64>,
    non_text_min_chars: Option<f64>,
    alnum_ws_ratio_threshold: Option<f64>,
    pipeline_min_quality: Option<f64>,
) -> OcrQualityThresholds {
    let mut __out = <OcrQualityThresholds>::default();
    if let Some(v) = min_total_non_whitespace {
        __out.min_total_non_whitespace = v;
    }
    if let Some(v) = min_non_whitespace_per_page {
        __out.min_non_whitespace_per_page = v;
    }
    if let Some(v) = min_meaningful_word_len {
        __out.min_meaningful_word_len = v;
    }
    if let Some(v) = min_meaningful_words {
        __out.min_meaningful_words = v;
    }
    if let Some(v) = min_alnum_ratio {
        __out.min_alnum_ratio = v;
    }
    if let Some(v) = min_garbage_chars {
        __out.min_garbage_chars = v;
    }
    if let Some(v) = max_fragmented_word_ratio {
        __out.max_fragmented_word_ratio = v;
    }
    if let Some(v) = critical_fragmented_word_ratio {
        __out.critical_fragmented_word_ratio = v;
    }
    if let Some(v) = min_avg_word_length {
        __out.min_avg_word_length = v;
    }
    if let Some(v) = min_words_for_avg_length_check {
        __out.min_words_for_avg_length_check = v;
    }
    if let Some(v) = min_consecutive_repeat_ratio {
        __out.min_consecutive_repeat_ratio = v;
    }
    if let Some(v) = min_words_for_repeat_check {
        __out.min_words_for_repeat_check = v;
    }
    if let Some(v) = substantive_min_chars {
        __out.substantive_min_chars = v;
    }
    if let Some(v) = non_text_min_chars {
        __out.non_text_min_chars = v;
    }
    if let Some(v) = alnum_ws_ratio_threshold {
        __out.alnum_ws_ratio_threshold = v;
    }
    if let Some(v) = pipeline_min_quality {
        __out.pipeline_min_quality = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrPipelineStage {
    /// Backend name: "tesseract", "paddleocr", "easyocr", or a custom registered name.
    pub backend: String,
    /// Priority weight (higher = tried first). Stages are sorted by priority descending.
    pub priority: i32,
    /// Language override for this stage (None = use parent OcrConfig.language).
    pub language: Option<String>,
    /// Tesseract-specific config override for this stage.
    pub tesseract_config: Option<TesseractConfig>,
    /// PaddleOCR-specific config for this stage.
    pub paddle_ocr_config: Option<String>,
    /// VLM config override for this pipeline stage.
    pub vlm_config: Option<LlmConfig>,
    /// Arbitrary per-call options passed through to the backend unchanged.
    ///
    /// Backends that support runtime tuning (mode switching, preprocessing
    /// flags, inference parameters, etc.) read this value and deserialize
    /// the keys they care about. Keys unknown to the backend are silently
    /// ignored, so options from different backends can coexist in the same
    /// config without conflict.
    ///
    /// Example (custom backend):
    /// ```json
    /// { "mode": "fast", "enable_layout": true }
    /// ```
    pub backend_options: Option<String>,
}

#[extendr]
impl OcrPipelineStage {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrPipelineConfig {
    /// Ordered list of backends to try. Sorted by priority (descending) at runtime.
    pub stages: Vec<OcrPipelineStage>,
    /// Quality thresholds for deciding whether to accept a result or try the next backend.
    pub quality_thresholds: OcrQualityThresholds,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrConfig {
    /// Whether OCR is enabled.
    ///
    /// Setting `enabled: false` is a shorthand for `disable_ocr: true` on the parent
    /// [`ExtractionConfig`](crate::core::config::ExtractionConfig). Images return
    /// metadata only; PDFs use native text extraction without OCR fallback.
    ///
    /// Defaults to `true`. When `false`, all other OCR settings are ignored.
    pub enabled: bool,
    /// OCR backend: tesseract, easyocr, paddleocr
    pub backend: String,
    /// Language code (e.g., "eng", "deu")
    pub language: String,
    /// Tesseract-specific configuration (optional)
    pub tesseract_config: Option<TesseractConfig>,
    /// Output format for OCR results (optional, for format conversion)
    pub output_format: Option<OutputFormat>,
    /// PaddleOCR-specific configuration (optional, JSON passthrough)
    pub paddle_ocr_config: Option<String>,
    /// Arbitrary per-call options passed through to the backend unchanged.
    ///
    /// Custom OCR backends and built-in backends that support runtime tuning
    /// can read this value and deserialize the keys they care about. Keys
    /// unknown to the backend are silently ignored.
    ///
    /// This is the recommended extension point for per-call parameters that
    /// are not covered by the typed fields above (e.g. mode switching,
    /// preprocessing flags, inference batch size).
    ///
    /// **Scope:** when `pipeline` is `None`, this value is propagated to the
    /// primary stage of the auto-constructed pipeline. When `pipeline` is
    /// explicitly set, this field has **no effect** — the caller must set
    /// `OcrPipelineStage.backend_options` directly on the relevant stage(s)
    /// instead.
    ///
    /// Example:
    /// ```json
    /// { "mode": "fast", "enable_layout": true, "timeout_ms": 5000 }
    /// ```
    pub backend_options: Option<String>,
    /// OCR element extraction configuration
    pub element_config: Option<OcrElementConfig>,
    /// Quality thresholds for the native-text-to-OCR fallback decision.
    /// When None, uses compiled defaults (matching previous hardcoded behavior).
    pub quality_thresholds: Option<OcrQualityThresholds>,
    /// Multi-backend OCR pipeline configuration. When set, enables weighted
    /// fallback across multiple OCR backends based on output quality.
    /// When None, uses the single `backend` field (same as today).
    pub pipeline: Option<OcrPipelineConfig>,
    /// Enable automatic page rotation based on orientation detection.
    ///
    /// When enabled, uses Tesseract's `DetectOrientationScript()` to detect
    /// page orientation (0/90/180/270 degrees) before OCR. If the page is
    /// rotated with high confidence, the image is corrected before recognition.
    /// This is critical for handling rotated scanned documents.
    pub auto_rotate: bool,
    /// VLM (Vision Language Model) OCR configuration.
    ///
    /// Required when `backend` is `"vlm"`. Uses liter-llm to send page
    /// images to a vision model for text extraction.
    pub vlm_config: Option<LlmConfig>,
    /// Custom Jinja2 prompt template for VLM OCR.
    ///
    /// When `None`, uses the default template. Available variables:
    /// - `{{ language }}` — The document language code (e.g., "eng", "deu").
    pub vlm_prompt: Option<String>,
    /// Hardware acceleration for ONNX Runtime models (e.g. PaddleOCR, layout detection).
    ///
    /// Not user-configurable via config files — injected at runtime from
    /// `ExtractionConfig::acceleration` before each `process_image` call.
    pub acceleration: Option<AccelerationConfig>,
    /// Caller-supplied Tesseract `traineddata` bytes per language code.
    ///
    /// Primary use case is the WASM build, which has no filesystem and cannot
    /// download tessdata at runtime. Native builds typically rely on
    /// `TessdataManager` and ignore this field. When present, the WASM
    /// Tesseract backend prefers these bytes over its compile-time-bundled
    /// English data.
    ///
    /// Skipped by serde to keep config files small — supply via the typed API
    /// at runtime.
    pub tessdata_bytes: Option<HashMap<String, Vec<u8>>>,
}

#[extendr]
impl OcrConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> OcrConfig {
        kreuzberg::OcrConfig::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<OcrConfig> {
        let core: kreuzberg::OcrConfig =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[extendr]
pub fn new_ocrconfig(
    enabled: Option<bool>,
    backend: Option<String>,
    language: Option<String>,
    output_format: Option<String>,
    paddle_ocr_config: Option<String>,
    backend_options: Option<String>,
    auto_rotate: Option<bool>,
    vlm_prompt: Option<String>,
    tessdata_bytes: Option<HashMap<String, Vec<u8>>>,
) -> OcrConfig {
    let mut __out = <OcrConfig>::default();
    if let Some(v) = enabled {
        __out.enabled = v;
    }
    if let Some(v) = backend {
        __out.backend = v;
    }
    if let Some(v) = language {
        __out.language = v;
    }
    if let Some(v) = output_format {
        __out.output_format = serde_json::from_str(&format!("\"{v}\"")).ok();
    }
    if let Some(v) = paddle_ocr_config {
        __out.paddle_ocr_config = Some(v);
    }
    if let Some(v) = backend_options {
        __out.backend_options = Some(v);
    }
    if let Some(v) = auto_rotate {
        __out.auto_rotate = v;
    }
    if let Some(v) = vlm_prompt {
        __out.vlm_prompt = Some(v);
    }
    if let Some(v) = tessdata_bytes {
        __out.tessdata_bytes = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PageConfig {
    /// Extract pages as separate array (ExtractionResult.pages)
    pub extract_pages: bool,
    /// Insert page markers in main content string
    pub insert_page_markers: bool,
    /// Page marker format (use {page_num} placeholder)
    /// Default: "\n\n<!-- PAGE {page_num} -->\n\n"
    pub marker_format: String,
}

#[extendr]
impl PageConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> PageConfig {
        kreuzberg::PageConfig::default().into()
    }
}

#[extendr]
pub fn new_pageconfig(
    extract_pages: Option<bool>,
    insert_page_markers: Option<bool>,
    marker_format: Option<String>,
) -> PageConfig {
    let mut __out = <PageConfig>::default();
    if let Some(v) = extract_pages {
        __out.extract_pages = v;
    }
    if let Some(v) = insert_page_markers {
        __out.insert_page_markers = v;
    }
    if let Some(v) = marker_format {
        __out.marker_format = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PdfConfig {
    /// Extract images from PDF
    pub extract_images: bool,
    /// Extract tables from PDF.
    ///
    /// When `true` (default), runs pdf_oxide's native grid detector and, if it
    /// finds nothing, falls back to the heuristic text-layer reconstruction in
    /// `pdf::oxide::table::extract_tables_heuristic`. Set to `false` to skip
    /// both passes — `tables` will then be empty in the result.
    pub extract_tables: bool,
    /// List of passwords to try when opening encrypted PDFs
    pub passwords: Option<Vec<String>>,
    /// Extract PDF metadata
    pub extract_metadata: bool,
    /// Hierarchy extraction configuration (None = hierarchy extraction disabled)
    pub hierarchy: Option<HierarchyConfig>,
    /// Extract PDF annotations (text notes, highlights, links, stamps).
    /// Default: false
    pub extract_annotations: bool,
    /// Top margin fraction (0.0–1.0) of page height to exclude headers/running heads.
    /// Default: 0.06 (6%)
    pub top_margin_fraction: Option<f64>,
    /// Bottom margin fraction (0.0–1.0) of page height to exclude footers/page numbers.
    /// Default: 0.05 (5%)
    pub bottom_margin_fraction: Option<f64>,
    /// Allow single-column pseudo tables in extraction results.
    ///
    /// By default, tables with fewer than 2 columns (layout-guided) or 3 columns
    /// (heuristic) are rejected. When `true`, the minimum column count is relaxed
    /// to 1, allowing single-column structured data (glossaries, itemized lists)
    /// to be emitted as tables. Other quality filters (density, sparsity, prose
    /// detection) still apply.
    pub allow_single_column_tables: bool,
    /// Perform OCR on inline images extracted from PDF pages and attach the
    /// recognized text to each `ExtractedImage.ocr_result`. Requires Tesseract
    /// to be available; if `ExtractionConfig.ocr` is `None` the extractor
    /// falls back to `TesseractConfig::default()`. Per-image failures degrade
    /// gracefully (the image is returned without OCR text rather than failing
    /// the whole extraction). Default: `false`.
    pub ocr_inline_images: bool,
}

#[extendr]
impl PdfConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> PdfConfig {
        kreuzberg::PdfConfig::default().into()
    }
}

#[extendr]
pub fn new_pdfconfig(
    extract_images: Option<bool>,
    extract_tables: Option<bool>,
    passwords: Option<Vec<String>>,
    extract_metadata: Option<bool>,
    extract_annotations: Option<bool>,
    top_margin_fraction: Option<f64>,
    bottom_margin_fraction: Option<f64>,
    allow_single_column_tables: Option<bool>,
    ocr_inline_images: Option<bool>,
) -> PdfConfig {
    let mut __out = <PdfConfig>::default();
    if let Some(v) = extract_images {
        __out.extract_images = v;
    }
    if let Some(v) = extract_tables {
        __out.extract_tables = v;
    }
    if let Some(v) = passwords {
        __out.passwords = Some(v);
    }
    if let Some(v) = extract_metadata {
        __out.extract_metadata = v;
    }
    if let Some(v) = extract_annotations {
        __out.extract_annotations = v;
    }
    if let Some(v) = top_margin_fraction {
        __out.top_margin_fraction = Some(v);
    }
    if let Some(v) = bottom_margin_fraction {
        __out.bottom_margin_fraction = Some(v);
    }
    if let Some(v) = allow_single_column_tables {
        __out.allow_single_column_tables = v;
    }
    if let Some(v) = ocr_inline_images {
        __out.ocr_inline_images = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HierarchyConfig {
    /// Enable hierarchy extraction
    pub enabled: bool,
    /// Number of font size clusters to use for hierarchy levels (1-7)
    ///
    /// Default: 6, which provides H1-H6 heading levels with body text.
    /// Larger values create more fine-grained hierarchy levels.
    pub k_clusters: f64,
    /// Include bounding box information in hierarchy blocks
    pub include_bbox: bool,
    /// OCR coverage threshold for smart OCR triggering (0.0-1.0)
    ///
    /// Determines when OCR should be triggered based on text block coverage.
    /// OCR is triggered when text blocks cover less than this fraction of the page.
    /// Default: 0.5 (trigger OCR if less than 50% of page has text)
    pub ocr_coverage_threshold: Option<f64>,
}

#[extendr]
impl HierarchyConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> HierarchyConfig {
        kreuzberg::HierarchyConfig::default().into()
    }
}

#[extendr]
pub fn new_hierarchyconfig(
    enabled: Option<bool>,
    k_clusters: Option<f64>,
    include_bbox: Option<bool>,
    ocr_coverage_threshold: Option<f64>,
) -> HierarchyConfig {
    let mut __out = <HierarchyConfig>::default();
    if let Some(v) = enabled {
        __out.enabled = v;
    }
    if let Some(v) = k_clusters {
        __out.k_clusters = v;
    }
    if let Some(v) = include_bbox {
        __out.include_bbox = v;
    }
    if let Some(v) = ocr_coverage_threshold {
        __out.ocr_coverage_threshold = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PostProcessorConfig {
    /// Enable post-processors
    pub enabled: bool,
    /// Whitelist of processor names to run (None = all enabled)
    pub enabled_processors: Option<Vec<String>>,
    /// Blacklist of processor names to skip (None = none disabled)
    pub disabled_processors: Option<Vec<String>>,
    /// Pre-computed AHashSet for O(1) enabled processor lookup
    pub enabled_set: Option<Vec<String>>,
    /// Pre-computed AHashSet for O(1) disabled processor lookup
    pub disabled_set: Option<Vec<String>>,
}

#[extendr]
impl PostProcessorConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> PostProcessorConfig {
        kreuzberg::PostProcessorConfig::default().into()
    }
}

#[extendr]
pub fn new_postprocessorconfig(
    enabled: Option<bool>,
    enabled_processors: Option<Vec<String>>,
    disabled_processors: Option<Vec<String>>,
    enabled_set: Option<Vec<String>>,
    disabled_set: Option<Vec<String>>,
) -> PostProcessorConfig {
    let mut __out = <PostProcessorConfig>::default();
    if let Some(v) = enabled {
        __out.enabled = v;
    }
    if let Some(v) = enabled_processors {
        __out.enabled_processors = Some(v);
    }
    if let Some(v) = disabled_processors {
        __out.disabled_processors = Some(v);
    }
    if let Some(v) = enabled_set {
        __out.enabled_set = Some(v);
    }
    if let Some(v) = disabled_set {
        __out.disabled_set = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ChunkingConfig {
    /// Maximum size per chunk (in units determined by `sizing`).
    ///
    /// When `sizing` is `Characters` (default), this is the max character count.
    /// When using token-based sizing, this is the max token count.
    ///
    /// Default: 1000
    #[serde(rename = "max_chars")]
    pub max_characters: f64,
    /// Overlap between chunks (in units determined by `sizing`).
    ///
    /// Default: 200
    #[serde(rename = "max_overlap")]
    pub overlap: f64,
    /// Whether to trim whitespace from chunk boundaries.
    ///
    /// Default: true
    pub trim: bool,
    /// Type of chunker to use (Text or Markdown).
    ///
    /// Default: Text
    pub chunker_type: ChunkerType,
    /// Optional embedding configuration for chunk embeddings.
    pub embedding: Option<EmbeddingConfig>,
    /// Use a preset configuration (overrides individual settings if provided).
    pub preset: Option<String>,
    /// How to measure chunk size.
    ///
    /// Default: `Characters` (Unicode character count).
    /// Enable `chunking-tiktoken` or `chunking-tokenizers` features for token-based sizing.
    pub sizing: ChunkSizing,
    /// When `true` and `chunker_type` is `Markdown`, prepend the heading hierarchy
    /// path (e.g. `"# Title > ## Section\n\n"`) to each chunk's content string.
    ///
    /// This is useful for RAG pipelines where each chunk needs self-contained
    /// context about its position in the document structure.
    ///
    /// Default: `false`
    pub prepend_heading_context: bool,
    /// Optional cosine similarity threshold for semantic topic boundary detection.
    ///
    /// Only used when `chunker_type` is `Semantic` and an `EmbeddingConfig` is
    /// provided. You almost never need to set this. When omitted, defaults to
    /// `0.75` which works well for most documents. Lower values detect more
    /// topic boundaries (more, smaller chunks); higher values detect fewer.
    /// Range: `0.0..=1.0`.
    pub topic_threshold: Option<f64>,
}

#[extendr]
impl ChunkingConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> ChunkingConfig {
        kreuzberg::ChunkingConfig::default().into()
    }
}

#[extendr]
pub fn new_chunkingconfig(
    max_characters: Option<f64>,
    overlap: Option<f64>,
    trim: Option<bool>,
    chunker_type: Option<String>,
    preset: Option<String>,
    sizing: Option<String>,
    prepend_heading_context: Option<bool>,
    topic_threshold: Option<f64>,
) -> ChunkingConfig {
    let mut __out = <ChunkingConfig>::default();
    if let Some(v) = max_characters {
        __out.max_characters = v;
    }
    if let Some(v) = overlap {
        __out.overlap = v;
    }
    if let Some(v) = trim {
        __out.trim = v;
    }
    if let Some(v) = chunker_type {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.chunker_type = parsed;
        }
    }
    if let Some(v) = preset {
        __out.preset = Some(v);
    }
    if let Some(v) = sizing {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.sizing = parsed;
        }
    }
    if let Some(v) = prepend_heading_context {
        __out.prepend_heading_context = v;
    }
    if let Some(v) = topic_threshold {
        __out.topic_threshold = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EmbeddingConfig {
    /// The embedding model to use (defaults to "balanced" preset if not specified)
    pub model: EmbeddingModelType,
    /// Whether to normalize embedding vectors (recommended for cosine similarity)
    pub normalize: bool,
    /// Batch size for embedding generation
    pub batch_size: f64,
    /// Show model download progress
    pub show_download_progress: bool,
    /// Custom cache directory for model files
    ///
    /// Defaults to `~/.cache/kreuzberg/embeddings/` if not specified.
    /// Allows full customization of model download location.
    pub cache_dir: Option<String>,
    /// Hardware acceleration for the embedding ONNX model.
    ///
    /// When set, controls which execution provider (CPU, CUDA, CoreML, TensorRT)
    /// is used for inference. Defaults to `None` (auto-select per platform).
    pub acceleration: Option<AccelerationConfig>,
    /// Maximum wall-clock duration (in seconds) for a single `embed()` call when
    /// using [`EmbeddingModelType::Plugin`].
    ///
    /// Applies only to the in-process plugin path — protects against hung
    /// host-language backends (e.g. a Python callback deadlocked on the GIL,
    /// a model stuck on CUDA OOM retries, etc.). On timeout, the dispatcher
    /// returns `Plugin` instead of blocking forever.
    ///
    /// `None` disables the timeout. The default (60 seconds) is conservative
    /// for common in-process inference; increase for large batches on slow
    /// hardware.
    pub max_embed_duration_secs: Option<f64>,
}

#[extendr]
impl EmbeddingConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> EmbeddingConfig {
        kreuzberg::EmbeddingConfig::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<EmbeddingConfig> {
        let core: kreuzberg::EmbeddingConfig =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[extendr]
pub fn new_embeddingconfig(
    model: Option<String>,
    normalize: Option<bool>,
    batch_size: Option<f64>,
    show_download_progress: Option<bool>,
    cache_dir: Option<String>,
    max_embed_duration_secs: Option<f64>,
) -> EmbeddingConfig {
    let mut __out = <EmbeddingConfig>::default();
    if let Some(v) = model {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.model = parsed;
        }
    }
    if let Some(v) = normalize {
        __out.normalize = v;
    }
    if let Some(v) = batch_size {
        __out.batch_size = v;
    }
    if let Some(v) = show_download_progress {
        __out.show_download_progress = v;
    }
    if let Some(v) = cache_dir {
        __out.cache_dir = Some(v);
    }
    if let Some(v) = max_embed_duration_secs {
        __out.max_embed_duration_secs = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct TreeSitterConfig {
    /// Enable code intelligence processing (default: true).
    ///
    /// When `false`, tree-sitter analysis is completely skipped even if
    /// the config section is present.
    pub enabled: bool,
    /// Custom cache directory for downloaded grammars.
    ///
    /// When `None`, uses the default: `~/.cache/tree-sitter-language-pack/v{version}/libs/`.
    pub cache_dir: Option<String>,
    /// Languages to pre-download on init (e.g., `["python", "rust"]`).
    pub languages: Option<Vec<String>>,
    /// Language groups to pre-download (e.g., `["web", "systems", "scripting"]`).
    pub groups: Option<Vec<String>>,
    /// Processing options for code analysis.
    pub process: TreeSitterProcessConfig,
}

#[extendr]
impl TreeSitterConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> TreeSitterConfig {
        kreuzberg::TreeSitterConfig::default().into()
    }
}

#[extendr]
pub fn new_treesitterconfig(
    enabled: Option<bool>,
    cache_dir: Option<String>,
    languages: Option<Vec<String>>,
    groups: Option<Vec<String>>,
) -> TreeSitterConfig {
    let mut __out = <TreeSitterConfig>::default();
    if let Some(v) = enabled {
        __out.enabled = v;
    }
    if let Some(v) = cache_dir {
        __out.cache_dir = Some(v);
    }
    if let Some(v) = languages {
        __out.languages = Some(v);
    }
    if let Some(v) = groups {
        __out.groups = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct TreeSitterProcessConfig {
    /// Extract structural items (functions, classes, structs, etc.). Default: true.
    pub structure: bool,
    /// Extract import statements. Default: true.
    pub imports: bool,
    /// Extract export statements. Default: true.
    pub exports: bool,
    /// Extract comments. Default: false.
    pub comments: bool,
    /// Extract docstrings. Default: false.
    pub docstrings: bool,
    /// Extract symbol definitions. Default: false.
    pub symbols: bool,
    /// Include parse diagnostics. Default: false.
    pub diagnostics: bool,
    /// Maximum chunk size in bytes. `None` disables chunking.
    pub chunk_max_size: Option<f64>,
    /// Content rendering mode for code extraction.
    pub content_mode: CodeContentMode,
}

#[extendr]
impl TreeSitterProcessConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> TreeSitterProcessConfig {
        kreuzberg::TreeSitterProcessConfig::default().into()
    }
}

#[extendr]
pub fn new_treesitterprocessconfig(
    structure: Option<bool>,
    imports: Option<bool>,
    exports: Option<bool>,
    comments: Option<bool>,
    docstrings: Option<bool>,
    symbols: Option<bool>,
    diagnostics: Option<bool>,
    chunk_max_size: Option<f64>,
    content_mode: Option<String>,
) -> TreeSitterProcessConfig {
    let mut __out = <TreeSitterProcessConfig>::default();
    if let Some(v) = structure {
        __out.structure = v;
    }
    if let Some(v) = imports {
        __out.imports = v;
    }
    if let Some(v) = exports {
        __out.exports = v;
    }
    if let Some(v) = comments {
        __out.comments = v;
    }
    if let Some(v) = docstrings {
        __out.docstrings = v;
    }
    if let Some(v) = symbols {
        __out.symbols = v;
    }
    if let Some(v) = diagnostics {
        __out.diagnostics = v;
    }
    if let Some(v) = chunk_max_size {
        __out.chunk_max_size = Some(v);
    }
    if let Some(v) = content_mode {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.content_mode = parsed;
        }
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SupportedFormat {
    /// File extension (without leading dot), e.g., "pdf", "docx"
    pub extension: String,
    /// MIME type string, e.g., "application/pdf"
    pub mime_type: String,
}

#[extendr]
impl SupportedFormat {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct ServerConfig {
    /// Server host address (e.g., "127.0.0.1", "0.0.0.0")
    pub host: String,
    /// Server port number
    pub port: i32,
    /// CORS allowed origins. Empty vector means allow all origins.
    ///
    /// If this is an empty vector, the server will accept requests from any origin.
    /// If populated with specific origins (e.g., `"https://example.com"`), only
    /// those origins will be allowed.
    pub cors_origins: Vec<String>,
    /// Maximum size of request body in bytes (default: 100 MB)
    pub max_request_body_bytes: f64,
    /// Maximum size of multipart fields in bytes (default: 100 MB)
    pub max_multipart_field_bytes: f64,
}

#[extendr]
impl ServerConfig {
    pub fn listen_addr(&self) -> String {
        let core_self = kreuzberg::ServerConfig {
            host: self.host.clone(),

            port: self.port as u16,

            cors_origins: self.cors_origins.clone(),

            max_request_body_bytes: self.max_request_body_bytes as usize,

            max_multipart_field_bytes: self.max_multipart_field_bytes as usize,
        };
        core_self.listen_addr()
    }

    pub fn cors_allows_all(&self) -> bool {
        let core_self = kreuzberg::ServerConfig {
            host: self.host.clone(),

            port: self.port as u16,

            cors_origins: self.cors_origins.clone(),

            max_request_body_bytes: self.max_request_body_bytes as usize,

            max_multipart_field_bytes: self.max_multipart_field_bytes as usize,
        };
        core_self.cors_allows_all()
    }

    pub fn is_origin_allowed(&self, origin: String) -> bool {
        let core_self = kreuzberg::ServerConfig {
            host: self.host.clone(),

            port: self.port as u16,

            cors_origins: self.cors_origins.clone(),

            max_request_body_bytes: self.max_request_body_bytes as usize,

            max_multipart_field_bytes: self.max_multipart_field_bytes as usize,
        };
        core_self.is_origin_allowed(&origin)
    }

    pub fn max_request_body_mb(&self) -> f64 {
        let core_self = kreuzberg::ServerConfig {
            host: self.host.clone(),

            port: self.port as u16,

            cors_origins: self.cors_origins.clone(),

            max_request_body_bytes: self.max_request_body_bytes as usize,

            max_multipart_field_bytes: self.max_multipart_field_bytes as usize,
        };
        core_self.max_request_body_mb() as f64
    }

    pub fn max_multipart_field_mb(&self) -> f64 {
        let core_self = kreuzberg::ServerConfig {
            host: self.host.clone(),

            port: self.port as u16,

            cors_origins: self.cors_origins.clone(),

            max_request_body_bytes: self.max_request_body_bytes as usize,

            max_multipart_field_bytes: self.max_multipart_field_bytes as usize,
        };
        core_self.max_multipart_field_mb() as f64
    }

    #[allow(clippy::should_implement_trait)]
    pub fn default() -> ServerConfig {
        kreuzberg::ServerConfig::default().into()
    }
}

#[extendr]
pub fn new_serverconfig(
    host: Option<String>,
    port: Option<i32>,
    cors_origins: Option<Vec<String>>,
    max_request_body_bytes: Option<f64>,
    max_multipart_field_bytes: Option<f64>,
) -> ServerConfig {
    let mut __out = <ServerConfig>::default();
    if let Some(v) = host {
        __out.host = v;
    }
    if let Some(v) = port {
        __out.port = v;
    }
    if let Some(v) = cors_origins {
        __out.cors_origins = v;
    }
    if let Some(v) = max_request_body_bytes {
        __out.max_request_body_bytes = v;
    }
    if let Some(v) = max_multipart_field_bytes {
        __out.max_multipart_field_bytes = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct StructuredDataResult {
    pub content: String,
    pub format: String,
    pub metadata: HashMap<String, String>,
    pub text_fields: Vec<String>,
}

#[extendr]
impl StructuredDataResult {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DocxAppProperties {
    /// Application name (e.g., "Microsoft Office Word")
    pub application: Option<String>,
    /// Application version
    pub app_version: Option<String>,
    /// Template filename
    pub template: Option<String>,
    /// Total editing time in minutes
    pub total_time: Option<i32>,
    /// Number of pages
    pub pages: Option<i32>,
    /// Number of words
    pub words: Option<i32>,
    /// Number of characters (excluding spaces)
    pub characters: Option<i32>,
    /// Number of characters (including spaces)
    pub characters_with_spaces: Option<i32>,
    /// Number of lines
    pub lines: Option<i32>,
    /// Number of paragraphs
    pub paragraphs: Option<i32>,
    /// Company name
    pub company: Option<String>,
    /// Document security level
    pub doc_security: Option<i32>,
    /// Scale crop flag
    pub scale_crop: Option<bool>,
    /// Links up to date flag
    pub links_up_to_date: Option<bool>,
    /// Shared document flag
    pub shared_doc: Option<bool>,
    /// Hyperlinks changed flag
    pub hyperlinks_changed: Option<bool>,
}

#[extendr]
impl DocxAppProperties {}

#[extendr]
pub fn new_docxappproperties(
    application: Option<String>,
    app_version: Option<String>,
    template: Option<String>,
    total_time: Option<i32>,
    pages: Option<i32>,
    words: Option<i32>,
    characters: Option<i32>,
    characters_with_spaces: Option<i32>,
    lines: Option<i32>,
    paragraphs: Option<i32>,
    company: Option<String>,
    doc_security: Option<i32>,
    scale_crop: Option<bool>,
    links_up_to_date: Option<bool>,
    shared_doc: Option<bool>,
    hyperlinks_changed: Option<bool>,
) -> DocxAppProperties {
    let mut __out = <DocxAppProperties>::default();
    if let Some(v) = application {
        __out.application = Some(v);
    }
    if let Some(v) = app_version {
        __out.app_version = Some(v);
    }
    if let Some(v) = template {
        __out.template = Some(v);
    }
    if let Some(v) = total_time {
        __out.total_time = Some(v);
    }
    if let Some(v) = pages {
        __out.pages = Some(v);
    }
    if let Some(v) = words {
        __out.words = Some(v);
    }
    if let Some(v) = characters {
        __out.characters = Some(v);
    }
    if let Some(v) = characters_with_spaces {
        __out.characters_with_spaces = Some(v);
    }
    if let Some(v) = lines {
        __out.lines = Some(v);
    }
    if let Some(v) = paragraphs {
        __out.paragraphs = Some(v);
    }
    if let Some(v) = company {
        __out.company = Some(v);
    }
    if let Some(v) = doc_security {
        __out.doc_security = Some(v);
    }
    if let Some(v) = scale_crop {
        __out.scale_crop = Some(v);
    }
    if let Some(v) = links_up_to_date {
        __out.links_up_to_date = Some(v);
    }
    if let Some(v) = shared_doc {
        __out.shared_doc = Some(v);
    }
    if let Some(v) = hyperlinks_changed {
        __out.hyperlinks_changed = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct XlsxAppProperties {
    /// Application name (e.g., "Microsoft Excel")
    pub application: Option<String>,
    /// Application version
    pub app_version: Option<String>,
    /// Document security level
    pub doc_security: Option<i32>,
    /// Scale crop flag
    pub scale_crop: Option<bool>,
    /// Links up to date flag
    pub links_up_to_date: Option<bool>,
    /// Shared document flag
    pub shared_doc: Option<bool>,
    /// Hyperlinks changed flag
    pub hyperlinks_changed: Option<bool>,
    /// Company name
    pub company: Option<String>,
    /// Worksheet names
    pub worksheet_names: Vec<String>,
}

#[extendr]
impl XlsxAppProperties {}

#[extendr]
pub fn new_xlsxappproperties(
    application: Option<String>,
    app_version: Option<String>,
    doc_security: Option<i32>,
    scale_crop: Option<bool>,
    links_up_to_date: Option<bool>,
    shared_doc: Option<bool>,
    hyperlinks_changed: Option<bool>,
    company: Option<String>,
    worksheet_names: Option<Vec<String>>,
) -> XlsxAppProperties {
    let mut __out = <XlsxAppProperties>::default();
    if let Some(v) = application {
        __out.application = Some(v);
    }
    if let Some(v) = app_version {
        __out.app_version = Some(v);
    }
    if let Some(v) = doc_security {
        __out.doc_security = Some(v);
    }
    if let Some(v) = scale_crop {
        __out.scale_crop = Some(v);
    }
    if let Some(v) = links_up_to_date {
        __out.links_up_to_date = Some(v);
    }
    if let Some(v) = shared_doc {
        __out.shared_doc = Some(v);
    }
    if let Some(v) = hyperlinks_changed {
        __out.hyperlinks_changed = Some(v);
    }
    if let Some(v) = company {
        __out.company = Some(v);
    }
    if let Some(v) = worksheet_names {
        __out.worksheet_names = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PptxAppProperties {
    /// Application name (e.g., "Microsoft Office PowerPoint")
    pub application: Option<String>,
    /// Application version
    pub app_version: Option<String>,
    /// Total editing time in minutes
    pub total_time: Option<i32>,
    /// Company name
    pub company: Option<String>,
    /// Document security level
    pub doc_security: Option<i32>,
    /// Scale crop flag
    pub scale_crop: Option<bool>,
    /// Links up to date flag
    pub links_up_to_date: Option<bool>,
    /// Shared document flag
    pub shared_doc: Option<bool>,
    /// Hyperlinks changed flag
    pub hyperlinks_changed: Option<bool>,
    /// Number of slides
    pub slides: Option<i32>,
    /// Number of notes
    pub notes: Option<i32>,
    /// Number of hidden slides
    pub hidden_slides: Option<i32>,
    /// Number of multimedia clips
    pub multimedia_clips: Option<i32>,
    /// Presentation format (e.g., "Widescreen", "Standard")
    pub presentation_format: Option<String>,
    /// Slide titles
    pub slide_titles: Vec<String>,
}

#[extendr]
impl PptxAppProperties {}

#[extendr]
pub fn new_pptxappproperties(
    application: Option<String>,
    app_version: Option<String>,
    total_time: Option<i32>,
    company: Option<String>,
    doc_security: Option<i32>,
    scale_crop: Option<bool>,
    links_up_to_date: Option<bool>,
    shared_doc: Option<bool>,
    hyperlinks_changed: Option<bool>,
    slides: Option<i32>,
    notes: Option<i32>,
    hidden_slides: Option<i32>,
    multimedia_clips: Option<i32>,
    presentation_format: Option<String>,
    slide_titles: Option<Vec<String>>,
) -> PptxAppProperties {
    let mut __out = <PptxAppProperties>::default();
    if let Some(v) = application {
        __out.application = Some(v);
    }
    if let Some(v) = app_version {
        __out.app_version = Some(v);
    }
    if let Some(v) = total_time {
        __out.total_time = Some(v);
    }
    if let Some(v) = company {
        __out.company = Some(v);
    }
    if let Some(v) = doc_security {
        __out.doc_security = Some(v);
    }
    if let Some(v) = scale_crop {
        __out.scale_crop = Some(v);
    }
    if let Some(v) = links_up_to_date {
        __out.links_up_to_date = Some(v);
    }
    if let Some(v) = shared_doc {
        __out.shared_doc = Some(v);
    }
    if let Some(v) = hyperlinks_changed {
        __out.hyperlinks_changed = Some(v);
    }
    if let Some(v) = slides {
        __out.slides = Some(v);
    }
    if let Some(v) = notes {
        __out.notes = Some(v);
    }
    if let Some(v) = hidden_slides {
        __out.hidden_slides = Some(v);
    }
    if let Some(v) = multimedia_clips {
        __out.multimedia_clips = Some(v);
    }
    if let Some(v) = presentation_format {
        __out.presentation_format = Some(v);
    }
    if let Some(v) = slide_titles {
        __out.slide_titles = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct CoreProperties {
    /// Document title
    pub title: Option<String>,
    /// Document subject/topic
    pub subject: Option<String>,
    /// Document creator/author
    pub creator: Option<String>,
    /// Keywords or tags
    pub keywords: Option<String>,
    /// Document description/abstract
    pub description: Option<String>,
    /// User who last modified the document
    pub last_modified_by: Option<String>,
    /// Revision number
    pub revision: Option<String>,
    /// Creation timestamp (ISO 8601)
    pub created: Option<String>,
    /// Last modification timestamp (ISO 8601)
    pub modified: Option<String>,
    /// Document category
    pub category: Option<String>,
    /// Content status (Draft, Final, etc.)
    pub content_status: Option<String>,
    /// Document language
    pub language: Option<String>,
    /// Unique identifier
    pub identifier: Option<String>,
    /// Document version
    pub version: Option<String>,
    /// Last print timestamp (ISO 8601)
    pub last_printed: Option<String>,
}

#[extendr]
impl CoreProperties {}

#[extendr]
pub fn new_coreproperties(
    title: Option<String>,
    subject: Option<String>,
    creator: Option<String>,
    keywords: Option<String>,
    description: Option<String>,
    last_modified_by: Option<String>,
    revision: Option<String>,
    created: Option<String>,
    modified: Option<String>,
    category: Option<String>,
    content_status: Option<String>,
    language: Option<String>,
    identifier: Option<String>,
    version: Option<String>,
    last_printed: Option<String>,
) -> CoreProperties {
    let mut __out = <CoreProperties>::default();
    if let Some(v) = title {
        __out.title = Some(v);
    }
    if let Some(v) = subject {
        __out.subject = Some(v);
    }
    if let Some(v) = creator {
        __out.creator = Some(v);
    }
    if let Some(v) = keywords {
        __out.keywords = Some(v);
    }
    if let Some(v) = description {
        __out.description = Some(v);
    }
    if let Some(v) = last_modified_by {
        __out.last_modified_by = Some(v);
    }
    if let Some(v) = revision {
        __out.revision = Some(v);
    }
    if let Some(v) = created {
        __out.created = Some(v);
    }
    if let Some(v) = modified {
        __out.modified = Some(v);
    }
    if let Some(v) = category {
        __out.category = Some(v);
    }
    if let Some(v) = content_status {
        __out.content_status = Some(v);
    }
    if let Some(v) = language {
        __out.language = Some(v);
    }
    if let Some(v) = identifier {
        __out.identifier = Some(v);
    }
    if let Some(v) = version {
        __out.version = Some(v);
    }
    if let Some(v) = last_printed {
        __out.last_printed = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SecurityLimits {
    /// Maximum uncompressed size for archives (500 MB)
    pub max_archive_size: f64,
    /// Maximum compression ratio before flagging as potential bomb (100:1)
    pub max_compression_ratio: f64,
    /// Maximum number of files in archive (10,000)
    pub max_files_in_archive: f64,
    /// Maximum nesting depth for structures (100)
    pub max_nesting_depth: f64,
    /// Maximum length of any single XML entity / attribute / token (1 MiB).
    /// This is a per-token cap, NOT a total cap — billion-laughs class
    /// attacks where a single entity expands to hundreds of MB are caught
    /// here, while normal long text content (a paragraph, a CDATA block) is
    /// caught by `max_content_size` instead.
    pub max_entity_length: f64,
    /// Maximum string growth per document (100 MB)
    pub max_content_size: f64,
    /// Maximum iterations per operation
    pub max_iterations: f64,
    /// Maximum XML depth (100 levels)
    pub max_xml_depth: f64,
    /// Maximum cells per table (100,000)
    pub max_table_cells: f64,
}

#[extendr]
impl SecurityLimits {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> SecurityLimits {
        kreuzberg::SecurityLimits::default().into()
    }
}

#[extendr]
pub fn new_securitylimits(
    max_archive_size: Option<f64>,
    max_compression_ratio: Option<f64>,
    max_files_in_archive: Option<f64>,
    max_nesting_depth: Option<f64>,
    max_entity_length: Option<f64>,
    max_content_size: Option<f64>,
    max_iterations: Option<f64>,
    max_xml_depth: Option<f64>,
    max_table_cells: Option<f64>,
) -> SecurityLimits {
    let mut __out = <SecurityLimits>::default();
    if let Some(v) = max_archive_size {
        __out.max_archive_size = v;
    }
    if let Some(v) = max_compression_ratio {
        __out.max_compression_ratio = v;
    }
    if let Some(v) = max_files_in_archive {
        __out.max_files_in_archive = v;
    }
    if let Some(v) = max_nesting_depth {
        __out.max_nesting_depth = v;
    }
    if let Some(v) = max_entity_length {
        __out.max_entity_length = v;
    }
    if let Some(v) = max_content_size {
        __out.max_content_size = v;
    }
    if let Some(v) = max_iterations {
        __out.max_iterations = v;
    }
    if let Some(v) = max_xml_depth {
        __out.max_xml_depth = v;
    }
    if let Some(v) = max_table_cells {
        __out.max_table_cells = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct TokenReductionConfig {
    pub level: ReductionLevel,
    pub language_hint: Option<String>,
    pub preserve_markdown: bool,
    pub preserve_code: bool,
    pub semantic_threshold: f64,
    pub enable_parallel: bool,
    pub use_simd: bool,
    pub custom_stopwords: Option<HashMap<String, Vec<String>>>,
    pub preserve_patterns: Vec<String>,
    pub target_reduction: Option<f64>,
    pub enable_semantic_clustering: bool,
}

#[extendr]
impl TokenReductionConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> TokenReductionConfig {
        kreuzberg::TokenReductionConfig::default().into()
    }
}

#[extendr]
pub fn new_tokenreductionconfig(
    level: Option<String>,
    language_hint: Option<String>,
    preserve_markdown: Option<bool>,
    preserve_code: Option<bool>,
    semantic_threshold: Option<f64>,
    enable_parallel: Option<bool>,
    use_simd: Option<bool>,
    custom_stopwords: Option<HashMap<String, Vec<String>>>,
    preserve_patterns: Option<Vec<String>>,
    target_reduction: Option<f64>,
    enable_semantic_clustering: Option<bool>,
) -> TokenReductionConfig {
    let mut __out = <TokenReductionConfig>::default();
    if let Some(v) = level {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.level = parsed;
        }
    }
    if let Some(v) = language_hint {
        __out.language_hint = Some(v);
    }
    if let Some(v) = preserve_markdown {
        __out.preserve_markdown = v;
    }
    if let Some(v) = preserve_code {
        __out.preserve_code = v;
    }
    if let Some(v) = semantic_threshold {
        __out.semantic_threshold = v;
    }
    if let Some(v) = enable_parallel {
        __out.enable_parallel = v;
    }
    if let Some(v) = use_simd {
        __out.use_simd = v;
    }
    if let Some(v) = custom_stopwords {
        __out.custom_stopwords = Some(v);
    }
    if let Some(v) = preserve_patterns {
        __out.preserve_patterns = v;
    }
    if let Some(v) = target_reduction {
        __out.target_reduction = Some(v);
    }
    if let Some(v) = enable_semantic_clustering {
        __out.enable_semantic_clustering = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PdfAnnotation {
    /// The type of annotation.
    pub annotation_type: PdfAnnotationType,
    /// Text content of the annotation (e.g., comment text, link URL).
    pub content: Option<String>,
    /// Page number where the annotation appears (1-indexed).
    pub page_number: i32,
    /// Bounding box of the annotation on the page.
    pub bounding_box: Option<BoundingBox>,
}

#[extendr]
impl PdfAnnotation {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DjotContent {
    /// Plain text representation for backwards compatibility
    pub plain_text: String,
    /// Structured block-level content
    pub blocks: Vec<FormattedBlock>,
    /// Metadata from YAML frontmatter
    pub metadata: Metadata,
    /// Extracted tables as structured data
    pub tables: Vec<Table>,
    /// Extracted images with metadata
    pub images: Vec<DjotImage>,
    /// Extracted links with URLs
    pub links: Vec<DjotLink>,
    /// Footnote definitions
    pub footnotes: Vec<Footnote>,
    /// Attributes mapped by element identifier (if present)
    pub attributes: Vec<String>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct FormattedBlock {
    /// Type of block element
    pub block_type: BlockType,
    /// Heading level (1-6) for headings, or nesting level for lists
    pub level: Option<f64>,
    /// Inline content within the block
    pub inline_content: Vec<InlineElement>,
    /// Element attributes (classes, IDs, key-value pairs)
    pub attributes: Option<String>,
    /// Language identifier for code blocks
    pub language: Option<String>,
    /// Raw code content for code blocks
    pub code: Option<String>,
    /// Nested blocks for containers (blockquotes, list items, divs)
    pub children: Vec<FormattedBlock>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct InlineElement {
    /// Type of inline element
    pub element_type: InlineType,
    /// Text content
    pub content: String,
    /// Element attributes
    pub attributes: Option<String>,
    /// Additional metadata (e.g., href for links, src/alt for images)
    pub metadata: Option<HashMap<String, String>>,
}

#[extendr]
impl InlineElement {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DjotImage {
    /// Image source URL or path
    pub src: String,
    /// Alternative text
    pub alt: String,
    /// Optional title
    pub title: Option<String>,
    /// Element attributes
    pub attributes: Option<String>,
}

#[extendr]
impl DjotImage {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DjotLink {
    /// Link URL
    pub url: String,
    /// Link text content
    pub text: String,
    /// Optional title
    pub title: Option<String>,
    /// Element attributes
    pub attributes: Option<String>,
}

#[extendr]
impl DjotLink {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Footnote {
    /// Footnote label
    pub label: String,
    /// Footnote content blocks
    pub content: Vec<FormattedBlock>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DocumentStructure {
    /// All nodes in document/reading order.
    pub nodes: Vec<DocumentNode>,
    /// Origin format identifier (e.g. "docx", "pptx", "html", "pdf").
    ///
    /// Allows renderers to apply format-aware heuristics when converting
    /// the document tree to output formats.
    pub source_format: Option<String>,
    /// Resolved relationships between nodes (footnote refs, citations, anchor links, etc.).
    ///
    /// Populated during derivation from the internal document representation.
    /// Empty when no relationships are detected.
    pub relationships: Vec<DocumentRelationship>,
    /// Sorted, deduplicated list of node type names present in this document.
    ///
    /// Each value is the snake_case `node_type` tag of the corresponding
    /// [`NodeContent`] variant (e.g. `"paragraph"`, `"heading"`, `"table"`, …).
    ///
    /// Computed from [`nodes`] via [`DocumentStructure::finalize_node_types`].
    /// Empty until that method is called (internal construction paths call it
    /// at the end of derivation).
    pub node_types: Vec<String>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DocumentRelationship {
    /// Source node index (the referencing node).
    pub source: i32,
    /// Target node index (the referenced node).
    pub target: i32,
    /// Semantic kind of the relationship.
    pub kind: RelationshipKind,
}

#[extendr]
impl DocumentRelationship {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DocumentNode {
    /// Deterministic identifier (hash of content + position).
    pub id: String,
    /// Node content — tagged enum, type-specific data only.
    pub content: NodeContent,
    /// Parent node index (`None` = root-level node).
    pub parent: Option<i32>,
    /// Child node indices in reading order.
    pub children: Vec<i32>,
    /// Content layer classification.
    pub content_layer: ContentLayer,
    /// Page number where this node starts (1-indexed).
    pub page: Option<i32>,
    /// Page number where this node ends (for multi-page tables/sections).
    pub page_end: Option<i32>,
    /// Bounding box in document coordinates.
    pub bbox: Option<BoundingBox>,
    /// Inline annotations (formatting, links) on this node's text content.
    ///
    /// Only meaningful for text-carrying nodes; empty for containers.
    pub annotations: Vec<TextAnnotation>,
    /// Format-specific key-value attributes.
    ///
    /// Extensible bag for miscellaneous data without a dedicated typed field: CSS classes,
    /// LaTeX environment names, Excel cell formulas, slide layout names, etc.
    pub attributes: Option<HashMap<String, String>>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct TableGrid {
    /// Number of rows in the table.
    pub rows: i32,
    /// Number of columns in the table.
    pub cols: i32,
    /// All cells in row-major order.
    pub cells: Vec<GridCell>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct GridCell {
    /// Cell text content.
    pub content: String,
    /// Zero-indexed row position.
    pub row: i32,
    /// Zero-indexed column position.
    pub col: i32,
    /// Number of rows this cell spans.
    pub row_span: i32,
    /// Number of columns this cell spans.
    pub col_span: i32,
    /// Whether this is a header cell.
    pub is_header: bool,
    /// Bounding box for this cell (if available).
    pub bbox: Option<BoundingBox>,
}

#[extendr]
impl GridCell {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct TextAnnotation {
    /// Start byte offset in the node's text content (inclusive).
    pub start: i32,
    /// End byte offset in the node's text content (exclusive).
    pub end: i32,
    /// Annotation type.
    pub kind: AnnotationKind,
}

#[extendr]
impl TextAnnotation {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ExtractionResult {
    pub content: String,
    pub mime_type: String,
    pub metadata: Metadata,
    /// Extraction strategy used to produce the returned text.
    ///
    /// Populated when the extractor can reliably distinguish native text extraction,
    /// OCR-only extraction, or mixed native/OCR output.
    pub extraction_method: Option<ExtractionMethod>,
    pub tables: Vec<Table>,
    pub detected_languages: Option<Vec<String>>,
    /// Text chunks when chunking is enabled.
    ///
    /// When chunking configuration is provided, the content is split into
    /// overlapping chunks for efficient processing. Each chunk contains the text,
    /// optional embeddings (if enabled), and metadata about its position.
    pub chunks: Option<Vec<Chunk>>,
    /// Extracted images from the document.
    ///
    /// When image extraction is enabled via `ImageExtractionConfig`, this field
    /// contains all images found in the document with their raw data and metadata.
    /// Each image may optionally contain a nested `ocr_result` if OCR was performed.
    pub images: Option<Vec<ExtractedImage>>,
    /// Per-page content when page extraction is enabled.
    ///
    /// When page extraction is configured, the document is split into per-page content
    /// with tables and images mapped to their respective pages.
    pub pages: Option<Vec<PageContent>>,
    /// Semantic elements when element-based result format is enabled.
    ///
    /// When result_format is set to ElementBased, this field contains semantic
    /// elements with type classification, unique identifiers, and metadata for
    /// Unstructured-compatible element-based processing.
    pub elements: Option<Vec<Element>>,
    /// Rich Djot content structure (when extracting Djot documents).
    ///
    /// When extracting Djot documents with structured extraction enabled,
    /// this field contains the full semantic structure including:
    /// - Block-level elements with nesting
    /// - Inline formatting with attributes
    /// - Links, images, footnotes
    /// - Math expressions
    /// - Complete attribute information
    ///
    /// The `content` field still contains plain text for backward compatibility.
    ///
    /// Always `None` for non-Djot documents.
    pub djot_content: Option<DjotContent>,
    /// OCR elements with full spatial and confidence metadata.
    ///
    /// When OCR is performed with element extraction enabled, this field contains
    /// the structured representation of detected text including:
    /// - Bounding geometry (rectangles or quadrilaterals)
    /// - Confidence scores (detection and recognition)
    /// - Rotation information
    /// - Hierarchical relationships (Tesseract only)
    ///
    /// This field preserves all metadata that would otherwise be lost when
    /// converting to plain text or markdown output formats.
    ///
    /// Only populated when `OcrElementConfig.include_elements` is true.
    pub ocr_elements: Option<Vec<OcrElement>>,
    /// Structured document tree (when document structure extraction is enabled).
    ///
    /// When `include_document_structure` is true in `ExtractionConfig`, this field
    /// contains the full hierarchical representation of the document including:
    /// - Heading-driven section nesting
    /// - Table grids with cell-level metadata
    /// - Content layer classification (body, header, footer, footnote)
    /// - Inline text annotations (formatting, links)
    /// - Bounding boxes and page numbers
    ///
    /// Independent of `result_format` — can be combined with Unified or ElementBased.
    pub document: Option<DocumentStructure>,
    /// Document quality score from quality analysis.
    ///
    /// A value between 0.0 and 1.0 indicating the overall text quality.
    /// Previously stored in `metadata.additional["quality_score"]`.
    pub quality_score: Option<f64>,
    /// Non-fatal warnings collected during processing pipeline stages.
    ///
    /// Captures errors from optional pipeline features (embedding, chunking,
    /// language detection, output formatting) that don't prevent extraction
    /// but may indicate degraded results.
    /// Previously stored as individual keys in `metadata.additional`.
    pub processing_warnings: Vec<ProcessingWarning>,
    /// PDF annotations extracted from the document.
    ///
    /// When annotation extraction is enabled via `PdfConfig::extract_annotations`,
    /// this field contains text notes, highlights, links, stamps, and other
    /// annotations found in PDF documents.
    pub annotations: Option<Vec<PdfAnnotation>>,
    /// Nested extraction results from archive contents.
    ///
    /// When extracting archives, each processable file inside produces its own
    /// full extraction result. Set to `None` for non-archive formats.
    /// Use `max_archive_depth` in config to control recursion depth.
    pub children: Option<Vec<ArchiveEntry>>,
    /// URIs/links discovered during document extraction.
    ///
    /// Contains hyperlinks, image references, citations, email addresses, and
    /// other URI-like references found in the document. Always extracted when
    /// present in the source document.
    pub uris: Option<Vec<ExtractedUri>>,
    /// Tracked changes embedded in the source document.
    ///
    /// Populated by per-format extractors that understand change-tracking
    /// metadata (DOCX `w:ins`/`w:del`/`w:rPrChange`, ODT `text:change-*`,
    /// …). Every extractor defaults to `None` until its format-specific
    /// implementation is added. Extractors that do populate this field follow
    /// the "accepted-changes" convention: inserted text is present in
    /// `content`, deleted text is absent — the revision list is the separate
    /// audit trail.
    pub revisions: Option<Vec<DocumentRevision>>,
    /// Structured extraction output from LLM-based JSON schema extraction.
    ///
    /// When `structured_extraction` is configured in `ExtractionConfig`, the
    /// extracted document content is sent to a VLM with the provided JSON schema.
    /// The response is parsed and stored here as a JSON value matching the schema.
    pub structured_output: Option<String>,
    /// LLM token usage and cost data for all LLM calls made during this extraction.
    ///
    /// Contains one entry per LLM call. Multiple entries are produced when
    /// VLM OCR, structured extraction, or LLM embeddings run during
    /// the same extraction.
    ///
    /// `None` when no LLM was used.
    pub llm_usage: Option<Vec<LlmUsage>>,
    /// Pre-rendered content in the requested output format.
    ///
    /// Populated during `derive_extraction_result` before tree derivation consumes
    /// element data. `apply_output_format` swaps this into `content` at the end
    /// of the pipeline, after post-processors have operated on plain text.
    pub formatted_content: Option<String>,
    /// Structured hOCR document for the OCR+layout pipeline.
    ///
    /// When tesseract produces hOCR output, the parsed `InternalDocument` carries
    /// paragraph structure with bounding boxes and confidence scores. The layout
    /// classification step enriches these elements before final rendering.
    pub ocr_internal_document: Option<String>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ArchiveEntry {
    /// Archive-relative file path (e.g. "folder/document.pdf").
    pub path: String,
    /// Detected MIME type of the file.
    pub mime_type: String,
    /// Full extraction result for this file.
    pub result: ExtractionResult,
}

#[extendr]
impl ArchiveEntry {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ProcessingWarning {
    /// The pipeline stage or feature that produced this warning
    /// (e.g., "embedding", "chunking", "language_detection", "output_format").
    pub source: String,
    /// Human-readable description of what went wrong.
    pub message: String,
}

#[extendr]
impl ProcessingWarning {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct LlmUsage {
    /// The LLM model identifier (e.g. "openai/gpt-4o", "anthropic/claude-sonnet-4-20250514").
    pub model: String,
    /// The pipeline stage that triggered this LLM call
    /// (e.g. "vlm_ocr", "structured_extraction", "embeddings").
    pub source: String,
    /// Number of input/prompt tokens consumed.
    pub input_tokens: Option<f64>,
    /// Number of output/completion tokens generated.
    pub output_tokens: Option<f64>,
    /// Total tokens (input + output).
    pub total_tokens: Option<f64>,
    /// Estimated cost in USD based on the provider's published pricing.
    pub estimated_cost: Option<f64>,
    /// Why the model stopped generating (e.g. "stop", "length", "content_filter").
    pub finish_reason: Option<String>,
}

#[extendr]
impl LlmUsage {}

#[extendr]
pub fn new_llmusage(
    model: Option<String>,
    source: Option<String>,
    input_tokens: Option<f64>,
    output_tokens: Option<f64>,
    total_tokens: Option<f64>,
    estimated_cost: Option<f64>,
    finish_reason: Option<String>,
) -> LlmUsage {
    let mut __out = <LlmUsage>::default();
    if let Some(v) = model {
        __out.model = v;
    }
    if let Some(v) = source {
        __out.source = v;
    }
    if let Some(v) = input_tokens {
        __out.input_tokens = Some(v);
    }
    if let Some(v) = output_tokens {
        __out.output_tokens = Some(v);
    }
    if let Some(v) = total_tokens {
        __out.total_tokens = Some(v);
    }
    if let Some(v) = estimated_cost {
        __out.estimated_cost = Some(v);
    }
    if let Some(v) = finish_reason {
        __out.finish_reason = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Chunk {
    /// The text content of this chunk.
    pub content: String,
    /// Semantic structural classification of this chunk.
    ///
    /// Assigned by the heuristic classifier based on content patterns and
    /// heading context. Defaults to `ChunkType::Unknown` when no rule matches.
    pub chunk_type: ChunkType,
    /// Optional embedding vector for this chunk.
    ///
    /// Only populated when `EmbeddingConfig` is provided in chunking configuration.
    /// The dimensionality depends on the chosen embedding model.
    pub embedding: Option<Vec<f64>>,
    /// Metadata about this chunk's position and properties.
    pub metadata: ChunkMetadata,
}

#[extendr]
impl Chunk {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HeadingContext {
    /// The heading hierarchy from document root to this chunk's section.
    /// Index 0 is the outermost (h1), last element is the most specific.
    pub headings: Vec<HeadingLevel>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HeadingLevel {
    /// Heading depth (1 = h1, 2 = h2, etc.)
    pub level: i32,
    /// The text content of the heading.
    pub text: String,
}

#[extendr]
impl HeadingLevel {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ChunkMetadata {
    /// Byte offset where this chunk starts in the original text (UTF-8 valid boundary).
    pub byte_start: f64,
    /// Byte offset where this chunk ends in the original text (UTF-8 valid boundary).
    pub byte_end: f64,
    /// Number of tokens in this chunk (if available).
    ///
    /// This is calculated by the embedding model's tokenizer if embeddings are enabled.
    pub token_count: Option<f64>,
    /// Zero-based index of this chunk in the document.
    pub chunk_index: f64,
    /// Total number of chunks in the document.
    pub total_chunks: f64,
    /// First page number this chunk spans (1-indexed).
    ///
    /// Only populated when page tracking is enabled in extraction configuration.
    pub first_page: Option<i32>,
    /// Last page number this chunk spans (1-indexed, equal to first_page for single-page chunks).
    ///
    /// Only populated when page tracking is enabled in extraction configuration.
    pub last_page: Option<i32>,
    /// Heading context when using Markdown chunker.
    ///
    /// Contains the heading hierarchy this chunk falls under.
    /// Only populated when `ChunkerType::Markdown` is used.
    pub heading_context: Option<HeadingContext>,
    /// Indices into `ExtractionResult.images` for images on pages covered by this chunk.
    ///
    /// Contains zero-based indices into the top-level `images` collection for every
    /// image whose `page_number` falls within `[first_page, last_page]`.
    /// Empty when image extraction is disabled or the chunk spans no pages with images.
    pub image_indices: Vec<i32>,
}

#[extendr]
impl ChunkMetadata {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ExtractedImage {
    /// Raw image data (PNG, JPEG, WebP, etc. bytes).
    /// Uses `bytes::Bytes` for cheap cloning of large buffers.
    pub data: Vec<u8>,
    /// Image format (e.g., "jpeg", "png", "webp")
    /// Uses Cow<'static, str> to avoid allocation for static literals.
    pub format: String,
    /// Zero-indexed position of this image in the document/page
    pub image_index: i32,
    /// Page/slide number where image was found (1-indexed)
    pub page_number: Option<i32>,
    /// Image width in pixels
    pub width: Option<i32>,
    /// Image height in pixels
    pub height: Option<i32>,
    /// Colorspace information (e.g., "RGB", "CMYK", "Gray")
    pub colorspace: Option<String>,
    /// Bits per color component (e.g., 8, 16)
    pub bits_per_component: Option<i32>,
    /// Whether this image is a mask image
    pub is_mask: bool,
    /// Optional description of the image
    pub description: Option<String>,
    /// Nested OCR extraction result (if image was OCRed)
    ///
    /// When OCR is performed on this image, the result is embedded here
    /// rather than in a separate collection, making the relationship explicit.
    pub ocr_result: Option<ExtractionResult>,
    /// Bounding box of the image on the page (PDF coordinates: x0=left, y0=bottom, x1=right, y1=top).
    /// Only populated for PDF-extracted images when position data is available from the PDF extractor.
    pub bounding_box: Option<BoundingBox>,
    /// Original source path of the image within the document archive (e.g., "media/image1.png" in DOCX).
    /// Used for rendering image references when the binary data is not extracted.
    pub source_path: Option<String>,
    /// Heuristic classification of what this image likely depicts.
    /// `None` if classification was disabled or inconclusive.
    pub image_kind: Option<ImageKind>,
    /// Confidence score for `image_kind`, in the range 0.0 to 1.0.
    pub kind_confidence: Option<f64>,
    /// Identifier shared across images that form a single logical figure
    /// (e.g. all raster tiles of one technical drawing). `None` for singletons.
    pub cluster_id: Option<i32>,
}

#[extendr]
impl ExtractedImage {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct BoundingBox {
    /// Left x-coordinate
    pub x0: f64,
    /// Bottom y-coordinate
    pub y0: f64,
    /// Right x-coordinate
    pub x1: f64,
    /// Top y-coordinate
    pub y1: f64,
}

#[extendr]
impl BoundingBox {}

#[extendr]
pub fn new_boundingbox(x0: Option<f64>, y0: Option<f64>, x1: Option<f64>, y1: Option<f64>) -> BoundingBox {
    let mut __out = <BoundingBox>::default();
    if let Some(v) = x0 {
        __out.x0 = v;
    }
    if let Some(v) = y0 {
        __out.y0 = v;
    }
    if let Some(v) = x1 {
        __out.x1 = v;
    }
    if let Some(v) = y1 {
        __out.y1 = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ElementMetadata {
    /// Page number (1-indexed)
    pub page_number: Option<i32>,
    /// Source filename or document name
    pub filename: Option<String>,
    /// Bounding box coordinates if available
    pub coordinates: Option<BoundingBox>,
    /// Position index in the element sequence
    pub element_index: Option<f64>,
    /// Additional custom metadata
    pub additional: HashMap<String, String>,
}

#[extendr]
impl ElementMetadata {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Element {
    /// Unique element identifier
    pub element_id: String,
    /// Semantic type of this element
    pub element_type: ElementType,
    /// Text content of the element
    pub text: String,
    /// Metadata about the element
    pub metadata: ElementMetadata,
}

#[extendr]
impl Element {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ExcelWorkbook {
    /// All sheets in the workbook
    pub sheets: Vec<ExcelSheet>,
    /// Workbook-level metadata (author, creation date, etc.)
    pub metadata: HashMap<String, String>,
    /// Collaborative-edit revision headers from `xl/revisions/revisionHeaders.xml`.
    ///
    /// Populated for legacy shared-workbook `.xlsx` files that contain the
    /// `xl/revisions/` directory. Each `<header>` element maps to one
    /// `DocumentRevision { kind: FormatChange }` carrying the header's `guid`
    /// (→ `revision_id`), `userName` (→ `author`), and `dateTime` (→ `timestamp`).
    /// `anchor` and `delta` are `None`/empty for v1 (per-cell log parsing is a
    /// follow-up). `None` when `xl/revisions/revisionHeaders.xml` is absent.
    pub revisions: Option<Vec<DocumentRevision>>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct ExcelSheet {
    /// Sheet name as it appears in Excel
    pub name: String,
    /// Sheet content converted to Markdown tables
    pub markdown: String,
    /// Number of rows
    pub row_count: f64,
    /// Number of columns
    pub col_count: f64,
    /// Total number of non-empty cells
    pub cell_count: f64,
    /// Pre-extracted table cells (2D vector of cell values)
    /// Populated during markdown generation to avoid re-parsing markdown.
    /// None for empty sheets.
    pub table_cells: Option<Vec<Vec<String>>>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct XmlExtractionResult {
    /// Extracted text content (XML structure filtered out)
    pub content: String,
    /// Total number of XML elements processed
    pub element_count: f64,
    /// List of unique element names found (sorted)
    pub unique_elements: Vec<String>,
}

#[extendr]
impl XmlExtractionResult {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct TextExtractionResult {
    /// Extracted text content
    pub content: String,
    /// Number of lines
    pub line_count: f64,
    /// Number of words
    pub word_count: f64,
    /// Number of characters
    pub character_count: f64,
    /// Markdown headers (text only, Markdown files only)
    pub headers: Option<Vec<String>>,
    /// Markdown links as (text, URL) tuples (Markdown files only)
    pub links: Option<Vec<Vec<String>>>,
    /// Code blocks as (language, code) tuples (Markdown files only)
    pub code_blocks: Option<Vec<Vec<String>>>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PptxExtractionResult {
    /// Extracted text content from all slides
    pub content: String,
    /// Presentation metadata
    pub metadata: PptxMetadata,
    /// Total number of slides
    pub slide_count: f64,
    /// Total number of embedded images
    pub image_count: f64,
    /// Total number of tables
    pub table_count: f64,
    /// Extracted images from the presentation
    pub images: Vec<ExtractedImage>,
    /// Slide structure with boundaries (when page tracking is enabled)
    pub page_structure: Option<PageStructure>,
    /// Per-slide content (when page tracking is enabled)
    pub page_contents: Option<Vec<PageContent>>,
    /// Structured document representation
    pub document: Option<DocumentStructure>,
    /// Hyperlinks discovered in slides as (url, optional_label) pairs.
    pub hyperlinks: Vec<String>,
    /// Office metadata extracted from docProps/core.xml and docProps/app.xml.
    ///
    /// Contains keys like "title", "author", "created_by", "subject", "keywords",
    /// "modified_by", "created_at", "modified_at", etc.
    pub office_metadata: HashMap<String, String>,
    /// Slide comments as revisions.
    ///
    /// Each `<p:cm>` element in `ppt/comments/comment{N}.xml` becomes a
    /// `DocumentRevision { kind: Comment }` with author (resolved from
    /// `ppt/commentAuthors.xml`), ISO-8601 timestamp, and
    /// `RevisionAnchor::Slide { index }`. `None` when no comment XML parts exist.
    pub revisions: Option<Vec<DocumentRevision>>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct EmailExtractionResult {
    /// Email subject line
    pub subject: Option<String>,
    /// Sender email address
    pub from_email: Option<String>,
    /// Primary recipient email addresses
    pub to_emails: Vec<String>,
    /// CC recipient email addresses
    pub cc_emails: Vec<String>,
    /// BCC recipient email addresses
    pub bcc_emails: Vec<String>,
    /// Email date/timestamp
    pub date: Option<String>,
    /// Message-ID header value
    pub message_id: Option<String>,
    /// Plain text version of the email body
    pub plain_text: Option<String>,
    /// HTML version of the email body
    pub html_content: Option<String>,
    /// Cleaned/processed text content. Aliased as `cleaned_text` for back-compat.
    pub content: String,
    /// List of email attachments
    pub attachments: Vec<EmailAttachment>,
    /// Additional email headers and metadata
    pub metadata: HashMap<String, String>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EmailAttachment {
    /// Attachment name (from Content-Disposition header)
    pub name: Option<String>,
    /// Filename of the attachment
    pub filename: Option<String>,
    /// MIME type of the attachment
    pub mime_type: Option<String>,
    /// Size in bytes
    pub size: Option<f64>,
    /// Whether this attachment is an image
    pub is_image: bool,
    /// Attachment data (if extracted).
    /// Uses `bytes::Bytes` for cheap cloning of large buffers.
    pub data: Option<Vec<u8>>,
}

#[extendr]
impl EmailAttachment {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrExtractionResult {
    /// Recognized text content
    pub content: String,
    /// Original MIME type of the processed image
    pub mime_type: String,
    /// OCR processing metadata (confidence scores, language, etc.)
    pub metadata: HashMap<String, String>,
    /// Tables detected and extracted via OCR
    pub tables: Vec<OcrTable>,
    /// Structured OCR elements with bounding boxes and confidence scores.
    /// Available when TSV output is requested or table detection is enabled.
    pub ocr_elements: Option<Vec<OcrElement>>,
    /// Structured document produced from hOCR parsing.
    /// Carries paragraph structure, bounding boxes, and confidence scores
    /// that the flattened `content` string discards.
    pub internal_document: Option<String>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrTable {
    /// Table cells as a 2D vector (rows × columns)
    pub cells: Vec<Vec<String>>,
    /// Markdown representation of the table
    pub markdown: String,
    /// Page number where the table was found (1-indexed)
    pub page_number: i32,
    /// Bounding box of the table in pixel coordinates (from OCR word positions).
    pub bounding_box: Option<OcrTableBoundingBox>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrTableBoundingBox {
    /// Left x-coordinate (pixels)
    pub left: i32,
    /// Top y-coordinate (pixels)
    pub top: i32,
    /// Right x-coordinate (pixels)
    pub right: i32,
    /// Bottom y-coordinate (pixels)
    pub bottom: i32,
}

#[extendr]
impl OcrTableBoundingBox {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ImagePreprocessingConfig {
    /// Target DPI for the image (300 is standard, 600 for small text).
    pub target_dpi: i32,
    /// Auto-detect and correct image rotation.
    pub auto_rotate: bool,
    /// Correct skew (tilted images).
    pub deskew: bool,
    /// Remove noise from the image.
    pub denoise: bool,
    /// Enhance contrast for better text visibility.
    pub contrast_enhance: bool,
    /// Binarization method: "otsu", "sauvola", "adaptive".
    pub binarization_method: String,
    /// Invert colors (white text on black → black on white).
    pub invert_colors: bool,
}

#[extendr]
impl ImagePreprocessingConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> ImagePreprocessingConfig {
        kreuzberg::ImagePreprocessingConfig::default().into()
    }
}

#[extendr]
pub fn new_imagepreprocessingconfig(
    target_dpi: Option<i32>,
    auto_rotate: Option<bool>,
    deskew: Option<bool>,
    denoise: Option<bool>,
    contrast_enhance: Option<bool>,
    binarization_method: Option<String>,
    invert_colors: Option<bool>,
) -> ImagePreprocessingConfig {
    let mut __out = <ImagePreprocessingConfig>::default();
    if let Some(v) = target_dpi {
        __out.target_dpi = v;
    }
    if let Some(v) = auto_rotate {
        __out.auto_rotate = v;
    }
    if let Some(v) = deskew {
        __out.deskew = v;
    }
    if let Some(v) = denoise {
        __out.denoise = v;
    }
    if let Some(v) = contrast_enhance {
        __out.contrast_enhance = v;
    }
    if let Some(v) = binarization_method {
        __out.binarization_method = v;
    }
    if let Some(v) = invert_colors {
        __out.invert_colors = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct TesseractConfig {
    /// Language code (e.g., "eng", "deu", "fra")
    pub language: String,
    /// Page Segmentation Mode (0-13).
    ///
    /// Common values:
    /// - 3: Fully automatic page segmentation (native default)
    /// - 6: Assume a single uniform block of text (WASM default — avoids layout-analysis hang)
    /// - 11: Sparse text with no particular order
    pub psm: i32,
    /// Output format ("text" or "markdown")
    pub output_format: String,
    /// OCR Engine Mode (0-3).
    ///
    /// - 0: Legacy engine only
    /// - 1: Neural nets (LSTM) only (usually best)
    /// - 2: Legacy + LSTM
    /// - 3: Default (based on what's available)
    pub oem: i32,
    /// Minimum confidence threshold (0.0-100.0).
    ///
    /// Words with confidence below this threshold may be rejected or flagged.
    pub min_confidence: f64,
    /// Image preprocessing configuration.
    ///
    /// Controls how images are preprocessed before OCR. Can significantly
    /// improve quality for scanned documents or low-quality images.
    pub preprocessing: Option<ImagePreprocessingConfig>,
    /// Enable automatic table detection and reconstruction
    pub enable_table_detection: bool,
    /// Minimum confidence threshold for table detection (0.0-1.0)
    pub table_min_confidence: f64,
    /// Column threshold for table detection (pixels)
    pub table_column_threshold: i32,
    /// Row threshold ratio for table detection (0.0-1.0)
    pub table_row_threshold_ratio: f64,
    /// Enable OCR result caching
    pub use_cache: bool,
    /// Use pre-adapted templates for character classification
    pub classify_use_pre_adapted_templates: bool,
    /// Enable N-gram language model
    pub language_model_ngram_on: bool,
    /// Don't reject good words during block-level processing
    pub tessedit_dont_blkrej_good_wds: bool,
    /// Don't reject good words during row-level processing
    pub tessedit_dont_rowrej_good_wds: bool,
    /// Enable dictionary correction
    pub tessedit_enable_dict_correction: bool,
    /// Whitelist of allowed characters (empty = all allowed)
    pub tessedit_char_whitelist: String,
    /// Blacklist of forbidden characters (empty = none forbidden)
    pub tessedit_char_blacklist: String,
    /// Use primary language params model
    pub tessedit_use_primary_params_model: bool,
    /// Variable-width space detection
    pub textord_space_size_is_variable: bool,
    /// Use adaptive thresholding method
    pub thresholding_method: bool,
}

#[extendr]
impl TesseractConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> TesseractConfig {
        kreuzberg::TesseractConfig::default().into()
    }
}

#[extendr]
pub fn new_tesseractconfig(
    language: Option<String>,
    psm: Option<i32>,
    output_format: Option<String>,
    oem: Option<i32>,
    min_confidence: Option<f64>,
    enable_table_detection: Option<bool>,
    table_min_confidence: Option<f64>,
    table_column_threshold: Option<i32>,
    table_row_threshold_ratio: Option<f64>,
    use_cache: Option<bool>,
    classify_use_pre_adapted_templates: Option<bool>,
    language_model_ngram_on: Option<bool>,
    tessedit_dont_blkrej_good_wds: Option<bool>,
    tessedit_dont_rowrej_good_wds: Option<bool>,
    tessedit_enable_dict_correction: Option<bool>,
    tessedit_char_whitelist: Option<String>,
    tessedit_char_blacklist: Option<String>,
    tessedit_use_primary_params_model: Option<bool>,
    textord_space_size_is_variable: Option<bool>,
    thresholding_method: Option<bool>,
) -> TesseractConfig {
    let mut __out = <TesseractConfig>::default();
    if let Some(v) = language {
        __out.language = v;
    }
    if let Some(v) = psm {
        __out.psm = v;
    }
    if let Some(v) = output_format {
        __out.output_format = v;
    }
    if let Some(v) = oem {
        __out.oem = v;
    }
    if let Some(v) = min_confidence {
        __out.min_confidence = v;
    }
    if let Some(v) = enable_table_detection {
        __out.enable_table_detection = v;
    }
    if let Some(v) = table_min_confidence {
        __out.table_min_confidence = v;
    }
    if let Some(v) = table_column_threshold {
        __out.table_column_threshold = v;
    }
    if let Some(v) = table_row_threshold_ratio {
        __out.table_row_threshold_ratio = v;
    }
    if let Some(v) = use_cache {
        __out.use_cache = v;
    }
    if let Some(v) = classify_use_pre_adapted_templates {
        __out.classify_use_pre_adapted_templates = v;
    }
    if let Some(v) = language_model_ngram_on {
        __out.language_model_ngram_on = v;
    }
    if let Some(v) = tessedit_dont_blkrej_good_wds {
        __out.tessedit_dont_blkrej_good_wds = v;
    }
    if let Some(v) = tessedit_dont_rowrej_good_wds {
        __out.tessedit_dont_rowrej_good_wds = v;
    }
    if let Some(v) = tessedit_enable_dict_correction {
        __out.tessedit_enable_dict_correction = v;
    }
    if let Some(v) = tessedit_char_whitelist {
        __out.tessedit_char_whitelist = v;
    }
    if let Some(v) = tessedit_char_blacklist {
        __out.tessedit_char_blacklist = v;
    }
    if let Some(v) = tessedit_use_primary_params_model {
        __out.tessedit_use_primary_params_model = v;
    }
    if let Some(v) = textord_space_size_is_variable {
        __out.textord_space_size_is_variable = v;
    }
    if let Some(v) = thresholding_method {
        __out.thresholding_method = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ImagePreprocessingMetadata {
    /// Original image dimensions (width, height) in pixels
    pub original_dimensions: Vec<f64>,
    /// Original image DPI (horizontal, vertical)
    pub original_dpi: Vec<f64>,
    /// Target DPI from configuration
    pub target_dpi: i32,
    /// Scaling factor applied to the image
    pub scale_factor: f64,
    /// Whether DPI was auto-adjusted based on content
    pub auto_adjusted: bool,
    /// Final DPI after processing
    pub final_dpi: i32,
    /// New dimensions after resizing (if resized)
    pub new_dimensions: Option<Vec<f64>>,
    /// Resampling algorithm used ("LANCZOS3", "CATMULLROM", etc.)
    pub resample_method: String,
    /// Whether dimensions were clamped to max_image_dimension
    pub dimension_clamped: bool,
    /// Calculated optimal DPI (if auto_adjust_dpi enabled)
    pub calculated_dpi: Option<i32>,
    /// Whether resize was skipped (dimensions already optimal)
    pub skipped_resize: bool,
    /// Error message if resize failed
    pub resize_error: Option<String>,
}

#[extendr]
impl ImagePreprocessingMetadata {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct Metadata {
    /// Document title
    pub title: Option<String>,
    /// Document subject or description
    pub subject: Option<String>,
    /// Primary author(s) - always Vec for consistency
    pub authors: Option<Vec<String>>,
    /// Keywords/tags - always Vec for consistency
    pub keywords: Option<Vec<String>>,
    /// Primary language (ISO 639 code)
    pub language: Option<String>,
    /// Creation timestamp (ISO 8601 format)
    pub created_at: Option<String>,
    /// Last modification timestamp (ISO 8601 format)
    pub modified_at: Option<String>,
    /// User who created the document
    pub created_by: Option<String>,
    /// User who last modified the document
    pub modified_by: Option<String>,
    /// Page/slide/sheet structure with boundaries
    pub pages: Option<PageStructure>,
    /// Format-specific metadata (discriminated union)
    ///
    /// Contains detailed metadata specific to the document format.
    /// Serialized as a nested `"format"` object with a `format_type` discriminator field.
    pub format: Option<FormatMetadata>,
    /// Image preprocessing metadata (when OCR preprocessing was applied)
    pub image_preprocessing: Option<ImagePreprocessingMetadata>,
    /// JSON schema (for structured data extraction)
    pub json_schema: Option<String>,
    /// Error metadata (for batch operations)
    pub error: Option<ErrorMetadata>,
    /// Extraction duration in milliseconds (for benchmarking).
    ///
    /// This field is populated by batch extraction to provide per-file timing
    /// information. It's `None` for single-file extraction (which uses external timing).
    pub extraction_duration_ms: Option<f64>,
    /// Document category (from frontmatter or classification).
    pub category: Option<String>,
    /// Document tags (from frontmatter).
    pub tags: Option<Vec<String>>,
    /// Document version string (from frontmatter).
    pub document_version: Option<String>,
    /// Abstract or summary text (from frontmatter).
    pub abstract_text: Option<String>,
    /// Output format identifier (e.g., "markdown", "html", "text").
    ///
    /// Set by the output format pipeline stage when format conversion is applied.
    /// Previously stored in `metadata.additional["output_format"]`.
    pub output_format: Option<String>,
    /// Whether OCR was used during extraction.
    ///
    /// Set to `true` whenever the extraction pipeline ran an OCR backend
    /// (Tesseract, PaddleOCR, VLM, etc.) and used that output as the primary
    /// or fallback text. `false` means native text extraction was used exclusively.
    pub ocr_used: bool,
    /// Additional custom fields from postprocessors.
    ///
    /// Serialized as a nested `"additional"` object (not flattened at root level).
    /// Uses `Cow<'static, str>` keys so static string keys avoid allocation.
    pub additional: HashMap<String, String>,
}

#[extendr]
impl Metadata {
    pub fn is_empty(&self) -> bool {
        let core_self = kreuzberg::Metadata {
            title: self.title.clone(),

            subject: self.subject.clone(),

            authors: self.authors.clone(),

            keywords: self.keywords.clone(),

            language: self.language.clone(),

            created_at: self.created_at.clone(),

            modified_at: self.modified_at.clone(),

            created_by: self.created_by.clone(),

            modified_by: self.modified_by.clone(),

            pages: self.pages.clone().map(Into::into),

            format: Default::default(),
            image_preprocessing: self.image_preprocessing.clone().map(Into::into),

            json_schema: self.json_schema.as_ref().and_then(|s| serde_json::from_str(s).ok()),

            error: self.error.clone().map(Into::into),

            extraction_duration_ms: self.extraction_duration_ms.map(|v| v as u64),

            category: self.category.clone(),

            tags: self.tags.clone(),

            document_version: self.document_version.clone(),

            abstract_text: self.abstract_text.clone(),

            output_format: self.output_format.clone(),

            ocr_used: self.ocr_used,

            additional: self
                .additional
                .clone()
                .into_iter()
                .map(|(k, v)| {
                    (
                        k.into(),
                        serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v)),
                    )
                })
                .collect(),
        };
        core_self.is_empty()
    }
}

#[extendr]
pub fn new_metadata(
    title: Option<String>,
    subject: Option<String>,
    authors: Option<Vec<String>>,
    keywords: Option<Vec<String>>,
    language: Option<String>,
    created_at: Option<String>,
    modified_at: Option<String>,
    created_by: Option<String>,
    modified_by: Option<String>,
    format: Option<String>,
    json_schema: Option<String>,
    extraction_duration_ms: Option<f64>,
    category: Option<String>,
    tags: Option<Vec<String>>,
    document_version: Option<String>,
    abstract_text: Option<String>,
    output_format: Option<String>,
    ocr_used: Option<bool>,
    additional: Option<HashMap<String, String>>,
) -> Metadata {
    let mut __out = <Metadata>::default();
    if let Some(v) = title {
        __out.title = Some(v);
    }
    if let Some(v) = subject {
        __out.subject = Some(v);
    }
    if let Some(v) = authors {
        __out.authors = Some(v);
    }
    if let Some(v) = keywords {
        __out.keywords = Some(v);
    }
    if let Some(v) = language {
        __out.language = Some(v);
    }
    if let Some(v) = created_at {
        __out.created_at = Some(v);
    }
    if let Some(v) = modified_at {
        __out.modified_at = Some(v);
    }
    if let Some(v) = created_by {
        __out.created_by = Some(v);
    }
    if let Some(v) = modified_by {
        __out.modified_by = Some(v);
    }
    if let Some(v) = format {
        __out.format = serde_json::from_str(&format!("\"{v}\"")).ok();
    }
    if let Some(v) = json_schema {
        __out.json_schema = Some(v);
    }
    if let Some(v) = extraction_duration_ms {
        __out.extraction_duration_ms = Some(v);
    }
    if let Some(v) = category {
        __out.category = Some(v);
    }
    if let Some(v) = tags {
        __out.tags = Some(v);
    }
    if let Some(v) = document_version {
        __out.document_version = Some(v);
    }
    if let Some(v) = abstract_text {
        __out.abstract_text = Some(v);
    }
    if let Some(v) = output_format {
        __out.output_format = Some(v);
    }
    if let Some(v) = ocr_used {
        __out.ocr_used = v;
    }
    if let Some(v) = additional {
        __out.additional = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ExcelMetadata {
    /// Number of sheets in the workbook.
    pub sheet_count: Option<i32>,
    /// Names of all sheets in the workbook.
    pub sheet_names: Option<Vec<String>>,
}

#[extendr]
impl ExcelMetadata {}

#[extendr]
pub fn new_excelmetadata(sheet_count: Option<i32>, sheet_names: Option<Vec<String>>) -> ExcelMetadata {
    let mut __out = <ExcelMetadata>::default();
    if let Some(v) = sheet_count {
        __out.sheet_count = Some(v);
    }
    if let Some(v) = sheet_names {
        __out.sheet_names = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct EmailMetadata {
    /// Sender's email address
    pub from_email: Option<String>,
    /// Sender's display name
    pub from_name: Option<String>,
    /// Primary recipients
    pub to_emails: Vec<String>,
    /// CC recipients
    pub cc_emails: Vec<String>,
    /// BCC recipients
    pub bcc_emails: Vec<String>,
    /// Message-ID header value
    pub message_id: Option<String>,
    /// List of attachment filenames
    pub attachments: Vec<String>,
}

#[extendr]
impl EmailMetadata {}

#[extendr]
pub fn new_emailmetadata(
    from_email: Option<String>,
    from_name: Option<String>,
    to_emails: Option<Vec<String>>,
    cc_emails: Option<Vec<String>>,
    bcc_emails: Option<Vec<String>>,
    message_id: Option<String>,
    attachments: Option<Vec<String>>,
) -> EmailMetadata {
    let mut __out = <EmailMetadata>::default();
    if let Some(v) = from_email {
        __out.from_email = Some(v);
    }
    if let Some(v) = from_name {
        __out.from_name = Some(v);
    }
    if let Some(v) = to_emails {
        __out.to_emails = v;
    }
    if let Some(v) = cc_emails {
        __out.cc_emails = v;
    }
    if let Some(v) = bcc_emails {
        __out.bcc_emails = v;
    }
    if let Some(v) = message_id {
        __out.message_id = Some(v);
    }
    if let Some(v) = attachments {
        __out.attachments = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ArchiveMetadata {
    /// Archive format ("ZIP", "TAR", "7Z", etc.)
    pub format: String,
    /// Total number of files in the archive
    pub file_count: i32,
    /// List of file paths within the archive
    pub file_list: Vec<String>,
    /// Total uncompressed size in bytes
    pub total_size: f64,
    /// Compressed size in bytes (if available)
    pub compressed_size: Option<f64>,
}

#[extendr]
impl ArchiveMetadata {}

#[extendr]
pub fn new_archivemetadata(
    format: Option<String>,
    file_count: Option<i32>,
    file_list: Option<Vec<String>>,
    total_size: Option<f64>,
    compressed_size: Option<f64>,
) -> ArchiveMetadata {
    let mut __out = <ArchiveMetadata>::default();
    if let Some(v) = format {
        __out.format = v;
    }
    if let Some(v) = file_count {
        __out.file_count = v;
    }
    if let Some(v) = file_list {
        __out.file_list = v;
    }
    if let Some(v) = total_size {
        __out.total_size = v;
    }
    if let Some(v) = compressed_size {
        __out.compressed_size = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ImageMetadata {
    /// Image width in pixels
    pub width: i32,
    /// Image height in pixels
    pub height: i32,
    /// Image format (e.g., "PNG", "JPEG", "TIFF")
    pub format: String,
    /// EXIF metadata tags
    pub exif: HashMap<String, String>,
}

#[extendr]
impl ImageMetadata {}

#[extendr]
pub fn new_imagemetadata(
    width: Option<i32>,
    height: Option<i32>,
    format: Option<String>,
    exif: Option<HashMap<String, String>>,
) -> ImageMetadata {
    let mut __out = <ImageMetadata>::default();
    if let Some(v) = width {
        __out.width = v;
    }
    if let Some(v) = height {
        __out.height = v;
    }
    if let Some(v) = format {
        __out.format = v;
    }
    if let Some(v) = exif {
        __out.exif = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct XmlMetadata {
    /// Total number of XML elements processed
    pub element_count: i32,
    /// List of unique element tag names (sorted)
    pub unique_elements: Vec<String>,
}

#[extendr]
impl XmlMetadata {}

#[extendr]
pub fn new_xmlmetadata(element_count: Option<i32>, unique_elements: Option<Vec<String>>) -> XmlMetadata {
    let mut __out = <XmlMetadata>::default();
    if let Some(v) = element_count {
        __out.element_count = v;
    }
    if let Some(v) = unique_elements {
        __out.unique_elements = v;
    }
    __out
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct TextMetadata {
    /// Number of lines in the document
    pub line_count: i32,
    /// Number of words
    pub word_count: i32,
    /// Number of characters
    pub character_count: i32,
    /// Markdown headers (headings text only, for Markdown files)
    pub headers: Option<Vec<String>>,
    /// Markdown links as (text, url) tuples (for Markdown files)
    pub links: Option<Vec<Vec<String>>>,
    /// Code blocks as (language, code) tuples (for Markdown files)
    pub code_blocks: Option<Vec<Vec<String>>>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HeaderMetadata {
    /// Header level: 1 (h1) through 6 (h6)
    pub level: i32,
    /// Normalized text content of the header
    pub text: String,
    /// HTML id attribute if present
    pub id: Option<String>,
    /// Document tree depth at the header element
    pub depth: i32,
    /// Byte offset in original HTML document
    pub html_offset: i32,
}

#[extendr]
impl HeaderMetadata {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct LinkMetadata {
    /// The href URL value
    pub href: String,
    /// Link text content (normalized)
    pub text: String,
    /// Optional title attribute
    pub title: Option<String>,
    /// Link type classification
    pub link_type: LinkType,
    /// Rel attribute values
    pub rel: Vec<String>,
    /// Additional attributes as key-value pairs
    pub attributes: Vec<Vec<String>>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ImageMetadataType {
    /// Image source (URL, data URI, or SVG content)
    pub src: String,
    /// Alternative text from alt attribute
    pub alt: Option<String>,
    /// Title attribute
    pub title: Option<String>,
    /// Image dimensions as (width, height) if available
    pub dimensions: Option<Vec<i32>>,
    /// Image type classification
    pub image_type: ImageType,
    /// Additional attributes as key-value pairs
    pub attributes: Vec<Vec<String>>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct StructuredData {
    /// Type of structured data
    pub data_type: StructuredDataType,
    /// Raw JSON string representation
    pub raw_json: String,
    /// Schema type if detectable (e.g., "Article", "Event", "Product")
    pub schema_type: Option<String>,
}

#[extendr]
impl StructuredData {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HtmlMetadata {
    /// Document title from `<title>` tag
    pub title: Option<String>,
    /// Document description from `<meta name="description">` tag
    pub description: Option<String>,
    /// Document keywords from `<meta name="keywords">` tag, split on commas
    pub keywords: Vec<String>,
    /// Document author from `<meta name="author">` tag
    pub author: Option<String>,
    /// Canonical URL from `<link rel="canonical">` tag
    pub canonical_url: Option<String>,
    /// Base URL from `<base href="">` tag for resolving relative URLs
    pub base_href: Option<String>,
    /// Document language from `lang` attribute
    pub language: Option<String>,
    /// Document text direction from `dir` attribute
    pub text_direction: Option<TextDirection>,
    /// Open Graph metadata (og:* properties) for social media
    /// Keys like "title", "description", "image", "url", etc.
    pub open_graph: HashMap<String, String>,
    /// Twitter Card metadata (twitter:* properties)
    /// Keys like "card", "site", "creator", "title", "description", "image", etc.
    pub twitter_card: HashMap<String, String>,
    /// Additional meta tags not covered by specific fields
    /// Keys are meta name/property attributes, values are content
    pub meta_tags: HashMap<String, String>,
    /// Extracted header elements with hierarchy
    pub headers: Vec<HeaderMetadata>,
    /// Extracted hyperlinks with type classification
    pub links: Vec<LinkMetadata>,
    /// Extracted images with source and dimensions
    pub images: Vec<ImageMetadataType>,
    /// Extracted structured data blocks
    pub structured_data: Vec<StructuredData>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct OcrMetadata {
    /// OCR language code(s) used
    pub language: String,
    /// Tesseract Page Segmentation Mode (PSM)
    pub psm: i32,
    /// Output format (e.g., "text", "hocr")
    pub output_format: String,
    /// Number of tables detected
    pub table_count: i32,
    pub table_rows: Option<i32>,
    pub table_cols: Option<i32>,
}

#[extendr]
impl OcrMetadata {}

#[extendr]
pub fn new_ocrmetadata(
    language: Option<String>,
    psm: Option<i32>,
    output_format: Option<String>,
    table_count: Option<i32>,
    table_rows: Option<i32>,
    table_cols: Option<i32>,
) -> OcrMetadata {
    let mut __out = <OcrMetadata>::default();
    if let Some(v) = language {
        __out.language = v;
    }
    if let Some(v) = psm {
        __out.psm = v;
    }
    if let Some(v) = output_format {
        __out.output_format = v;
    }
    if let Some(v) = table_count {
        __out.table_count = v;
    }
    if let Some(v) = table_rows {
        __out.table_rows = Some(v);
    }
    if let Some(v) = table_cols {
        __out.table_cols = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ErrorMetadata {
    pub error_type: String,
    pub message: String,
}

#[extendr]
impl ErrorMetadata {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PptxMetadata {
    /// Total number of slides in the presentation
    pub slide_count: i32,
    /// Names of slides (if available)
    pub slide_names: Vec<String>,
    /// Number of embedded images
    pub image_count: Option<i32>,
    /// Number of tables
    pub table_count: Option<i32>,
}

#[extendr]
impl PptxMetadata {}

#[extendr]
pub fn new_pptxmetadata(
    slide_count: Option<i32>,
    slide_names: Option<Vec<String>>,
    image_count: Option<i32>,
    table_count: Option<i32>,
) -> PptxMetadata {
    let mut __out = <PptxMetadata>::default();
    if let Some(v) = slide_count {
        __out.slide_count = v;
    }
    if let Some(v) = slide_names {
        __out.slide_names = v;
    }
    if let Some(v) = image_count {
        __out.image_count = Some(v);
    }
    if let Some(v) = table_count {
        __out.table_count = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DocxMetadata {
    /// Core properties from docProps/core.xml (Dublin Core metadata)
    ///
    /// Contains title, creator, subject, keywords, dates, etc.
    /// Shared format across DOCX/PPTX/XLSX documents.
    pub core_properties: Option<CoreProperties>,
    /// Application properties from docProps/app.xml (Word-specific statistics)
    ///
    /// Contains word count, page count, paragraph count, editing time, etc.
    /// DOCX-specific variant of Office application properties.
    pub app_properties: Option<DocxAppProperties>,
    /// Custom properties from docProps/custom.xml (user-defined properties)
    ///
    /// Contains key-value pairs defined by users or applications.
    /// Values can be strings, numbers, booleans, or dates.
    pub custom_properties: Option<HashMap<String, String>>,
}

#[extendr]
impl DocxMetadata {}

#[extendr]
pub fn new_docxmetadata(custom_properties: Option<HashMap<String, String>>) -> DocxMetadata {
    let mut __out = <DocxMetadata>::default();
    if let Some(v) = custom_properties {
        __out.custom_properties = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CsvMetadata {
    pub row_count: i32,
    pub column_count: i32,
    pub delimiter: Option<String>,
    pub has_header: bool,
    pub column_types: Option<Vec<String>>,
}

#[extendr]
impl CsvMetadata {}

#[extendr]
pub fn new_csvmetadata(
    row_count: Option<i32>,
    column_count: Option<i32>,
    delimiter: Option<String>,
    has_header: Option<bool>,
    column_types: Option<Vec<String>>,
) -> CsvMetadata {
    let mut __out = <CsvMetadata>::default();
    if let Some(v) = row_count {
        __out.row_count = v;
    }
    if let Some(v) = column_count {
        __out.column_count = v;
    }
    if let Some(v) = delimiter {
        __out.delimiter = Some(v);
    }
    if let Some(v) = has_header {
        __out.has_header = v;
    }
    if let Some(v) = column_types {
        __out.column_types = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct BibtexMetadata {
    /// Number of entries in the bibliography.
    pub entry_count: f64,
    pub citation_keys: Vec<String>,
    pub authors: Vec<String>,
    pub year_range: Option<YearRange>,
    pub entry_types: Option<HashMap<String, f64>>,
}

#[extendr]
impl BibtexMetadata {}

#[extendr]
pub fn new_bibtexmetadata(
    entry_count: Option<f64>,
    citation_keys: Option<Vec<String>>,
    authors: Option<Vec<String>>,
    entry_types: Option<HashMap<String, f64>>,
) -> BibtexMetadata {
    let mut __out = <BibtexMetadata>::default();
    if let Some(v) = entry_count {
        __out.entry_count = v;
    }
    if let Some(v) = citation_keys {
        __out.citation_keys = v;
    }
    if let Some(v) = authors {
        __out.authors = v;
    }
    if let Some(v) = entry_types {
        __out.entry_types = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CitationMetadata {
    pub citation_count: f64,
    pub format: Option<String>,
    pub authors: Vec<String>,
    pub year_range: Option<YearRange>,
    pub dois: Vec<String>,
    pub keywords: Vec<String>,
}

#[extendr]
impl CitationMetadata {}

#[extendr]
pub fn new_citationmetadata(
    citation_count: Option<f64>,
    format: Option<String>,
    authors: Option<Vec<String>>,
    dois: Option<Vec<String>>,
    keywords: Option<Vec<String>>,
) -> CitationMetadata {
    let mut __out = <CitationMetadata>::default();
    if let Some(v) = citation_count {
        __out.citation_count = v;
    }
    if let Some(v) = format {
        __out.format = Some(v);
    }
    if let Some(v) = authors {
        __out.authors = v;
    }
    if let Some(v) = dois {
        __out.dois = v;
    }
    if let Some(v) = keywords {
        __out.keywords = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct YearRange {
    pub min: Option<i32>,
    pub max: Option<i32>,
    pub years: Vec<i32>,
}

#[extendr]
impl YearRange {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct FictionBookMetadata {
    pub genres: Vec<String>,
    pub sequences: Vec<String>,
    pub annotation: Option<String>,
}

#[extendr]
impl FictionBookMetadata {}

#[extendr]
pub fn new_fictionbookmetadata(
    genres: Option<Vec<String>>,
    sequences: Option<Vec<String>>,
    annotation: Option<String>,
) -> FictionBookMetadata {
    let mut __out = <FictionBookMetadata>::default();
    if let Some(v) = genres {
        __out.genres = v;
    }
    if let Some(v) = sequences {
        __out.sequences = v;
    }
    if let Some(v) = annotation {
        __out.annotation = Some(v);
    }
    __out
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DbfMetadata {
    pub record_count: f64,
    pub field_count: f64,
    pub fields: Vec<DbfFieldInfo>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DbfFieldInfo {
    pub name: String,
    pub field_type: String,
}

#[extendr]
impl DbfFieldInfo {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct JatsMetadata {
    pub copyright: Option<String>,
    pub license: Option<String>,
    pub history_dates: HashMap<String, String>,
    pub contributor_roles: Vec<ContributorRole>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ContributorRole {
    pub name: String,
    pub role: Option<String>,
}

#[extendr]
impl ContributorRole {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EpubMetadata {
    pub coverage: Option<String>,
    pub dc_format: Option<String>,
    pub relation: Option<String>,
    pub source: Option<String>,
    pub dc_type: Option<String>,
    pub cover_image: Option<String>,
}

#[extendr]
impl EpubMetadata {}

#[extendr]
pub fn new_epubmetadata(
    coverage: Option<String>,
    dc_format: Option<String>,
    relation: Option<String>,
    source: Option<String>,
    dc_type: Option<String>,
    cover_image: Option<String>,
) -> EpubMetadata {
    let mut __out = <EpubMetadata>::default();
    if let Some(v) = coverage {
        __out.coverage = Some(v);
    }
    if let Some(v) = dc_format {
        __out.dc_format = Some(v);
    }
    if let Some(v) = relation {
        __out.relation = Some(v);
    }
    if let Some(v) = source {
        __out.source = Some(v);
    }
    if let Some(v) = dc_type {
        __out.dc_type = Some(v);
    }
    if let Some(v) = cover_image {
        __out.cover_image = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PstMetadata {
    pub message_count: f64,
}

#[extendr]
impl PstMetadata {}

#[extendr]
pub fn new_pstmetadata(message_count: Option<f64>) -> PstMetadata {
    let mut __out = <PstMetadata>::default();
    if let Some(v) = message_count {
        __out.message_count = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrConfidence {
    /// Detection confidence: how confident the OCR engine is that text exists here.
    ///
    /// PaddleOCR provides this as `box_score`, Tesseract doesn't have a direct equivalent.
    /// Range: 0.0 to 1.0 (or None if not available).
    pub detection: Option<f64>,
    /// Recognition confidence: how confident about the text content.
    ///
    /// Range: 0.0 to 1.0.
    pub recognition: f64,
}

#[extendr]
impl OcrConfidence {}

#[extendr]
pub fn new_ocrconfidence(detection: Option<f64>, recognition: Option<f64>) -> OcrConfidence {
    let mut __out = <OcrConfidence>::default();
    if let Some(v) = detection {
        __out.detection = Some(v);
    }
    if let Some(v) = recognition {
        __out.recognition = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrRotation {
    /// Rotation angle in degrees (0, 90, 180, 270 for PaddleOCR).
    pub angle_degrees: f64,
    /// Confidence score for the rotation detection.
    pub confidence: Option<f64>,
}

#[extendr]
impl OcrRotation {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrElement {
    /// The recognized text content.
    pub text: String,
    /// Bounding geometry (rectangle or quadrilateral).
    pub geometry: OcrBoundingGeometry,
    /// Confidence scores for detection and recognition.
    pub confidence: OcrConfidence,
    /// Hierarchical level (word, line, block, page).
    pub level: OcrElementLevel,
    /// Rotation information (if detected).
    pub rotation: Option<OcrRotation>,
    /// Page number (1-indexed).
    pub page_number: i32,
    /// Parent element ID for hierarchical relationships.
    ///
    /// Only used for Tesseract output which has word -> line -> block hierarchy.
    pub parent_id: Option<String>,
    /// Backend-specific metadata that doesn't fit the unified schema.
    pub backend_metadata: HashMap<String, String>,
}

#[extendr]
impl OcrElement {}

#[extendr]
pub fn new_ocrelement(
    text: Option<String>,
    geometry: Option<String>,
    level: Option<String>,
    page_number: Option<i32>,
    parent_id: Option<String>,
    backend_metadata: Option<HashMap<String, String>>,
) -> OcrElement {
    let mut __out = <OcrElement>::default();
    if let Some(v) = text {
        __out.text = v;
    }
    if let Some(v) = geometry {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.geometry = parsed;
        }
    }
    if let Some(v) = level {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.level = parsed;
        }
    }
    if let Some(v) = page_number {
        __out.page_number = v;
    }
    if let Some(v) = parent_id {
        __out.parent_id = Some(v);
    }
    if let Some(v) = backend_metadata {
        __out.backend_metadata = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OcrElementConfig {
    /// Whether to include OCR elements in the extraction result.
    ///
    /// When true, the `ocr_elements` field in `ExtractionResult` will be populated.
    pub include_elements: bool,
    /// Minimum hierarchical level to include.
    ///
    /// Elements below this level (e.g., words when min_level is Line) will be excluded.
    pub min_level: OcrElementLevel,
    /// Minimum recognition confidence threshold (0.0-1.0).
    ///
    /// Elements with confidence below this threshold will be filtered out.
    pub min_confidence: f64,
    /// Whether to build hierarchical relationships between elements.
    ///
    /// When true, `parent_id` fields will be populated based on spatial containment.
    /// Only meaningful for Tesseract output.
    pub build_hierarchy: bool,
}

#[extendr]
impl OcrElementConfig {}

#[extendr]
pub fn new_ocrelementconfig(
    include_elements: Option<bool>,
    min_level: Option<String>,
    min_confidence: Option<f64>,
    build_hierarchy: Option<bool>,
) -> OcrElementConfig {
    let mut __out = <OcrElementConfig>::default();
    if let Some(v) = include_elements {
        __out.include_elements = v;
    }
    if let Some(v) = min_level {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.min_level = parsed;
        }
    }
    if let Some(v) = min_confidence {
        __out.min_confidence = v;
    }
    if let Some(v) = build_hierarchy {
        __out.build_hierarchy = v;
    }
    __out
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PageStructure {
    /// Total number of pages/slides/sheets
    pub total_count: i32,
    /// Type of paginated unit
    pub unit_type: PageUnitType,
    /// Character offset boundaries for each page
    ///
    /// Maps character ranges in the extracted content to page numbers.
    /// Used for chunk page range calculation.
    pub boundaries: Option<Vec<PageBoundary>>,
    /// Detailed per-page metadata (optional, only when needed)
    pub pages: Option<Vec<PageInfo>>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PageBoundary {
    /// Byte offset where this page starts in the content string (UTF-8 valid boundary, inclusive)
    pub byte_start: f64,
    /// Byte offset where this page ends in the content string (UTF-8 valid boundary, exclusive)
    pub byte_end: f64,
    /// Page number (1-indexed)
    pub page_number: i32,
}

#[extendr]
impl PageBoundary {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PageInfo {
    /// Page number (1-indexed)
    pub number: i32,
    /// Page title (usually for presentations)
    pub title: Option<String>,
    /// Dimensions in points (PDF) or pixels (images): (width, height)
    pub dimensions: Option<Vec<f64>>,
    /// Number of images on this page
    pub image_count: Option<i32>,
    /// Number of tables on this page
    pub table_count: Option<i32>,
    /// Whether this page is hidden (e.g., in presentations)
    pub hidden: Option<bool>,
    /// Whether this page is blank (no meaningful text, no images, no tables)
    ///
    /// A page is considered blank if it has fewer than 3 non-whitespace characters
    /// and contains no tables or images. This is useful for filtering out empty pages
    /// in scanned documents or PDFs with blank separator pages.
    pub is_blank: Option<bool>,
    /// Whether this page contains non-trivial vector graphics (paths, shapes, curves)
    ///
    /// Indicates the presence of vector-drawn content such as charts, diagrams,
    /// or geometric shapes (e.g., from Adobe InDesign, LaTeX TikZ). These are
    /// invisible to `ExtractionResult.images` since they are not embedded as raster
    /// XObjects. Set to `true` when path count exceeds a heuristic threshold,
    /// signaling that downstream consumers may want to rasterize the page to
    /// capture this content.
    ///
    /// Only populated for PDFs; `None` for other document types.
    pub has_vector_graphics: bool,
}

#[extendr]
impl PageInfo {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PageContent {
    /// Page number (1-indexed)
    pub page_number: i32,
    /// Text content for this page
    pub content: String,
    /// Tables found on this page (uses Arc for memory efficiency)
    ///
    /// Serializes as Vec<Table> for JSON compatibility while maintaining
    /// Arc semantics in-memory for zero-copy sharing.
    pub tables: Vec<Table>,
    /// Indices into `ExtractionResult.images` for images found on this page.
    ///
    /// Each value is a zero-based index into the top-level `images` collection.
    /// Only populated when `extract_images = true` in the extraction config.
    pub image_indices: Vec<i32>,
    /// Hierarchy information for the page (when hierarchy extraction is enabled)
    ///
    /// Contains text hierarchy levels (H1-H6) extracted from the page content.
    pub hierarchy: Option<PageHierarchy>,
    /// Whether this page is blank (no meaningful text content)
    ///
    /// Determined during extraction based on text content analysis.
    /// A page is blank if it has fewer than 3 non-whitespace characters
    /// and contains no tables or images.
    pub is_blank: Option<bool>,
    /// Layout detection regions for this page (when layout detection is enabled).
    ///
    /// Contains detected layout regions with class, confidence, bounding box,
    /// and area fraction. Only populated when layout detection is configured.
    pub layout_regions: Option<Vec<LayoutRegion>>,
    /// Speaker notes for this slide (PPTX only).
    ///
    /// Contains the text from the slide's notes pane (`ppt/notesSlides/notesSlide{N}.xml`).
    /// Only populated when the source is a PPTX file and notes are present.
    pub speaker_notes: Option<String>,
    /// Section name this slide belongs to (PPTX only).
    ///
    /// PowerPoint sections group slides into logical chapters (`<p:sectionLst>` in
    /// `ppt/presentation.xml`). Only populated when the source is a PPTX file and
    /// the slide belongs to a named section.
    pub section_name: Option<String>,
    /// Sheet name for this page (XLSX/ODS only).
    ///
    /// Each spreadsheet sheet maps to one `PageContent` entry. This field carries the
    /// sheet's display name as it appears in the workbook. `None` for all non-spreadsheet
    /// formats and for sheets with an empty name.
    pub sheet_name: Option<String>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct LayoutRegion {
    /// Layout class name (e.g. "picture", "table", "text", "section_header").
    pub class_name: String,
    /// Confidence score from the layout detection model (0.0 to 1.0).
    pub confidence: f64,
    /// Bounding box in document coordinate space.
    pub bounding_box: BoundingBox,
    /// Fraction of the page area covered by this region (0.0 to 1.0).
    pub area_fraction: f64,
}

#[extendr]
impl LayoutRegion {}

#[extendr]
pub fn new_layoutregion(
    class_name: Option<String>,
    confidence: Option<f64>,
    area_fraction: Option<f64>,
) -> LayoutRegion {
    let mut __out = <LayoutRegion>::default();
    if let Some(v) = class_name {
        __out.class_name = v;
    }
    if let Some(v) = confidence {
        __out.confidence = v;
    }
    if let Some(v) = area_fraction {
        __out.area_fraction = v;
    }
    __out
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PageHierarchy {
    /// Number of hierarchy blocks on this page
    pub block_count: i32,
    /// Hierarchical blocks with heading levels
    pub blocks: Vec<HierarchicalBlock>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct HierarchicalBlock {
    /// The text content of this block
    pub text: String,
    /// The font size of the text in this block
    pub font_size: f64,
    /// The hierarchy level of this block (H1-H6 or Body)
    ///
    /// Levels correspond to HTML heading tags:
    /// - "h1": Top-level heading
    /// - "h2": Secondary heading
    /// - "h3": Tertiary heading
    /// - "h4": Quaternary heading
    /// - "h5": Quinary heading
    /// - "h6": Senary heading
    /// - "body": Body text (no heading level)
    pub level: String,
    /// Bounding box information for the block
    ///
    /// Contains coordinates as (left, top, right, bottom) in PDF units.
    pub bbox: Option<Vec<f64>>,
}

#[extendr]
impl HierarchicalBlock {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct CellChange {
    /// Zero-based row index.
    pub row: f64,
    /// Zero-based column index.
    pub col: f64,
    /// Value before the change.
    pub from: String,
    /// Value after the change.
    pub to: String,
}

#[extendr]
impl CellChange {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct DocumentRevision {
    /// Format-specific revision identifier.
    ///
    /// For DOCX this is the `w:id` attribute value on the change element
    /// (e.g. `"42"`). When the attribute is absent a synthetic fallback is
    /// generated (`"docx-ins-0"`, `"docx-del-3"`, …).
    pub revision_id: String,
    /// Display name of the author who made this change, when available.
    pub author: Option<String>,
    /// ISO-8601 timestamp of the change, when available.
    ///
    /// Stored as a plain string so this type remains FFI-friendly and
    /// unconditionally available without the `chrono` optional dep.
    /// DOCX populates this from the `w:date` attribute (e.g.
    /// `"2024-03-15T10:30:00Z"`).
    pub timestamp: Option<String>,
    /// Semantic kind of this revision.
    pub kind: RevisionKind,
    /// Best-effort document location for this revision.
    ///
    /// Resolution is format-dependent and may be `None` when the location
    /// cannot be determined (e.g. changes inside table cells before
    /// table-cell anchor support is added).
    pub anchor: Option<RevisionAnchor>,
    /// The content changes that make up this revision.
    pub delta: RevisionDelta,
}

#[extendr]
impl DocumentRevision {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct RevisionDelta {
    /// Line-level content changes for this revision.
    pub content: Vec<DiffLine>,
    /// Cell-level table changes for this revision.
    pub table_changes: Vec<CellChange>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Table {
    /// Table cells as a 2D vector (rows × columns)
    pub cells: Vec<Vec<String>>,
    /// Markdown representation of the table
    pub markdown: String,
    /// Page number where the table was found (1-indexed)
    pub page_number: i32,
    /// Bounding box of the table on the page (PDF coordinates: x0=left, y0=bottom, x1=right, y1=top).
    /// Only populated for PDF-extracted tables when position data is available.
    pub bounding_box: Option<BoundingBox>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct TableCell {
    /// Cell content as text
    pub content: String,
    /// Row span (number of rows this cell spans)
    pub row_span: i32,
    /// Column span (number of columns this cell spans)
    pub col_span: i32,
    /// Whether this is a header cell
    pub is_header: bool,
}

#[extendr]
impl TableCell {}

#[extendr]
pub fn new_tablecell(
    content: Option<String>,
    row_span: Option<i32>,
    col_span: Option<i32>,
    is_header: Option<bool>,
) -> TableCell {
    let mut __out = <TableCell>::default();
    if let Some(v) = content {
        __out.content = v;
    }
    if let Some(v) = row_span {
        __out.row_span = v;
    }
    if let Some(v) = col_span {
        __out.col_span = v;
    }
    if let Some(v) = is_header {
        __out.is_header = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ExtractedUri {
    /// The URL or path string.
    pub url: String,
    /// Optional display text / label for the link.
    pub label: Option<String>,
    /// Optional page number where the URI was found (1-indexed).
    pub page: Option<i32>,
    /// Semantic classification of the URI.
    pub kind: UriKind,
}

#[extendr]
impl ExtractedUri {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DetectResponse {
    /// Detected MIME type
    pub mime_type: String,
    /// Original filename (if provided)
    pub filename: Option<String>,
}

#[extendr]
impl DetectResponse {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DiffOptions {
    /// Include metadata changes in the diff. Default: `true`.
    pub include_metadata: bool,
    /// Include embedded-children changes in the diff. Default: `true`.
    pub include_embedded: bool,
    /// Truncate content to this many characters before diffing.
    ///
    /// Useful for very large documents where only the first N characters matter.
    /// `None` means no truncation.
    pub max_content_chars: Option<f64>,
}

#[extendr]
impl DiffOptions {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> DiffOptions {
        kreuzberg::DiffOptions::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<DiffOptions> {
        let core: kreuzberg::DiffOptions =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[extendr]
pub fn new_diffoptions(
    include_metadata: Option<bool>,
    include_embedded: Option<bool>,
    max_content_chars: Option<f64>,
) -> DiffOptions {
    let mut __out = <DiffOptions>::default();
    if let Some(v) = include_metadata {
        __out.include_metadata = v;
    }
    if let Some(v) = include_embedded {
        __out.include_embedded = v;
    }
    if let Some(v) = max_content_chars {
        __out.max_content_chars = Some(v);
    }
    __out
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ExtractionDiff {
    /// Unified-diff hunks for the `content` field.
    ///
    /// Empty when the content is identical.
    pub content_diff: Vec<DiffHunk>,
    /// Tables present in `b` but not in `a` (by index position, excess right-side tables).
    pub tables_added: Vec<Table>,
    /// Tables present in `a` but not in `b` (by index position, excess left-side tables).
    pub tables_removed: Vec<Table>,
    /// Cell-level changes for table pairs that share the same index and dimensions.
    pub tables_changed: Vec<TableDiff>,
    /// Metadata difference, encoded as a JSON object with three top-level keys:
    /// `added` (keys present in `b` but not `a`), `removed` (keys present in `a`
    /// but not `b`), and `changed` (keys whose values differ — each entry is
    /// `{ "from": <value-in-a>, "to": <value-in-b> }`).
    ///
    /// This is NOT RFC 6902 JSON Patch — we deliberately chose a flatter shape
    /// to avoid pulling in a json-patch crate. If you need RFC 6902 semantics
    /// (with JSON Pointer paths) feed `a.metadata` and `b.metadata` to your
    /// preferred json-patch impl directly.
    pub metadata_changed: String,
    /// Changes to embedded archive children.
    pub embedded_changes: EmbeddedChanges,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DiffHunk {
    /// Starting line number in the old content (0-indexed).
    pub from_line: f64,
    /// Number of lines from the old content in this hunk.
    pub from_count: f64,
    /// Starting line number in the new content (0-indexed).
    pub to_line: f64,
    /// Number of lines from the new content in this hunk.
    pub to_count: f64,
    /// Lines that make up this hunk.
    pub lines: Vec<DiffLine>,
}

#[extendr]
impl DiffHunk {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct TableDiff {
    /// Zero-based index of the table in both `a.tables` and `b.tables`.
    pub from_index: f64,
    /// Zero-based index in `b.tables` (equal to `from_index` for same-dimension tables).
    pub to_index: f64,
    /// Cell-level changes within the table.
    pub cell_changes: Vec<CellChange>,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EmbeddedChanges {
    /// Children present in `b` but not in `a` (matched by `path`).
    pub added: Vec<ArchiveEntry>,
    /// Children present in `a` but not in `b` (matched by `path`).
    pub removed: Vec<ArchiveEntry>,
    /// Children present in both but with differing content (matched by `path`).
    ///
    /// Each entry holds the diff of the nested `ExtractionResult`.
    pub changed: Vec<EmbeddedDiff>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EmbeddedDiff {
    /// Archive-relative path identifying this entry.
    pub path: String,
    /// The recursive diff of the entry's extraction result.
    pub diff: ExtractionDiff,
}

#[extendr]
impl EmbeddedDiff {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EmbeddingPreset {
    pub name: String,
    pub chunk_size: f64,
    pub overlap: f64,
    /// HuggingFace repository name for the model.
    pub model_repo: String,
    /// Pooling strategy: "cls" or "mean".
    pub pooling: String,
    /// Path to the ONNX model file within the repo.
    pub model_file: String,
    pub dimensions: f64,
    pub description: String,
}

#[extendr]
impl EmbeddingPreset {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct YakeParams {
    /// Window size for co-occurrence analysis (default: 2).
    ///
    /// Controls the context window for computing co-occurrence statistics.
    pub window_size: f64,
}

#[extendr]
impl YakeParams {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> YakeParams {
        kreuzberg::YakeParams::default().into()
    }
}

#[extendr]
pub fn new_yakeparams(window_size: Option<f64>) -> YakeParams {
    let mut __out = <YakeParams>::default();
    if let Some(v) = window_size {
        __out.window_size = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct RakeParams {
    /// Minimum word length to consider (default: 1).
    pub min_word_length: f64,
    /// Maximum words in a keyword phrase (default: 3).
    pub max_words_per_phrase: f64,
}

#[extendr]
impl RakeParams {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> RakeParams {
        kreuzberg::RakeParams::default().into()
    }
}

#[extendr]
pub fn new_rakeparams(min_word_length: Option<f64>, max_words_per_phrase: Option<f64>) -> RakeParams {
    let mut __out = <RakeParams>::default();
    if let Some(v) = min_word_length {
        __out.min_word_length = v;
    }
    if let Some(v) = max_words_per_phrase {
        __out.max_words_per_phrase = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct KeywordConfig {
    /// Algorithm to use for extraction.
    pub algorithm: KeywordAlgorithm,
    /// Maximum number of keywords to extract (default: 10).
    pub max_keywords: f64,
    /// Minimum score threshold (0.0-1.0, default: 0.0).
    ///
    /// Keywords with scores below this threshold are filtered out.
    /// Note: Score ranges differ between algorithms.
    pub min_score: f64,
    /// N-gram range for keyword extraction (min, max).
    ///
    /// (1, 1) = unigrams only
    /// (1, 2) = unigrams and bigrams
    /// (1, 3) = unigrams, bigrams, and trigrams (default)
    pub ngram_range: Vec<f64>,
    /// Language code for stopword filtering (e.g., "en", "de", "fr").
    ///
    /// If None, no stopword filtering is applied.
    pub language: Option<String>,
}

#[extendr]
impl KeywordConfig {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> KeywordConfig {
        kreuzberg::KeywordConfig::default().into()
    }
}

#[extendr]
pub fn new_keywordconfig(
    algorithm: Option<String>,
    max_keywords: Option<f64>,
    min_score: Option<f64>,
    ngram_range: Option<Vec<f64>>,
    language: Option<String>,
) -> KeywordConfig {
    let mut __out = <KeywordConfig>::default();
    if let Some(v) = algorithm {
        if let Ok(parsed) = serde_json::from_str(&format!("\"{v}\"")) {
            __out.algorithm = parsed;
        }
    }
    if let Some(v) = max_keywords {
        __out.max_keywords = v;
    }
    if let Some(v) = min_score {
        __out.min_score = v;
    }
    if let Some(v) = ngram_range {
        __out.ngram_range = v;
    }
    if let Some(v) = language {
        __out.language = Some(v);
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Keyword {
    /// The keyword text.
    pub text: String,
    /// Relevance score (higher is better, algorithm-specific range).
    pub score: f64,
    /// Algorithm that extracted this keyword.
    pub algorithm: KeywordAlgorithm,
    /// Optional positions where keyword appears in text (character offsets).
    pub positions: Option<Vec<f64>>,
}

#[extendr]
impl Keyword {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PaddleOcrConfig {
    /// Language code (e.g., "en", "ch", "jpn", "kor", "deu", "fra")
    pub language: String,
    /// Optional custom cache directory for model files
    pub cache_dir: Option<String>,
    /// Enable angle classification for rotated text (default: false).
    /// Can misfire on short text regions, rotating crops incorrectly before recognition.
    pub use_angle_cls: bool,
    /// Enable table structure detection (default: false)
    pub enable_table_detection: bool,
    /// Database threshold for text detection (default: 0.3)
    /// Range: 0.0-1.0, higher values require more confident detections
    pub det_db_thresh: f64,
    /// Box threshold for text bounding box refinement (default: 0.5)
    /// Range: 0.0-1.0
    pub det_db_box_thresh: f64,
    /// Unclip ratio for expanding text bounding boxes (default: 1.6)
    /// Controls the expansion of detected text regions
    pub det_db_unclip_ratio: f64,
    /// Maximum side length for detection image (default: 960)
    /// Larger images may be resized to this limit for faster inference
    pub det_limit_side_len: i32,
    /// Batch size for recognition inference (default: 6)
    /// Number of text regions to process simultaneously
    pub rec_batch_num: i32,
    /// Padding in pixels added around the image before detection (default: 10).
    /// Large values can include surrounding content like table gridlines.
    pub padding: i32,
    /// Minimum recognition confidence score for text lines (default: 0.5).
    /// Text regions with recognition confidence below this threshold are discarded.
    /// Matches PaddleOCR Python's `drop_score` parameter.
    /// Range: 0.0-1.0
    pub drop_score: f64,
    /// Model tier controlling detection/recognition model size and accuracy trade-off.
    /// - `"mobile"` (default): Lightweight models (~4.5MB detection, ~16.5MB recognition), fast download and inference
    /// - `"server"`: Large, high-accuracy models (~88MB detection, ~84MB recognition), best for GPU or complex documents
    pub model_tier: String,
}

#[extendr]
impl PaddleOcrConfig {
    pub fn with_cache_dir(&self, path: String) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_cache_dir(std::path::PathBuf::from(path)).into()
    }

    pub fn with_table_detection(&self, enable: bool) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_table_detection(enable).into()
    }

    pub fn with_angle_cls(&self, enable: bool) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_angle_cls(enable).into()
    }

    pub fn with_det_db_thresh(&self, threshold: f64) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_det_db_thresh(threshold as f32).into()
    }

    pub fn with_det_db_box_thresh(&self, threshold: f64) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_det_db_box_thresh(threshold as f32).into()
    }

    pub fn with_det_db_unclip_ratio(&self, ratio: f64) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_det_db_unclip_ratio(ratio as f32).into()
    }

    pub fn with_det_limit_side_len(&self, length: i32) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_det_limit_side_len(length as u32).into()
    }

    pub fn with_rec_batch_num(&self, batch_size: i32) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_rec_batch_num(batch_size as u32).into()
    }

    pub fn with_drop_score(&self, score: f64) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_drop_score(score as f32).into()
    }

    pub fn with_padding(&self, padding: i32) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_padding(padding as u32).into()
    }

    pub fn with_model_tier(&self, tier: String) -> PaddleOcrConfig {
        let core_self = kreuzberg::PaddleOcrConfig {
            language: self.language.clone(),

            cache_dir: self.cache_dir.clone().map(Into::into),

            use_angle_cls: self.use_angle_cls,

            enable_table_detection: self.enable_table_detection,

            det_db_thresh: self.det_db_thresh as f32,

            det_db_box_thresh: self.det_db_box_thresh as f32,

            det_db_unclip_ratio: self.det_db_unclip_ratio as f32,

            det_limit_side_len: self.det_limit_side_len as u32,

            rec_batch_num: self.rec_batch_num as u32,

            padding: self.padding as u32,

            drop_score: self.drop_score as f32,

            model_tier: self.model_tier.clone(),
        };
        core_self.with_model_tier(tier).into()
    }

    #[allow(clippy::should_implement_trait)]
    pub fn default() -> PaddleOcrConfig {
        kreuzberg::PaddleOcrConfig::default().into()
    }
}

#[extendr]
pub fn new_paddleocrconfig(
    language: Option<String>,
    cache_dir: Option<String>,
    use_angle_cls: Option<bool>,
    enable_table_detection: Option<bool>,
    det_db_thresh: Option<f64>,
    det_db_box_thresh: Option<f64>,
    det_db_unclip_ratio: Option<f64>,
    det_limit_side_len: Option<i32>,
    rec_batch_num: Option<i32>,
    padding: Option<i32>,
    drop_score: Option<f64>,
    model_tier: Option<String>,
) -> PaddleOcrConfig {
    let mut __out = <PaddleOcrConfig>::default();
    if let Some(v) = language {
        __out.language = v;
    }
    if let Some(v) = cache_dir {
        __out.cache_dir = Some(v);
    }
    if let Some(v) = use_angle_cls {
        __out.use_angle_cls = v;
    }
    if let Some(v) = enable_table_detection {
        __out.enable_table_detection = v;
    }
    if let Some(v) = det_db_thresh {
        __out.det_db_thresh = v;
    }
    if let Some(v) = det_db_box_thresh {
        __out.det_db_box_thresh = v;
    }
    if let Some(v) = det_db_unclip_ratio {
        __out.det_db_unclip_ratio = v;
    }
    if let Some(v) = det_limit_side_len {
        __out.det_limit_side_len = v;
    }
    if let Some(v) = rec_batch_num {
        __out.rec_batch_num = v;
    }
    if let Some(v) = padding {
        __out.padding = v;
    }
    if let Some(v) = drop_score {
        __out.drop_score = v;
    }
    if let Some(v) = model_tier {
        __out.model_tier = v;
    }
    __out
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct ModelPaths {
    /// Path to the detection model directory.
    pub det_model: String,
    /// Path to the classification model directory.
    pub cls_model: String,
    /// Path to the recognition model directory.
    pub rec_model: String,
    /// Path to the character dictionary file.
    pub dict_file: String,
}

#[extendr]
impl ModelPaths {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OrientationResult {
    /// Detected orientation in degrees (0, 90, 180, or 270).
    pub degrees: i32,
    /// Confidence score (0.0-1.0).
    pub confidence: f64,
}

#[extendr]
impl OrientationResult {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[allow(clippy::similar_names)]
pub struct BBox {
    pub x1: f64,
    pub y1: f64,
    pub x2: f64,
    pub y2: f64,
}

#[extendr]
impl BBox {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct LayoutDetection {
    pub class_name: LayoutClass,
    pub confidence: f64,
    pub bbox: BBox,
}

#[extendr]
impl LayoutDetection {}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct RecognizedTable {
    /// Detection bbox that this table corresponds to (for matching).
    pub detection_bbox: BBox,
    /// Table cells as a 2D vector (rows × columns).
    pub cells: Vec<Vec<String>>,
    /// Rendered markdown table.
    pub markdown: String,
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DetectionResult {
    pub page_width: i32,
    pub page_height: i32,
    pub detections: Vec<LayoutDetection>,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EmbeddedFile {
    /// The filename as stored in the PDF name tree.
    pub name: String,
    /// Raw file bytes from the embedded stream (already decompressed by lopdf).
    pub data: Vec<u8>,
    /// Compressed byte count of the original stream (before decompression).
    ///
    /// Used by callers to compute the decompression ratio and detect zip-bomb-style
    /// attacks that embed a tiny compressed stream expanding to gigabytes of data.
    pub compressed_size: f64,
    /// MIME type if specified in the filespec, otherwise `None`.
    pub mime_type: Option<String>,
}

#[extendr]
impl EmbeddedFile {}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct PdfMetadata {
    /// PDF version (e.g., "1.7", "2.0")
    pub pdf_version: Option<String>,
    /// PDF producer (application that created the PDF)
    pub producer: Option<String>,
    /// Whether the PDF is encrypted/password-protected
    pub is_encrypted: Option<bool>,
    /// First page width in points (1/72 inch)
    pub width: Option<f64>,
    /// First page height in points (1/72 inch)
    pub height: Option<f64>,
    /// Total number of pages in the PDF document
    pub page_count: Option<i32>,
}

#[extendr]
impl PdfMetadata {}

#[extendr]
pub fn new_pdfmetadata(
    pdf_version: Option<String>,
    producer: Option<String>,
    is_encrypted: Option<bool>,
    width: Option<f64>,
    height: Option<f64>,
    page_count: Option<i32>,
) -> PdfMetadata {
    let mut __out = <PdfMetadata>::default();
    if let Some(v) = pdf_version {
        __out.pdf_version = Some(v);
    }
    if let Some(v) = producer {
        __out.producer = Some(v);
    }
    if let Some(v) = is_encrypted {
        __out.is_encrypted = Some(v);
    }
    if let Some(v) = width {
        __out.width = Some(v);
    }
    if let Some(v) = height {
        __out.height = Some(v);
    }
    if let Some(v) = page_count {
        __out.page_count = Some(v);
    }
    __out
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExecutionProviderType {
    #[default]
    Auto = 0,
    Cpu = 1,
    CoreMl = 2,
    Cuda = 3,
    TensorRt = 4,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct OutputFormat {
    pub format_type: String,
    pub custom: Option<String>,
}

#[extendr]
impl OutputFormat {}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HtmlTheme {
    Default = 0,
    GitHub = 1,
    Dark = 2,
    Light = 3,
    #[default]
    Unstyled = 4,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TableModel {
    #[default]
    Tatr = 0,
    SlanetWired = 1,
    SlanetWireless = 2,
    SlanetPlus = 3,
    SlanetAuto = 4,
    Disabled = 5,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChunkerType {
    #[default]
    Text = 0,
    Markdown = 1,
    Yaml = 2,
    Semantic = 3,
}

#[extendr]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(from = "kreuzberg::ChunkSizing", into = "kreuzberg::ChunkSizing")]
pub struct ChunkSizing {
    /// Serde-JSON encoding of the underlying core enum value. Preserves the
    /// tagged-variant payload across the FFI boundary so round trips don't drop
    /// inner field data. The field is private-by-convention (double-underscore
    /// prefix) and not surfaced in R; construction goes through `from_json`.
    #[serde(skip)]
    pub __inner: String,
}

impl From<kreuzberg::ChunkSizing> for ChunkSizing {
    fn from(value: kreuzberg::ChunkSizing) -> Self {
        Self {
            __inner: serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
        }
    }
}

impl From<ChunkSizing> for kreuzberg::ChunkSizing {
    fn from(value: ChunkSizing) -> Self {
        if value.__inner.is_empty() {
            return <kreuzberg::ChunkSizing>::default();
        }
        serde_json::from_str(&value.__inner).unwrap_or_default()
    }
}

#[extendr]
impl ChunkSizing {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> ChunkSizing {
        <kreuzberg::ChunkSizing>::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<ChunkSizing> {
        let core: kreuzberg::ChunkSizing =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[extendr]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(from = "kreuzberg::EmbeddingModelType", into = "kreuzberg::EmbeddingModelType")]
pub struct EmbeddingModelType {
    /// Serde-JSON encoding of the underlying core enum value. Preserves the
    /// tagged-variant payload across the FFI boundary so round trips don't drop
    /// inner field data. The field is private-by-convention (double-underscore
    /// prefix) and not surfaced in R; construction goes through `from_json`.
    #[serde(skip)]
    pub __inner: String,
}

impl From<kreuzberg::EmbeddingModelType> for EmbeddingModelType {
    fn from(value: kreuzberg::EmbeddingModelType) -> Self {
        Self {
            __inner: serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
        }
    }
}

impl From<EmbeddingModelType> for kreuzberg::EmbeddingModelType {
    fn from(value: EmbeddingModelType) -> Self {
        if value.__inner.is_empty() {
            return <kreuzberg::EmbeddingModelType>::default();
        }
        serde_json::from_str(&value.__inner).unwrap_or_default()
    }
}

#[extendr]
impl EmbeddingModelType {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> EmbeddingModelType {
        <kreuzberg::EmbeddingModelType>::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<EmbeddingModelType> {
        let core: kreuzberg::EmbeddingModelType =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeContentMode {
    #[default]
    Chunks = 0,
    Raw = 1,
    Structure = 2,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum ListType {
    #[default]
    Bullet = 0,
    Numbered = 1,
    Lettered = 2,
    Indented = 3,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum OcrBackendType {
    #[default]
    Tesseract = 0,
    EasyOCR = 1,
    PaddleOCR = 2,
    Custom = 3,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum ProcessingStage {
    #[default]
    Early = 0,
    Middle = 1,
    Late = 2,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum ReductionLevel {
    #[default]
    Off = 0,
    Light = 1,
    Moderate = 2,
    Aggressive = 3,
    Maximum = 4,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PdfAnnotationType {
    #[default]
    Text = 0,
    Highlight = 1,
    Link = 2,
    Stamp = 3,
    Underline = 4,
    StrikeOut = 5,
    Other = 6,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BlockType {
    #[default]
    Paragraph = 0,
    Heading = 1,
    Blockquote = 2,
    CodeBlock = 3,
    ListItem = 4,
    OrderedList = 5,
    BulletList = 6,
    TaskList = 7,
    DefinitionList = 8,
    DefinitionTerm = 9,
    DefinitionDescription = 10,
    Div = 11,
    Section = 12,
    ThematicBreak = 13,
    RawBlock = 14,
    MathDisplay = 15,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InlineType {
    #[default]
    Text = 0,
    Strong = 1,
    Emphasis = 2,
    Highlight = 3,
    Subscript = 4,
    Superscript = 5,
    Insert = 6,
    Delete = 7,
    Code = 8,
    Link = 9,
    Image = 10,
    Span = 11,
    Math = 12,
    RawInline = 13,
    FootnoteRef = 14,
    Symbol = 15,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RelationshipKind {
    #[default]
    FootnoteReference = 0,
    CitationReference = 1,
    InternalLink = 2,
    Caption = 3,
    Label = 4,
    TocEntry = 5,
    CrossReference = 6,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentLayer {
    #[default]
    Body = 0,
    Header = 1,
    Footer = 2,
    Footnote = 3,
}

#[extendr]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(from = "kreuzberg::NodeContent", into = "kreuzberg::NodeContent")]
pub struct NodeContent {
    /// Serde-JSON encoding of the underlying core enum value. Preserves the
    /// tagged-variant payload across the FFI boundary so round trips don't drop
    /// inner field data. The field is private-by-convention (double-underscore
    /// prefix) and not surfaced in R; construction goes through `from_json`.
    #[serde(skip)]
    pub __inner: String,
}

impl From<kreuzberg::NodeContent> for NodeContent {
    fn from(value: kreuzberg::NodeContent) -> Self {
        Self {
            __inner: serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
        }
    }
}

impl From<NodeContent> for kreuzberg::NodeContent {
    fn from(value: NodeContent) -> Self {
        if value.__inner.is_empty() {
            return <kreuzberg::NodeContent>::default();
        }
        serde_json::from_str(&value.__inner).unwrap_or_default()
    }
}

#[extendr]
impl NodeContent {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> NodeContent {
        <kreuzberg::NodeContent>::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<NodeContent> {
        let core: kreuzberg::NodeContent =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[extendr]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(from = "kreuzberg::AnnotationKind", into = "kreuzberg::AnnotationKind")]
pub struct AnnotationKind {
    /// Serde-JSON encoding of the underlying core enum value. Preserves the
    /// tagged-variant payload across the FFI boundary so round trips don't drop
    /// inner field data. The field is private-by-convention (double-underscore
    /// prefix) and not surfaced in R; construction goes through `from_json`.
    #[serde(skip)]
    pub __inner: String,
}

impl From<kreuzberg::AnnotationKind> for AnnotationKind {
    fn from(value: kreuzberg::AnnotationKind) -> Self {
        Self {
            __inner: serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
        }
    }
}

impl From<AnnotationKind> for kreuzberg::AnnotationKind {
    fn from(value: AnnotationKind) -> Self {
        if value.__inner.is_empty() {
            return <kreuzberg::AnnotationKind>::default();
        }
        serde_json::from_str(&value.__inner).unwrap_or_default()
    }
}

#[extendr]
impl AnnotationKind {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> AnnotationKind {
        <kreuzberg::AnnotationKind>::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<AnnotationKind> {
        let core: kreuzberg::AnnotationKind =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExtractionMethod {
    #[default]
    Native = 0,
    Ocr = 1,
    Mixed = 2,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChunkType {
    Heading = 0,
    PartyList = 1,
    Definitions = 2,
    OperativeClause = 3,
    SignatureBlock = 4,
    Schedule = 5,
    TableLike = 6,
    Formula = 7,
    CodeBlock = 8,
    Image = 9,
    OrgChart = 10,
    Diagram = 11,
    #[default]
    Unknown = 12,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImageKind {
    #[default]
    Photograph = 0,
    Diagram = 1,
    Chart = 2,
    Drawing = 3,
    TextBlock = 4,
    Decoration = 5,
    Logo = 6,
    Icon = 7,
    TileFragment = 8,
    Mask = 9,
    PageRaster = 10,
    Unknown = 11,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResultFormat {
    #[default]
    Unified = 0,
    ElementBased = 1,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ElementType {
    #[default]
    Title = 0,
    NarrativeText = 1,
    Heading = 2,
    ListItem = 3,
    Table = 4,
    Image = 5,
    PageBreak = 6,
    CodeBlock = 7,
    BlockQuote = 8,
    Footer = 9,
    Header = 10,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct FormatMetadata {
    pub format_type: String,
    pub pdf: Option<PdfMetadata>,
    pub docx: Option<DocxMetadata>,
    pub excel: Option<ExcelMetadata>,
    pub email: Option<EmailMetadata>,
    pub pptx: Option<PptxMetadata>,
    pub archive: Option<ArchiveMetadata>,
    pub image: Option<ImageMetadata>,
    pub xml: Option<XmlMetadata>,
    pub text: Option<TextMetadata>,
    pub html: Option<HtmlMetadata>,
    pub ocr: Option<OcrMetadata>,
    pub csv: Option<CsvMetadata>,
    pub bibtex: Option<BibtexMetadata>,
    pub citation: Option<CitationMetadata>,
    pub fiction_book: Option<FictionBookMetadata>,
    pub dbf: Option<DbfMetadata>,
    pub jats: Option<JatsMetadata>,
    pub epub: Option<EpubMetadata>,
    pub pst: Option<PstMetadata>,
    pub code: Option<String>,
}

#[extendr]
impl FormatMetadata {}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TextDirection {
    #[default]
    #[serde(rename = "ltr")]
    LeftToRight = 0,
    #[serde(rename = "rtl")]
    RightToLeft = 1,
    #[serde(rename = "auto")]
    Auto = 2,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LinkType {
    #[default]
    Anchor = 0,
    Internal = 1,
    External = 2,
    Email = 3,
    Phone = 4,
    Other = 5,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImageType {
    #[default]
    #[serde(rename = "data-uri")]
    DataUri = 0,
    #[serde(rename = "inline-svg")]
    InlineSvg = 1,
    External = 2,
    Relative = 3,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StructuredDataType {
    #[default]
    #[serde(rename = "json-ld")]
    JsonLd = 0,
    Microdata = 1,
    #[serde(rename = "rdfa")]
    RDFa = 2,
}

#[extendr]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(from = "kreuzberg::OcrBoundingGeometry", into = "kreuzberg::OcrBoundingGeometry")]
pub struct OcrBoundingGeometry {
    /// Serde-JSON encoding of the underlying core enum value. Preserves the
    /// tagged-variant payload across the FFI boundary so round trips don't drop
    /// inner field data. The field is private-by-convention (double-underscore
    /// prefix) and not surfaced in R; construction goes through `from_json`.
    #[serde(skip)]
    pub __inner: String,
}

impl From<kreuzberg::OcrBoundingGeometry> for OcrBoundingGeometry {
    fn from(value: kreuzberg::OcrBoundingGeometry) -> Self {
        Self {
            __inner: serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
        }
    }
}

impl From<OcrBoundingGeometry> for kreuzberg::OcrBoundingGeometry {
    fn from(value: OcrBoundingGeometry) -> Self {
        if value.__inner.is_empty() {
            return <kreuzberg::OcrBoundingGeometry>::default();
        }
        serde_json::from_str(&value.__inner).unwrap_or_default()
    }
}

#[extendr]
impl OcrBoundingGeometry {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> OcrBoundingGeometry {
        <kreuzberg::OcrBoundingGeometry>::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<OcrBoundingGeometry> {
        let core: kreuzberg::OcrBoundingGeometry =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OcrElementLevel {
    Word = 0,
    #[default]
    Line = 1,
    Block = 2,
    Page = 3,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PageUnitType {
    #[default]
    Page = 0,
    Slide = 1,
    Sheet = 2,
}

#[extendr]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct DiffLine {
    pub kind: String,
    pub context: Option<String>,
    pub added: Option<String>,
    pub removed: Option<String>,
}

#[extendr]
impl DiffLine {}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RevisionKind {
    #[default]
    Insertion = 0,
    Deletion = 1,
    FormatChange = 2,
    Comment = 3,
}

#[extendr]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(from = "kreuzberg::RevisionAnchor", into = "kreuzberg::RevisionAnchor")]
pub struct RevisionAnchor {
    /// Serde-JSON encoding of the underlying core enum value. Preserves the
    /// tagged-variant payload across the FFI boundary so round trips don't drop
    /// inner field data. The field is private-by-convention (double-underscore
    /// prefix) and not surfaced in R; construction goes through `from_json`.
    #[serde(skip)]
    pub __inner: String,
}

impl From<kreuzberg::RevisionAnchor> for RevisionAnchor {
    fn from(value: kreuzberg::RevisionAnchor) -> Self {
        Self {
            __inner: serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
        }
    }
}

impl From<RevisionAnchor> for kreuzberg::RevisionAnchor {
    fn from(value: RevisionAnchor) -> Self {
        if value.__inner.is_empty() {
            return <kreuzberg::RevisionAnchor>::default();
        }
        serde_json::from_str(&value.__inner).unwrap_or_default()
    }
}

#[extendr]
impl RevisionAnchor {
    #[allow(clippy::should_implement_trait)]
    pub fn default() -> RevisionAnchor {
        <kreuzberg::RevisionAnchor>::default().into()
    }
    pub fn from_json(json: String) -> extendr_api::Result<RevisionAnchor> {
        let core: kreuzberg::RevisionAnchor =
            serde_json::from_str(&json).map_err(|e| extendr_api::Error::Other(e.to_string()))?;
        Ok(core.into())
    }
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UriKind {
    #[default]
    Hyperlink = 0,
    Image = 1,
    Anchor = 2,
    Citation = 3,
    Reference = 4,
    Email = 5,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KeywordAlgorithm {
    #[default]
    Yake = 0,
    Rake = 1,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum PSMMode {
    #[default]
    OsdOnly = 0,
    AutoOsd = 1,
    AutoOnly = 2,
    Auto = 3,
    SingleColumn = 4,
    SingleBlockVertical = 5,
    SingleBlock = 6,
    SingleLine = 7,
    SingleWord = 8,
    CircleWord = 9,
    SingleChar = 10,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum PaddleLanguage {
    #[default]
    English = 0,
    Chinese = 1,
    Japanese = 2,
    Korean = 3,
    German = 4,
    French = 5,
    Latin = 6,
    Cyrillic = 7,
    TraditionalChinese = 8,
    Thai = 9,
    Greek = 10,
    EastSlavic = 11,
    Arabic = 12,
    Devanagari = 13,
    Tamil = 14,
    Telugu = 15,
}

#[derive(Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LayoutClass {
    #[default]
    Caption = 0,
    Footnote = 1,
    Formula = 2,
    ListItem = 3,
    PageFooter = 4,
    PageHeader = 5,
    Picture = 6,
    SectionHeader = 7,
    Table = 8,
    Text = 9,
    Title = 10,
    DocumentIndex = 11,
    Code = 12,
    CheckboxSelected = 13,
    CheckboxUnselected = 14,
    Form = 15,
    KeyValueRegion = 16,
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CacheStats> for CacheStats {
    fn from(val: kreuzberg::CacheStats) -> Self {
        Self {
            total_files: val.total_files as f64,
            total_size_mb: val.total_size_mb,
            available_space_mb: val.available_space_mb,
            oldest_file_age_days: val.oldest_file_age_days,
            newest_file_age_days: val.newest_file_age_days,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<AccelerationConfig> for kreuzberg::AccelerationConfig {
    fn from(val: AccelerationConfig) -> Self {
        Self {
            provider: val.provider.into(),
            device_id: val.device_id as u32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::AccelerationConfig> for AccelerationConfig {
    fn from(val: kreuzberg::AccelerationConfig) -> Self {
        Self {
            provider: val.provider.into(),
            device_id: val.device_id as i32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ContentFilterConfig> for kreuzberg::ContentFilterConfig {
    fn from(val: ContentFilterConfig) -> Self {
        Self {
            include_headers: val.include_headers,
            include_footers: val.include_footers,
            strip_repeating_text: val.strip_repeating_text,
            include_watermarks: val.include_watermarks,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ContentFilterConfig> for ContentFilterConfig {
    fn from(val: kreuzberg::ContentFilterConfig) -> Self {
        Self {
            include_headers: val.include_headers,
            include_footers: val.include_footers,
            strip_repeating_text: val.strip_repeating_text,
            include_watermarks: val.include_watermarks,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmailConfig> for kreuzberg::EmailConfig {
    fn from(val: EmailConfig) -> Self {
        Self {
            msg_fallback_codepage: val.msg_fallback_codepage.map(|v| v as u32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailConfig> for EmailConfig {
    fn from(val: kreuzberg::EmailConfig) -> Self {
        Self {
            msg_fallback_codepage: val.msg_fallback_codepage.map(|v| v as i32),
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractionConfig> for kreuzberg::ExtractionConfig {
    fn from(val: ExtractionConfig) -> Self {
        Self {
            use_cache: val.use_cache,
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val.force_ocr_pages.map(|v| v.into_iter().map(|x| x as u32).collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            extraction_timeout_secs: val.extraction_timeout_secs.map(|v| v as u64),
            max_concurrent_extractions: val.max_concurrent_extractions.map(|v| v as usize),
            result_format: val.result_format.into(),
            security_limits: val.security_limits.map(Into::into),
            max_embedded_file_bytes: val.max_embedded_file_bytes.map(|v| v as u64),
            output_format: val.output_format.into(),
            use_layout_for_markdown: val.use_layout_for_markdown,
            include_document_structure: val.include_document_structure,
            acceleration: val.acceleration.map(Into::into),
            cache_namespace: val.cache_namespace,
            cache_ttl_secs: val.cache_ttl_secs.map(|v| v as u64),
            email: val.email.map(Into::into),
            concurrency: Default::default(),
            max_archive_depth: val.max_archive_depth as usize,
            structured_extraction: val.structured_extraction.map(Into::into),
            cancel_token: Default::default(),
            ..Default::default()
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionConfig> for ExtractionConfig {
    fn from(val: kreuzberg::ExtractionConfig) -> Self {
        Self {
            use_cache: val.use_cache,
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val
                .force_ocr_pages
                .as_ref()
                .map(|v| v.iter().map(|&x| x as i32).collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            extraction_timeout_secs: val.extraction_timeout_secs.map(|v| v as f64),
            max_concurrent_extractions: val.max_concurrent_extractions.map(|v| v as f64),
            result_format: val.result_format.into(),
            security_limits: val.security_limits.map(Into::into),
            max_embedded_file_bytes: val.max_embedded_file_bytes.map(|v| v as f64),
            output_format: val.output_format.into(),
            use_layout_for_markdown: val.use_layout_for_markdown,
            include_document_structure: val.include_document_structure,
            acceleration: val.acceleration.map(Into::into),
            cache_namespace: val.cache_namespace,
            cache_ttl_secs: val.cache_ttl_secs.map(|v| v as f64),
            email: val.email.map(Into::into),
            concurrency: val.concurrency.as_ref().map(|v| format!("{v:?}")),
            max_archive_depth: val.max_archive_depth as f64,
            structured_extraction: val.structured_extraction.map(Into::into),
            cancel_token: val.cancel_token.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<FileExtractionConfig> for kreuzberg::FileExtractionConfig {
    fn from(val: FileExtractionConfig) -> Self {
        Self {
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val.force_ocr_pages.map(|v| v.into_iter().map(|x| x as u32).collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            result_format: val.result_format.map(Into::into),
            output_format: val.output_format.map(Into::into),
            include_document_structure: val.include_document_structure,
            timeout_secs: val.timeout_secs.map(|v| v as u64),
            structured_extraction: val.structured_extraction.map(Into::into),
            ..Default::default()
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FileExtractionConfig> for FileExtractionConfig {
    fn from(val: kreuzberg::FileExtractionConfig) -> Self {
        Self {
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val
                .force_ocr_pages
                .as_ref()
                .map(|v| v.iter().map(|&x| x as i32).collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            result_format: val.result_format.map(Into::into),
            output_format: val.output_format.map(Into::into),
            include_document_structure: val.include_document_structure,
            timeout_secs: val.timeout_secs.map(|v| v as f64),
            structured_extraction: val.structured_extraction.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<BatchBytesItem> for kreuzberg::BatchBytesItem {
    fn from(val: BatchBytesItem) -> Self {
        Self {
            content: val.content.to_vec().into(),
            mime_type: val.mime_type,
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BatchBytesItem> for BatchBytesItem {
    fn from(val: kreuzberg::BatchBytesItem) -> Self {
        Self {
            content: val.content.to_vec().into(),
            mime_type: val.mime_type,
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<BatchFileItem> for kreuzberg::BatchFileItem {
    fn from(val: BatchFileItem) -> Self {
        Self {
            path: val.path.into(),
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BatchFileItem> for BatchFileItem {
    fn from(val: kreuzberg::BatchFileItem) -> Self {
        Self {
            path: val.path.to_string_lossy().to_string(),
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImageExtractionConfig> for kreuzberg::ImageExtractionConfig {
    fn from(val: ImageExtractionConfig) -> Self {
        Self {
            extract_images: val.extract_images,
            target_dpi: val.target_dpi,
            max_image_dimension: val.max_image_dimension,
            inject_placeholders: val.inject_placeholders,
            auto_adjust_dpi: val.auto_adjust_dpi,
            min_dpi: val.min_dpi,
            max_dpi: val.max_dpi,
            max_images_per_page: val.max_images_per_page.map(|v| v as u32),
            classify: val.classify,
            include_page_rasters: val.include_page_rasters,
            run_ocr_on_images: val.run_ocr_on_images,
            ocr_text_only: val.ocr_text_only,
            append_ocr_text: val.append_ocr_text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageExtractionConfig> for ImageExtractionConfig {
    fn from(val: kreuzberg::ImageExtractionConfig) -> Self {
        Self {
            extract_images: val.extract_images,
            target_dpi: val.target_dpi,
            max_image_dimension: val.max_image_dimension,
            inject_placeholders: val.inject_placeholders,
            auto_adjust_dpi: val.auto_adjust_dpi,
            min_dpi: val.min_dpi,
            max_dpi: val.max_dpi,
            max_images_per_page: val.max_images_per_page.map(|v| v as i32),
            classify: val.classify,
            include_page_rasters: val.include_page_rasters,
            run_ocr_on_images: val.run_ocr_on_images,
            ocr_text_only: val.ocr_text_only,
            append_ocr_text: val.append_ocr_text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TokenReductionOptions> for kreuzberg::TokenReductionOptions {
    fn from(val: TokenReductionOptions) -> Self {
        Self {
            mode: val.mode,
            preserve_important_words: val.preserve_important_words,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TokenReductionOptions> for TokenReductionOptions {
    fn from(val: kreuzberg::TokenReductionOptions) -> Self {
        Self {
            mode: val.mode,
            preserve_important_words: val.preserve_important_words,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LanguageDetectionConfig> for kreuzberg::LanguageDetectionConfig {
    fn from(val: LanguageDetectionConfig) -> Self {
        Self {
            enabled: val.enabled,
            min_confidence: val.min_confidence,
            detect_multiple: val.detect_multiple,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LanguageDetectionConfig> for LanguageDetectionConfig {
    fn from(val: kreuzberg::LanguageDetectionConfig) -> Self {
        Self {
            enabled: val.enabled,
            min_confidence: val.min_confidence,
            detect_multiple: val.detect_multiple,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HtmlOutputConfig> for kreuzberg::HtmlOutputConfig {
    fn from(val: HtmlOutputConfig) -> Self {
        Self {
            css: val.css,
            css_file: val.css_file.map(Into::into),
            theme: val.theme.into(),
            class_prefix: val.class_prefix,
            embed_css: val.embed_css,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HtmlOutputConfig> for HtmlOutputConfig {
    fn from(val: kreuzberg::HtmlOutputConfig) -> Self {
        Self {
            css: val.css,
            css_file: val.css_file.map(|p| p.to_string_lossy().to_string()),
            theme: val.theme.into(),
            class_prefix: val.class_prefix,
            embed_css: val.embed_css,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LayoutDetectionConfig> for kreuzberg::LayoutDetectionConfig {
    fn from(val: LayoutDetectionConfig) -> Self {
        Self {
            confidence_threshold: val.confidence_threshold.map(|v| v as f32),
            apply_heuristics: val.apply_heuristics,
            table_model: val.table_model.into(),
            acceleration: val.acceleration.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LayoutDetectionConfig> for LayoutDetectionConfig {
    fn from(val: kreuzberg::LayoutDetectionConfig) -> Self {
        Self {
            confidence_threshold: val.confidence_threshold.map(|v| v as f64),
            apply_heuristics: val.apply_heuristics,
            table_model: val.table_model.into(),
            acceleration: val.acceleration.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LlmConfig> for kreuzberg::LlmConfig {
    fn from(val: LlmConfig) -> Self {
        Self {
            model: val.model,
            api_key: val.api_key,
            base_url: val.base_url,
            timeout_secs: val.timeout_secs.map(|v| v as u64),
            max_retries: val.max_retries.map(|v| v as u32),
            temperature: val.temperature,
            max_tokens: val.max_tokens.map(|v| v as u64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LlmConfig> for LlmConfig {
    fn from(val: kreuzberg::LlmConfig) -> Self {
        Self {
            model: val.model,
            api_key: val.api_key,
            base_url: val.base_url,
            timeout_secs: val.timeout_secs.map(|v| v as f64),
            max_retries: val.max_retries.map(|v| v as i32),
            temperature: val.temperature,
            max_tokens: val.max_tokens.map(|v| v as f64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<StructuredExtractionConfig> for kreuzberg::StructuredExtractionConfig {
    fn from(val: StructuredExtractionConfig) -> Self {
        Self {
            schema: serde_json::from_str(&val.schema).unwrap_or_default(),
            schema_name: val.schema_name,
            schema_description: val.schema_description,
            strict: val.strict,
            prompt: val.prompt,
            llm: val.llm.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::StructuredExtractionConfig> for StructuredExtractionConfig {
    fn from(val: kreuzberg::StructuredExtractionConfig) -> Self {
        Self {
            schema: val.schema.to_string(),
            schema_name: val.schema_name,
            schema_description: val.schema_description,
            strict: val.strict,
            prompt: val.prompt,
            llm: val.llm.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrQualityThresholds> for kreuzberg::OcrQualityThresholds {
    fn from(val: OcrQualityThresholds) -> Self {
        Self {
            min_total_non_whitespace: val.min_total_non_whitespace as usize,
            min_non_whitespace_per_page: val.min_non_whitespace_per_page,
            min_meaningful_word_len: val.min_meaningful_word_len as usize,
            min_meaningful_words: val.min_meaningful_words as usize,
            min_alnum_ratio: val.min_alnum_ratio,
            min_garbage_chars: val.min_garbage_chars as usize,
            max_fragmented_word_ratio: val.max_fragmented_word_ratio,
            critical_fragmented_word_ratio: val.critical_fragmented_word_ratio,
            min_avg_word_length: val.min_avg_word_length,
            min_words_for_avg_length_check: val.min_words_for_avg_length_check as usize,
            min_consecutive_repeat_ratio: val.min_consecutive_repeat_ratio,
            min_words_for_repeat_check: val.min_words_for_repeat_check as usize,
            substantive_min_chars: val.substantive_min_chars as usize,
            non_text_min_chars: val.non_text_min_chars as usize,
            alnum_ws_ratio_threshold: val.alnum_ws_ratio_threshold,
            pipeline_min_quality: val.pipeline_min_quality,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrQualityThresholds> for OcrQualityThresholds {
    fn from(val: kreuzberg::OcrQualityThresholds) -> Self {
        Self {
            min_total_non_whitespace: val.min_total_non_whitespace as f64,
            min_non_whitespace_per_page: val.min_non_whitespace_per_page,
            min_meaningful_word_len: val.min_meaningful_word_len as f64,
            min_meaningful_words: val.min_meaningful_words as f64,
            min_alnum_ratio: val.min_alnum_ratio,
            min_garbage_chars: val.min_garbage_chars as f64,
            max_fragmented_word_ratio: val.max_fragmented_word_ratio,
            critical_fragmented_word_ratio: val.critical_fragmented_word_ratio,
            min_avg_word_length: val.min_avg_word_length,
            min_words_for_avg_length_check: val.min_words_for_avg_length_check as f64,
            min_consecutive_repeat_ratio: val.min_consecutive_repeat_ratio,
            min_words_for_repeat_check: val.min_words_for_repeat_check as f64,
            substantive_min_chars: val.substantive_min_chars as f64,
            non_text_min_chars: val.non_text_min_chars as f64,
            alnum_ws_ratio_threshold: val.alnum_ws_ratio_threshold,
            pipeline_min_quality: val.pipeline_min_quality,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrPipelineStage> for kreuzberg::OcrPipelineStage {
    fn from(val: OcrPipelineStage) -> Self {
        Self {
            backend: val.backend,
            priority: val.priority as u32,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            paddle_ocr_config: val
                .paddle_ocr_config
                .as_ref()
                .and_then(|s| serde_json::from_str(s).ok()),
            vlm_config: val.vlm_config.map(Into::into),
            backend_options: val.backend_options.as_ref().and_then(|s| serde_json::from_str(s).ok()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrPipelineStage> for OcrPipelineStage {
    fn from(val: kreuzberg::OcrPipelineStage) -> Self {
        Self {
            backend: val.backend,
            priority: val.priority as i32,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            paddle_ocr_config: val.paddle_ocr_config.as_ref().map(ToString::to_string),
            vlm_config: val.vlm_config.map(Into::into),
            backend_options: val.backend_options.as_ref().map(ToString::to_string),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrPipelineConfig> for kreuzberg::OcrPipelineConfig {
    fn from(val: OcrPipelineConfig) -> Self {
        Self {
            stages: val.stages.into_iter().map(Into::into).collect(),
            quality_thresholds: val.quality_thresholds.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrPipelineConfig> for OcrPipelineConfig {
    fn from(val: kreuzberg::OcrPipelineConfig) -> Self {
        Self {
            stages: val.stages.into_iter().map(Into::into).collect(),
            quality_thresholds: val.quality_thresholds.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrConfig> for kreuzberg::OcrConfig {
    fn from(val: OcrConfig) -> Self {
        Self {
            enabled: val.enabled,
            backend: val.backend,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            output_format: val.output_format.map(Into::into),
            paddle_ocr_config: val
                .paddle_ocr_config
                .as_ref()
                .and_then(|s| serde_json::from_str(s).ok()),
            backend_options: val.backend_options.as_ref().and_then(|s| serde_json::from_str(s).ok()),
            element_config: val.element_config.map(Into::into),
            quality_thresholds: val.quality_thresholds.map(Into::into),
            pipeline: val.pipeline.map(Into::into),
            auto_rotate: val.auto_rotate,
            vlm_config: val.vlm_config.map(Into::into),
            vlm_prompt: val.vlm_prompt,
            acceleration: val.acceleration.map(Into::into),
            tessdata_bytes: val
                .tessdata_bytes
                .map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrConfig> for OcrConfig {
    fn from(val: kreuzberg::OcrConfig) -> Self {
        Self {
            enabled: val.enabled,
            backend: val.backend,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            output_format: val.output_format.map(Into::into),
            paddle_ocr_config: val.paddle_ocr_config.as_ref().map(ToString::to_string),
            backend_options: val.backend_options.as_ref().map(ToString::to_string),
            element_config: val.element_config.map(Into::into),
            quality_thresholds: val.quality_thresholds.map(Into::into),
            pipeline: val.pipeline.map(Into::into),
            auto_rotate: val.auto_rotate,
            vlm_config: val.vlm_config.map(Into::into),
            vlm_prompt: val.vlm_prompt,
            acceleration: val.acceleration.map(Into::into),
            tessdata_bytes: val
                .tessdata_bytes
                .map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageConfig> for kreuzberg::PageConfig {
    fn from(val: PageConfig) -> Self {
        Self {
            extract_pages: val.extract_pages,
            insert_page_markers: val.insert_page_markers,
            marker_format: val.marker_format,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageConfig> for PageConfig {
    fn from(val: kreuzberg::PageConfig) -> Self {
        Self {
            extract_pages: val.extract_pages,
            insert_page_markers: val.insert_page_markers,
            marker_format: val.marker_format,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PdfConfig> for kreuzberg::PdfConfig {
    fn from(val: PdfConfig) -> Self {
        Self {
            extract_images: val.extract_images,
            extract_tables: val.extract_tables,
            passwords: val.passwords.map(|v| v.into_iter().collect()),
            extract_metadata: val.extract_metadata,
            hierarchy: val.hierarchy.map(Into::into),
            extract_annotations: val.extract_annotations,
            top_margin_fraction: val.top_margin_fraction.map(|v| v as f32),
            bottom_margin_fraction: val.bottom_margin_fraction.map(|v| v as f32),
            allow_single_column_tables: val.allow_single_column_tables,
            ocr_inline_images: val.ocr_inline_images,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PdfConfig> for PdfConfig {
    fn from(val: kreuzberg::PdfConfig) -> Self {
        Self {
            extract_images: val.extract_images,
            extract_tables: val.extract_tables,
            passwords: val.passwords.map(|v| v.into_iter().collect()),
            extract_metadata: val.extract_metadata,
            hierarchy: val.hierarchy.map(Into::into),
            extract_annotations: val.extract_annotations,
            top_margin_fraction: val.top_margin_fraction.map(|v| v as f64),
            bottom_margin_fraction: val.bottom_margin_fraction.map(|v| v as f64),
            allow_single_column_tables: val.allow_single_column_tables,
            ocr_inline_images: val.ocr_inline_images,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HierarchyConfig> for kreuzberg::HierarchyConfig {
    fn from(val: HierarchyConfig) -> Self {
        Self {
            enabled: val.enabled,
            k_clusters: val.k_clusters as usize,
            include_bbox: val.include_bbox,
            ocr_coverage_threshold: val.ocr_coverage_threshold.map(|v| v as f32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HierarchyConfig> for HierarchyConfig {
    fn from(val: kreuzberg::HierarchyConfig) -> Self {
        Self {
            enabled: val.enabled,
            k_clusters: val.k_clusters as f64,
            include_bbox: val.include_bbox,
            ocr_coverage_threshold: val.ocr_coverage_threshold.map(|v| v as f64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PostProcessorConfig> for kreuzberg::PostProcessorConfig {
    fn from(val: PostProcessorConfig) -> Self {
        Self {
            enabled: val.enabled,
            enabled_processors: val.enabled_processors.map(|v| v.into_iter().collect()),
            disabled_processors: val.disabled_processors.map(|v| v.into_iter().collect()),
            enabled_set: val.enabled_set.map(|v| v.into_iter().collect()),
            disabled_set: val.disabled_set.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PostProcessorConfig> for PostProcessorConfig {
    fn from(val: kreuzberg::PostProcessorConfig) -> Self {
        Self {
            enabled: val.enabled,
            enabled_processors: val.enabled_processors.map(|v| v.into_iter().collect()),
            disabled_processors: val.disabled_processors.map(|v| v.into_iter().collect()),
            enabled_set: val.enabled_set.map(|v| v.into_iter().collect()),
            disabled_set: val.disabled_set.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ChunkingConfig> for kreuzberg::ChunkingConfig {
    fn from(val: ChunkingConfig) -> Self {
        Self {
            max_characters: val.max_characters as usize,
            overlap: val.overlap as usize,
            trim: val.trim,
            chunker_type: val.chunker_type.into(),
            embedding: val.embedding.map(Into::into),
            preset: val.preset,
            sizing: val.sizing.into(),
            prepend_heading_context: val.prepend_heading_context,
            topic_threshold: val.topic_threshold.map(|v| v as f32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ChunkingConfig> for ChunkingConfig {
    fn from(val: kreuzberg::ChunkingConfig) -> Self {
        Self {
            max_characters: val.max_characters as f64,
            overlap: val.overlap as f64,
            trim: val.trim,
            chunker_type: val.chunker_type.into(),
            embedding: val.embedding.map(Into::into),
            preset: val.preset,
            sizing: val.sizing.into(),
            prepend_heading_context: val.prepend_heading_context,
            topic_threshold: val.topic_threshold.map(|v| v as f64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmbeddingConfig> for kreuzberg::EmbeddingConfig {
    fn from(val: EmbeddingConfig) -> Self {
        Self {
            model: val.model.into(),
            normalize: val.normalize,
            batch_size: val.batch_size as usize,
            show_download_progress: val.show_download_progress,
            cache_dir: val.cache_dir.map(Into::into),
            acceleration: val.acceleration.map(Into::into),
            max_embed_duration_secs: val.max_embed_duration_secs.map(|v| v as u64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddingConfig> for EmbeddingConfig {
    fn from(val: kreuzberg::EmbeddingConfig) -> Self {
        Self {
            model: val.model.into(),
            normalize: val.normalize,
            batch_size: val.batch_size as f64,
            show_download_progress: val.show_download_progress,
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            acceleration: val.acceleration.map(Into::into),
            max_embed_duration_secs: val.max_embed_duration_secs.map(|v| v as f64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TreeSitterConfig> for kreuzberg::TreeSitterConfig {
    fn from(val: TreeSitterConfig) -> Self {
        Self {
            enabled: val.enabled,
            cache_dir: val.cache_dir.map(Into::into),
            languages: val.languages.map(|v| v.into_iter().collect()),
            groups: val.groups.map(|v| v.into_iter().collect()),
            process: val.process.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TreeSitterConfig> for TreeSitterConfig {
    fn from(val: kreuzberg::TreeSitterConfig) -> Self {
        Self {
            enabled: val.enabled,
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            languages: val.languages.map(|v| v.into_iter().collect()),
            groups: val.groups.map(|v| v.into_iter().collect()),
            process: val.process.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TreeSitterProcessConfig> for kreuzberg::TreeSitterProcessConfig {
    fn from(val: TreeSitterProcessConfig) -> Self {
        Self {
            structure: val.structure,
            imports: val.imports,
            exports: val.exports,
            comments: val.comments,
            docstrings: val.docstrings,
            symbols: val.symbols,
            diagnostics: val.diagnostics,
            chunk_max_size: val.chunk_max_size.map(|v| v as usize),
            content_mode: val.content_mode.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TreeSitterProcessConfig> for TreeSitterProcessConfig {
    fn from(val: kreuzberg::TreeSitterProcessConfig) -> Self {
        Self {
            structure: val.structure,
            imports: val.imports,
            exports: val.exports,
            comments: val.comments,
            docstrings: val.docstrings,
            symbols: val.symbols,
            diagnostics: val.diagnostics,
            chunk_max_size: val.chunk_max_size.map(|v| v as f64),
            content_mode: val.content_mode.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::SupportedFormat> for SupportedFormat {
    fn from(val: kreuzberg::SupportedFormat) -> Self {
        Self {
            extension: val.extension,
            mime_type: val.mime_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ServerConfig> for kreuzberg::ServerConfig {
    fn from(val: ServerConfig) -> Self {
        Self {
            host: val.host,
            port: val.port as u16,
            cors_origins: val.cors_origins.into_iter().collect(),
            max_request_body_bytes: val.max_request_body_bytes as usize,
            max_multipart_field_bytes: val.max_multipart_field_bytes as usize,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ServerConfig> for ServerConfig {
    fn from(val: kreuzberg::ServerConfig) -> Self {
        Self {
            host: val.host,
            port: val.port as i32,
            cors_origins: val.cors_origins.into_iter().collect(),
            max_request_body_bytes: val.max_request_body_bytes as f64,
            max_multipart_field_bytes: val.max_multipart_field_bytes as f64,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::extraction::structured::StructuredDataResult> for StructuredDataResult {
    fn from(val: kreuzberg::extraction::structured::StructuredDataResult) -> Self {
        Self {
            content: val.content,
            format: val.format.to_string(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            text_fields: val.text_fields.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocxAppProperties> for kreuzberg::DocxAppProperties {
    fn from(val: DocxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            template: val.template,
            total_time: val.total_time,
            pages: val.pages,
            words: val.words,
            characters: val.characters,
            characters_with_spaces: val.characters_with_spaces,
            lines: val.lines,
            paragraphs: val.paragraphs,
            company: val.company,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocxAppProperties> for DocxAppProperties {
    fn from(val: kreuzberg::DocxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            template: val.template,
            total_time: val.total_time,
            pages: val.pages,
            words: val.words,
            characters: val.characters,
            characters_with_spaces: val.characters_with_spaces,
            lines: val.lines,
            paragraphs: val.paragraphs,
            company: val.company,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::extraction::office_metadata::app_properties::XlsxAppProperties> for XlsxAppProperties {
    fn from(val: kreuzberg::extraction::office_metadata::app_properties::XlsxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
            company: val.company,
            worksheet_names: val.worksheet_names.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::extraction::office_metadata::app_properties::PptxAppProperties> for PptxAppProperties {
    fn from(val: kreuzberg::extraction::office_metadata::app_properties::PptxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            total_time: val.total_time,
            company: val.company,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
            slides: val.slides,
            notes: val.notes,
            hidden_slides: val.hidden_slides,
            multimedia_clips: val.multimedia_clips,
            presentation_format: val.presentation_format,
            slide_titles: val.slide_titles.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<CoreProperties> for kreuzberg::CoreProperties {
    fn from(val: CoreProperties) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            creator: val.creator,
            keywords: val.keywords,
            description: val.description,
            last_modified_by: val.last_modified_by,
            revision: val.revision,
            created: val.created,
            modified: val.modified,
            category: val.category,
            content_status: val.content_status,
            language: val.language,
            identifier: val.identifier,
            version: val.version,
            last_printed: val.last_printed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CoreProperties> for CoreProperties {
    fn from(val: kreuzberg::CoreProperties) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            creator: val.creator,
            keywords: val.keywords,
            description: val.description,
            last_modified_by: val.last_modified_by,
            revision: val.revision,
            created: val.created,
            modified: val.modified,
            category: val.category,
            content_status: val.content_status,
            language: val.language,
            identifier: val.identifier,
            version: val.version,
            last_printed: val.last_printed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<SecurityLimits> for kreuzberg::SecurityLimits {
    fn from(val: SecurityLimits) -> Self {
        Self {
            max_archive_size: val.max_archive_size as usize,
            max_compression_ratio: val.max_compression_ratio as usize,
            max_files_in_archive: val.max_files_in_archive as usize,
            max_nesting_depth: val.max_nesting_depth as usize,
            max_entity_length: val.max_entity_length as usize,
            max_content_size: val.max_content_size as usize,
            max_iterations: val.max_iterations as usize,
            max_xml_depth: val.max_xml_depth as usize,
            max_table_cells: val.max_table_cells as usize,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::SecurityLimits> for SecurityLimits {
    fn from(val: kreuzberg::SecurityLimits) -> Self {
        Self {
            max_archive_size: val.max_archive_size as f64,
            max_compression_ratio: val.max_compression_ratio as f64,
            max_files_in_archive: val.max_files_in_archive as f64,
            max_nesting_depth: val.max_nesting_depth as f64,
            max_entity_length: val.max_entity_length as f64,
            max_content_size: val.max_content_size as f64,
            max_iterations: val.max_iterations as f64,
            max_xml_depth: val.max_xml_depth as f64,
            max_table_cells: val.max_table_cells as f64,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TokenReductionConfig> for kreuzberg::TokenReductionConfig {
    fn from(val: TokenReductionConfig) -> Self {
        Self {
            level: val.level.into(),
            language_hint: val.language_hint,
            preserve_markdown: val.preserve_markdown,
            preserve_code: val.preserve_code,
            semantic_threshold: val.semantic_threshold as f32,
            enable_parallel: val.enable_parallel,
            use_simd: val.use_simd,
            custom_stopwords: val.custom_stopwords.map(|m| m.into_iter().collect()),
            preserve_patterns: val.preserve_patterns.into_iter().collect(),
            target_reduction: val.target_reduction.map(|v| v as f32),
            enable_semantic_clustering: val.enable_semantic_clustering,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TokenReductionConfig> for TokenReductionConfig {
    fn from(val: kreuzberg::TokenReductionConfig) -> Self {
        Self {
            level: val.level.into(),
            language_hint: val.language_hint,
            preserve_markdown: val.preserve_markdown,
            preserve_code: val.preserve_code,
            semantic_threshold: val.semantic_threshold as f64,
            enable_parallel: val.enable_parallel,
            use_simd: val.use_simd,
            custom_stopwords: val.custom_stopwords.map(|m| m.into_iter().collect()),
            preserve_patterns: val.preserve_patterns.into_iter().collect(),
            target_reduction: val.target_reduction.map(|v| v as f64),
            enable_semantic_clustering: val.enable_semantic_clustering,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PdfAnnotation> for kreuzberg::PdfAnnotation {
    fn from(val: PdfAnnotation) -> Self {
        Self {
            annotation_type: val.annotation_type.into(),
            content: val.content,
            page_number: val.page_number as u32,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PdfAnnotation> for PdfAnnotation {
    fn from(val: kreuzberg::PdfAnnotation) -> Self {
        Self {
            annotation_type: val.annotation_type.into(),
            content: val.content,
            page_number: val.page_number as i32,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DjotContent> for kreuzberg::DjotContent {
    fn from(val: DjotContent) -> Self {
        Self {
            plain_text: val.plain_text,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            footnotes: val.footnotes.into_iter().map(Into::into).collect(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotContent> for DjotContent {
    fn from(val: kreuzberg::DjotContent) -> Self {
        Self {
            plain_text: val.plain_text,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            footnotes: val.footnotes.into_iter().map(Into::into).collect(),
            attributes: val.attributes.iter().map(|i| format!("{:?}", i)).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<FormattedBlock> for kreuzberg::FormattedBlock {
    fn from(val: FormattedBlock) -> Self {
        Self {
            block_type: val.block_type.into(),
            level: val.level.map(|v| v as usize),
            inline_content: val.inline_content.into_iter().map(Into::into).collect(),
            attributes: Default::default(),
            language: val.language,
            code: val.code,
            children: val.children.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FormattedBlock> for FormattedBlock {
    fn from(val: kreuzberg::FormattedBlock) -> Self {
        Self {
            block_type: val.block_type.into(),
            level: val.level.map(|v| v as f64),
            inline_content: val.inline_content.into_iter().map(Into::into).collect(),
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
            language: val.language,
            code: val.code,
            children: val.children.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<InlineElement> for kreuzberg::InlineElement {
    fn from(val: InlineElement) -> Self {
        Self {
            element_type: val.element_type.into(),
            content: val.content,
            attributes: Default::default(),
            metadata: val
                .metadata
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::InlineElement> for InlineElement {
    fn from(val: kreuzberg::InlineElement) -> Self {
        Self {
            element_type: val.element_type.into(),
            content: val.content,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
            metadata: val
                .metadata
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DjotImage> for kreuzberg::DjotImage {
    fn from(val: DjotImage) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotImage> for DjotImage {
    fn from(val: kreuzberg::DjotImage) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DjotLink> for kreuzberg::DjotLink {
    fn from(val: DjotLink) -> Self {
        Self {
            url: val.url,
            text: val.text,
            title: val.title,
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotLink> for DjotLink {
    fn from(val: kreuzberg::DjotLink) -> Self {
        Self {
            url: val.url,
            text: val.text,
            title: val.title,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Footnote> for kreuzberg::Footnote {
    fn from(val: Footnote) -> Self {
        Self {
            label: val.label,
            content: val.content.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Footnote> for Footnote {
    fn from(val: kreuzberg::Footnote) -> Self {
        Self {
            label: val.label,
            content: val.content.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocumentStructure> for kreuzberg::DocumentStructure {
    fn from(val: DocumentStructure) -> Self {
        Self {
            nodes: val.nodes.into_iter().map(Into::into).collect(),
            source_format: val.source_format,
            relationships: val.relationships.into_iter().map(Into::into).collect(),
            node_types: val.node_types.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentStructure> for DocumentStructure {
    fn from(val: kreuzberg::DocumentStructure) -> Self {
        Self {
            nodes: val.nodes.into_iter().map(Into::into).collect(),
            source_format: val.source_format,
            relationships: val.relationships.into_iter().map(Into::into).collect(),
            node_types: val.node_types.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocumentRelationship> for kreuzberg::DocumentRelationship {
    fn from(val: DocumentRelationship) -> Self {
        Self {
            source: kreuzberg::NodeIndex(val.source as u32),
            target: kreuzberg::NodeIndex(val.target as u32),
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentRelationship> for DocumentRelationship {
    fn from(val: kreuzberg::DocumentRelationship) -> Self {
        Self {
            source: val.source.0 as i32,
            target: val.target.0 as i32,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocumentNode> for kreuzberg::DocumentNode {
    fn from(val: DocumentNode) -> Self {
        Self {
            id: Default::default(),
            content: val.content.into(),
            parent: (val.parent.map(|v| v as u32)).map(kreuzberg::NodeIndex),
            children: (val.children.into_iter().map(|v| v as u32).collect::<Vec<_>>())
                .into_iter()
                .map(kreuzberg::NodeIndex)
                .collect(),
            content_layer: val.content_layer.into(),
            page: val.page.map(|v| v as u32),
            page_end: val.page_end.map(|v| v as u32),
            bbox: val.bbox.map(Into::into),
            annotations: val.annotations.into_iter().map(Into::into).collect(),
            attributes: val
                .attributes
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentNode> for DocumentNode {
    fn from(val: kreuzberg::DocumentNode) -> Self {
        Self {
            id: format!("{:?}", val.id),
            content: val.content.into(),
            parent: val.parent.map(|v| v.0).map(|v| v as i32),
            children: val
                .children
                .iter()
                .map(|v| v.0)
                .collect::<Vec<_>>()
                .iter()
                .map(|&v| v as i32)
                .collect(),
            content_layer: val.content_layer.into(),
            page: val.page.map(|v| v as i32),
            page_end: val.page_end.map(|v| v as i32),
            bbox: val.bbox.map(Into::into),
            annotations: val.annotations.into_iter().map(Into::into).collect(),
            attributes: val
                .attributes
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TableGrid> for kreuzberg::TableGrid {
    fn from(val: TableGrid) -> Self {
        Self {
            rows: val.rows as u32,
            cols: val.cols as u32,
            cells: val.cells.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TableGrid> for TableGrid {
    fn from(val: kreuzberg::TableGrid) -> Self {
        Self {
            rows: val.rows as i32,
            cols: val.cols as i32,
            cells: val.cells.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<GridCell> for kreuzberg::GridCell {
    fn from(val: GridCell) -> Self {
        Self {
            content: val.content,
            row: val.row as u32,
            col: val.col as u32,
            row_span: val.row_span as u32,
            col_span: val.col_span as u32,
            is_header: val.is_header,
            bbox: val.bbox.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::GridCell> for GridCell {
    fn from(val: kreuzberg::GridCell) -> Self {
        Self {
            content: val.content,
            row: val.row as i32,
            col: val.col as i32,
            row_span: val.row_span as i32,
            col_span: val.col_span as i32,
            is_header: val.is_header,
            bbox: val.bbox.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TextAnnotation> for kreuzberg::TextAnnotation {
    fn from(val: TextAnnotation) -> Self {
        Self {
            start: val.start as u32,
            end: val.end as u32,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TextAnnotation> for TextAnnotation {
    fn from(val: kreuzberg::TextAnnotation) -> Self {
        Self {
            start: val.start as i32,
            end: val.end as i32,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractionResult> for kreuzberg::ExtractionResult {
    fn from(val: ExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type.into(),
            metadata: val.metadata.into(),
            extraction_method: val.extraction_method.map(Into::into),
            tables: val.tables.into_iter().map(Into::into).collect(),
            detected_languages: val.detected_languages.map(|v| v.into_iter().collect()),
            chunks: val.chunks.map(|v| v.into_iter().map(Into::into).collect()),
            images: val.images.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
            elements: val.elements.map(|v| v.into_iter().map(Into::into).collect()),
            djot_content: val.djot_content.map(Into::into),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            document: val.document.map(Into::into),
            quality_score: val.quality_score,
            processing_warnings: val.processing_warnings.into_iter().map(Into::into).collect(),
            annotations: val.annotations.map(|v| v.into_iter().map(Into::into).collect()),
            children: val.children.map(|v| v.into_iter().map(Into::into).collect()),
            uris: val.uris.map(|v| v.into_iter().map(Into::into).collect()),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
            structured_output: val
                .structured_output
                .as_ref()
                .and_then(|s| serde_json::from_str(s).ok()),
            llm_usage: val.llm_usage.map(|v| v.into_iter().map(Into::into).collect()),
            formatted_content: val.formatted_content,
            ocr_internal_document: Default::default(),
            ..Default::default()
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionResult> for ExtractionResult {
    fn from(val: kreuzberg::ExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type.to_string(),
            metadata: val.metadata.into(),
            extraction_method: val.extraction_method.map(Into::into),
            tables: val.tables.into_iter().map(Into::into).collect(),
            detected_languages: val.detected_languages.map(|v| v.into_iter().collect()),
            chunks: val.chunks.map(|v| v.into_iter().map(Into::into).collect()),
            images: val.images.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
            elements: val.elements.map(|v| v.into_iter().map(Into::into).collect()),
            djot_content: val.djot_content.map(Into::into),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            document: val.document.map(Into::into),
            quality_score: val.quality_score,
            processing_warnings: val.processing_warnings.into_iter().map(Into::into).collect(),
            annotations: val.annotations.map(|v| v.into_iter().map(Into::into).collect()),
            children: val.children.map(|v| v.into_iter().map(Into::into).collect()),
            uris: val.uris.map(|v| v.into_iter().map(Into::into).collect()),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
            structured_output: val.structured_output.as_ref().map(ToString::to_string),
            llm_usage: val.llm_usage.map(|v| v.into_iter().map(Into::into).collect()),
            formatted_content: val.formatted_content,
            ocr_internal_document: val.ocr_internal_document.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ArchiveEntry> for kreuzberg::ArchiveEntry {
    fn from(val: ArchiveEntry) -> Self {
        Self {
            path: val.path,
            mime_type: val.mime_type,
            result: Box::new(val.result.into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ArchiveEntry> for ArchiveEntry {
    fn from(val: kreuzberg::ArchiveEntry) -> Self {
        Self {
            path: val.path,
            mime_type: val.mime_type,
            result: (*val.result).into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ProcessingWarning> for kreuzberg::ProcessingWarning {
    fn from(val: ProcessingWarning) -> Self {
        Self {
            source: val.source.into(),
            message: val.message.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ProcessingWarning> for ProcessingWarning {
    fn from(val: kreuzberg::ProcessingWarning) -> Self {
        Self {
            source: val.source.to_string(),
            message: val.message.to_string(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LlmUsage> for kreuzberg::LlmUsage {
    fn from(val: LlmUsage) -> Self {
        Self {
            model: val.model,
            source: val.source,
            input_tokens: val.input_tokens.map(|v| v as u64),
            output_tokens: val.output_tokens.map(|v| v as u64),
            total_tokens: val.total_tokens.map(|v| v as u64),
            estimated_cost: val.estimated_cost,
            finish_reason: val.finish_reason,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LlmUsage> for LlmUsage {
    fn from(val: kreuzberg::LlmUsage) -> Self {
        Self {
            model: val.model,
            source: val.source,
            input_tokens: val.input_tokens.map(|v| v as f64),
            output_tokens: val.output_tokens.map(|v| v as f64),
            total_tokens: val.total_tokens.map(|v| v as f64),
            estimated_cost: val.estimated_cost,
            finish_reason: val.finish_reason,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Chunk> for kreuzberg::Chunk {
    fn from(val: Chunk) -> Self {
        Self {
            content: val.content,
            chunk_type: val.chunk_type.into(),
            embedding: val.embedding.map(|v| v.into_iter().map(|x| x as f32).collect()),
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Chunk> for Chunk {
    fn from(val: kreuzberg::Chunk) -> Self {
        Self {
            content: val.content,
            chunk_type: val.chunk_type.into(),
            embedding: val.embedding.as_ref().map(|v| v.iter().map(|&x| x as f64).collect()),
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HeadingContext> for kreuzberg::HeadingContext {
    fn from(val: HeadingContext) -> Self {
        Self {
            headings: val.headings.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeadingContext> for HeadingContext {
    fn from(val: kreuzberg::HeadingContext) -> Self {
        Self {
            headings: val.headings.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HeadingLevel> for kreuzberg::HeadingLevel {
    fn from(val: HeadingLevel) -> Self {
        Self {
            level: val.level as u8,
            text: val.text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeadingLevel> for HeadingLevel {
    fn from(val: kreuzberg::HeadingLevel) -> Self {
        Self {
            level: val.level as i32,
            text: val.text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ChunkMetadata> for kreuzberg::ChunkMetadata {
    fn from(val: ChunkMetadata) -> Self {
        Self {
            byte_start: val.byte_start as usize,
            byte_end: val.byte_end as usize,
            token_count: val.token_count.map(|v| v as usize),
            chunk_index: val.chunk_index as usize,
            total_chunks: val.total_chunks as usize,
            first_page: val.first_page.map(|v| v as u32),
            last_page: val.last_page.map(|v| v as u32),
            heading_context: val.heading_context.map(Into::into),
            image_indices: val.image_indices.into_iter().map(|v| v as u32).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ChunkMetadata> for ChunkMetadata {
    fn from(val: kreuzberg::ChunkMetadata) -> Self {
        Self {
            byte_start: val.byte_start as f64,
            byte_end: val.byte_end as f64,
            token_count: val.token_count.map(|v| v as f64),
            chunk_index: val.chunk_index as f64,
            total_chunks: val.total_chunks as f64,
            first_page: val.first_page.map(|v| v as i32),
            last_page: val.last_page.map(|v| v as i32),
            heading_context: val.heading_context.map(Into::into),
            image_indices: val.image_indices.iter().map(|&v| v as i32).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractedImage> for kreuzberg::ExtractedImage {
    fn from(val: ExtractedImage) -> Self {
        Self {
            data: val.data.to_vec().into(),
            format: val.format.into(),
            image_index: val.image_index as u32,
            page_number: val.page_number.map(|v| v as u32),
            width: val.width.map(|v| v as u32),
            height: val.height.map(|v| v as u32),
            colorspace: val.colorspace,
            bits_per_component: val.bits_per_component.map(|v| v as u32),
            is_mask: val.is_mask,
            description: val.description,
            ocr_result: val.ocr_result.map(Into::into).map(Box::new),
            bounding_box: val.bounding_box.map(Into::into),
            source_path: val.source_path,
            image_kind: val.image_kind.map(Into::into),
            kind_confidence: val.kind_confidence.map(|v| v as f32),
            cluster_id: val.cluster_id.map(|v| v as u32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractedImage> for ExtractedImage {
    fn from(val: kreuzberg::ExtractedImage) -> Self {
        Self {
            data: val.data.to_vec().into(),
            format: val.format.to_string(),
            image_index: val.image_index as i32,
            page_number: val.page_number.map(|v| v as i32),
            width: val.width.map(|v| v as i32),
            height: val.height.map(|v| v as i32),
            colorspace: val.colorspace,
            bits_per_component: val.bits_per_component.map(|v| v as i32),
            is_mask: val.is_mask,
            description: val.description,
            ocr_result: val.ocr_result.map(|v| (*v).into()),
            bounding_box: val.bounding_box.map(Into::into),
            source_path: val.source_path,
            image_kind: val.image_kind.map(Into::into),
            kind_confidence: val.kind_confidence.map(|v| v as f64),
            cluster_id: val.cluster_id.map(|v| v as i32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<BoundingBox> for kreuzberg::BoundingBox {
    fn from(val: BoundingBox) -> Self {
        Self {
            x0: val.x0,
            y0: val.y0,
            x1: val.x1,
            y1: val.y1,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BoundingBox> for BoundingBox {
    fn from(val: kreuzberg::BoundingBox) -> Self {
        Self {
            x0: val.x0,
            y0: val.y0,
            x1: val.x1,
            y1: val.y1,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ElementMetadata> for kreuzberg::ElementMetadata {
    fn from(val: ElementMetadata) -> Self {
        Self {
            page_number: val.page_number.map(|v| v as u32),
            filename: val.filename,
            coordinates: val.coordinates.map(Into::into),
            element_index: val.element_index.map(|v| v as usize),
            additional: val.additional.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ElementMetadata> for ElementMetadata {
    fn from(val: kreuzberg::ElementMetadata) -> Self {
        Self {
            page_number: val.page_number.map(|v| v as i32),
            filename: val.filename,
            coordinates: val.coordinates.map(Into::into),
            element_index: val.element_index.map(|v| v as f64),
            additional: val.additional.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Element> for kreuzberg::Element {
    fn from(val: Element) -> Self {
        Self {
            element_id: Default::default(),
            element_type: val.element_type.into(),
            text: val.text,
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Element> for Element {
    fn from(val: kreuzberg::Element) -> Self {
        Self {
            element_id: format!("{:?}", val.element_id),
            element_type: val.element_type.into(),
            text: val.text,
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExcelWorkbook> for ExcelWorkbook {
    fn from(val: kreuzberg::ExcelWorkbook) -> Self {
        Self {
            sheets: val.sheets.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExcelSheet> for ExcelSheet {
    fn from(val: kreuzberg::ExcelSheet) -> Self {
        Self {
            name: val.name,
            markdown: val.markdown,
            row_count: val.row_count as f64,
            col_count: val.col_count as f64,
            cell_count: val.cell_count as f64,
            table_cells: val.table_cells.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::XmlExtractionResult> for XmlExtractionResult {
    fn from(val: kreuzberg::XmlExtractionResult) -> Self {
        Self {
            content: val.content,
            element_count: val.element_count as f64,
            unique_elements: val.unique_elements.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TextExtractionResult> for TextExtractionResult {
    fn from(val: kreuzberg::TextExtractionResult) -> Self {
        Self {
            content: val.content,
            line_count: val.line_count as f64,
            word_count: val.word_count as f64,
            character_count: val.character_count as f64,
            headers: val.headers.map(|v| v.into_iter().collect()),
            links: val.links.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
            code_blocks: val.code_blocks.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PptxExtractionResult> for PptxExtractionResult {
    fn from(val: kreuzberg::PptxExtractionResult) -> Self {
        Self {
            content: val.content,
            metadata: val.metadata.into(),
            slide_count: val.slide_count as f64,
            image_count: val.image_count as f64,
            table_count: val.table_count as f64,
            images: val.images.into_iter().map(Into::into).collect(),
            page_structure: val.page_structure.map(Into::into),
            page_contents: val.page_contents.map(|v| v.into_iter().map(Into::into).collect()),
            document: val.document.map(Into::into),
            hyperlinks: val.hyperlinks.iter().map(|i| format!("{:?}", i)).collect(),
            office_metadata: val
                .office_metadata
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailExtractionResult> for EmailExtractionResult {
    fn from(val: kreuzberg::EmailExtractionResult) -> Self {
        Self {
            subject: val.subject,
            from_email: val.from_email,
            to_emails: val.to_emails.into_iter().collect(),
            cc_emails: val.cc_emails.into_iter().collect(),
            bcc_emails: val.bcc_emails.into_iter().collect(),
            date: val.date,
            message_id: val.message_id,
            plain_text: val.plain_text,
            html_content: val.html_content,
            content: val.content,
            attachments: val.attachments.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailAttachment> for EmailAttachment {
    fn from(val: kreuzberg::EmailAttachment) -> Self {
        Self {
            name: val.name,
            filename: val.filename,
            mime_type: val.mime_type,
            size: val.size.map(|v| v as f64),
            is_image: val.is_image,
            data: val.data.map(|v| v.to_vec().into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrExtractionResult> for kreuzberg::OcrExtractionResult {
    fn from(val: OcrExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type,
            metadata: val
                .metadata
                .into_iter()
                .map(|(k, v)| (k.into(), serde_json::from_str(&v).unwrap_or_default()))
                .collect(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            internal_document: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrExtractionResult> for OcrExtractionResult {
    fn from(val: kreuzberg::OcrExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type,
            metadata: val
                .metadata
                .into_iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            internal_document: val.internal_document.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrTable> for kreuzberg::OcrTable {
    fn from(val: OcrTable) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number as u32,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrTable> for OcrTable {
    fn from(val: kreuzberg::OcrTable) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number as i32,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrTableBoundingBox> for kreuzberg::OcrTableBoundingBox {
    fn from(val: OcrTableBoundingBox) -> Self {
        Self {
            left: val.left as u32,
            top: val.top as u32,
            right: val.right as u32,
            bottom: val.bottom as u32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrTableBoundingBox> for OcrTableBoundingBox {
    fn from(val: kreuzberg::OcrTableBoundingBox) -> Self {
        Self {
            left: val.left as i32,
            top: val.top as i32,
            right: val.right as i32,
            bottom: val.bottom as i32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImagePreprocessingConfig> for kreuzberg::ImagePreprocessingConfig {
    fn from(val: ImagePreprocessingConfig) -> Self {
        Self {
            target_dpi: val.target_dpi,
            auto_rotate: val.auto_rotate,
            deskew: val.deskew,
            denoise: val.denoise,
            contrast_enhance: val.contrast_enhance,
            binarization_method: val.binarization_method,
            invert_colors: val.invert_colors,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImagePreprocessingConfig> for ImagePreprocessingConfig {
    fn from(val: kreuzberg::ImagePreprocessingConfig) -> Self {
        Self {
            target_dpi: val.target_dpi,
            auto_rotate: val.auto_rotate,
            deskew: val.deskew,
            denoise: val.denoise,
            contrast_enhance: val.contrast_enhance,
            binarization_method: val.binarization_method,
            invert_colors: val.invert_colors,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TesseractConfig> for kreuzberg::TesseractConfig {
    fn from(val: TesseractConfig) -> Self {
        Self {
            language: val.language,
            psm: val.psm,
            output_format: val.output_format,
            oem: val.oem,
            min_confidence: val.min_confidence,
            preprocessing: val.preprocessing.map(Into::into),
            enable_table_detection: val.enable_table_detection,
            table_min_confidence: val.table_min_confidence,
            table_column_threshold: val.table_column_threshold,
            table_row_threshold_ratio: val.table_row_threshold_ratio,
            use_cache: val.use_cache,
            classify_use_pre_adapted_templates: val.classify_use_pre_adapted_templates,
            language_model_ngram_on: val.language_model_ngram_on,
            tessedit_dont_blkrej_good_wds: val.tessedit_dont_blkrej_good_wds,
            tessedit_dont_rowrej_good_wds: val.tessedit_dont_rowrej_good_wds,
            tessedit_enable_dict_correction: val.tessedit_enable_dict_correction,
            tessedit_char_whitelist: val.tessedit_char_whitelist,
            tessedit_char_blacklist: val.tessedit_char_blacklist,
            tessedit_use_primary_params_model: val.tessedit_use_primary_params_model,
            textord_space_size_is_variable: val.textord_space_size_is_variable,
            thresholding_method: val.thresholding_method,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TesseractConfig> for TesseractConfig {
    fn from(val: kreuzberg::TesseractConfig) -> Self {
        Self {
            language: val.language,
            psm: val.psm,
            output_format: val.output_format,
            oem: val.oem,
            min_confidence: val.min_confidence,
            preprocessing: val.preprocessing.map(Into::into),
            enable_table_detection: val.enable_table_detection,
            table_min_confidence: val.table_min_confidence,
            table_column_threshold: val.table_column_threshold,
            table_row_threshold_ratio: val.table_row_threshold_ratio,
            use_cache: val.use_cache,
            classify_use_pre_adapted_templates: val.classify_use_pre_adapted_templates,
            language_model_ngram_on: val.language_model_ngram_on,
            tessedit_dont_blkrej_good_wds: val.tessedit_dont_blkrej_good_wds,
            tessedit_dont_rowrej_good_wds: val.tessedit_dont_rowrej_good_wds,
            tessedit_enable_dict_correction: val.tessedit_enable_dict_correction,
            tessedit_char_whitelist: val.tessedit_char_whitelist,
            tessedit_char_blacklist: val.tessedit_char_blacklist,
            tessedit_use_primary_params_model: val.tessedit_use_primary_params_model,
            textord_space_size_is_variable: val.textord_space_size_is_variable,
            thresholding_method: val.thresholding_method,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImagePreprocessingMetadata> for kreuzberg::ImagePreprocessingMetadata {
    fn from(val: ImagePreprocessingMetadata) -> Self {
        Self {
            original_dimensions: Default::default(),
            original_dpi: Default::default(),
            target_dpi: val.target_dpi,
            scale_factor: val.scale_factor,
            auto_adjusted: val.auto_adjusted,
            final_dpi: val.final_dpi,
            new_dimensions: Default::default(),
            resample_method: val.resample_method,
            dimension_clamped: val.dimension_clamped,
            calculated_dpi: val.calculated_dpi,
            skipped_resize: val.skipped_resize,
            resize_error: val.resize_error,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImagePreprocessingMetadata> for ImagePreprocessingMetadata {
    fn from(val: kreuzberg::ImagePreprocessingMetadata) -> Self {
        Self {
            original_dimensions: vec![val.original_dimensions.0 as _, val.original_dimensions.1 as _],
            original_dpi: vec![val.original_dpi.0 as _, val.original_dpi.1 as _],
            target_dpi: val.target_dpi,
            scale_factor: val.scale_factor,
            auto_adjusted: val.auto_adjusted,
            final_dpi: val.final_dpi,
            new_dimensions: val.new_dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            resample_method: val.resample_method,
            dimension_clamped: val.dimension_clamped,
            calculated_dpi: val.calculated_dpi,
            skipped_resize: val.skipped_resize,
            resize_error: val.resize_error,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Metadata> for kreuzberg::Metadata {
    fn from(val: Metadata) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            authors: val.authors.map(|v| v.into_iter().collect()),
            keywords: val.keywords.map(|v| v.into_iter().collect()),
            language: val.language,
            created_at: val.created_at,
            modified_at: val.modified_at,
            created_by: val.created_by,
            modified_by: val.modified_by,
            pages: val.pages.map(Into::into),
            format: Default::default(),
            image_preprocessing: val.image_preprocessing.map(Into::into),
            json_schema: val.json_schema.as_ref().and_then(|s| serde_json::from_str(s).ok()),
            error: val.error.map(Into::into),
            extraction_duration_ms: val.extraction_duration_ms.map(|v| v as u64),
            category: val.category,
            tags: val.tags.map(|v| v.into_iter().collect()),
            document_version: val.document_version,
            abstract_text: val.abstract_text,
            output_format: val.output_format,
            ocr_used: val.ocr_used,
            additional: val
                .additional
                .into_iter()
                .map(|(k, v)| (k.into(), serde_json::from_str(&v).unwrap_or_default()))
                .collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Metadata> for Metadata {
    fn from(val: kreuzberg::Metadata) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            authors: val.authors.map(|v| v.into_iter().collect()),
            keywords: val.keywords.map(|v| v.into_iter().collect()),
            language: val.language,
            created_at: val.created_at,
            modified_at: val.modified_at,
            created_by: val.created_by,
            modified_by: val.modified_by,
            pages: val.pages.map(Into::into),
            format: val.format.map(Into::into),
            image_preprocessing: val.image_preprocessing.map(Into::into),
            json_schema: val.json_schema.as_ref().map(ToString::to_string),
            error: val.error.map(Into::into),
            extraction_duration_ms: val.extraction_duration_ms.map(|v| v as f64),
            category: val.category,
            tags: val.tags.map(|v| v.into_iter().collect()),
            document_version: val.document_version,
            abstract_text: val.abstract_text,
            output_format: val.output_format,
            ocr_used: val.ocr_used,
            additional: val
                .additional
                .into_iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExcelMetadata> for kreuzberg::ExcelMetadata {
    fn from(val: ExcelMetadata) -> Self {
        Self {
            sheet_count: val.sheet_count.map(|v| v as u32),
            sheet_names: val.sheet_names.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExcelMetadata> for ExcelMetadata {
    fn from(val: kreuzberg::ExcelMetadata) -> Self {
        Self {
            sheet_count: val.sheet_count.map(|v| v as i32),
            sheet_names: val.sheet_names.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmailMetadata> for kreuzberg::EmailMetadata {
    fn from(val: EmailMetadata) -> Self {
        Self {
            from_email: val.from_email,
            from_name: val.from_name,
            to_emails: val.to_emails.into_iter().collect(),
            cc_emails: val.cc_emails.into_iter().collect(),
            bcc_emails: val.bcc_emails.into_iter().collect(),
            message_id: val.message_id,
            attachments: val.attachments.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailMetadata> for EmailMetadata {
    fn from(val: kreuzberg::EmailMetadata) -> Self {
        Self {
            from_email: val.from_email,
            from_name: val.from_name,
            to_emails: val.to_emails.into_iter().collect(),
            cc_emails: val.cc_emails.into_iter().collect(),
            bcc_emails: val.bcc_emails.into_iter().collect(),
            message_id: val.message_id,
            attachments: val.attachments.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ArchiveMetadata> for kreuzberg::ArchiveMetadata {
    fn from(val: ArchiveMetadata) -> Self {
        Self {
            format: val.format.into(),
            file_count: val.file_count as u32,
            file_list: val.file_list.into_iter().collect(),
            total_size: val.total_size as u64,
            compressed_size: val.compressed_size.map(|v| v as u64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ArchiveMetadata> for ArchiveMetadata {
    fn from(val: kreuzberg::ArchiveMetadata) -> Self {
        Self {
            format: val.format.to_string(),
            file_count: val.file_count as i32,
            file_list: val.file_list.into_iter().collect(),
            total_size: val.total_size as f64,
            compressed_size: val.compressed_size.map(|v| v as f64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImageMetadata> for kreuzberg::ImageMetadata {
    fn from(val: ImageMetadata) -> Self {
        Self {
            width: val.width as u32,
            height: val.height as u32,
            format: val.format,
            exif: val.exif.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageMetadata> for ImageMetadata {
    fn from(val: kreuzberg::ImageMetadata) -> Self {
        Self {
            width: val.width as i32,
            height: val.height as i32,
            format: val.format,
            exif: val.exif.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<XmlMetadata> for kreuzberg::XmlMetadata {
    fn from(val: XmlMetadata) -> Self {
        Self {
            element_count: val.element_count as u32,
            unique_elements: val.unique_elements.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::XmlMetadata> for XmlMetadata {
    fn from(val: kreuzberg::XmlMetadata) -> Self {
        Self {
            element_count: val.element_count as i32,
            unique_elements: val.unique_elements.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TextMetadata> for kreuzberg::TextMetadata {
    fn from(val: TextMetadata) -> Self {
        Self {
            line_count: val.line_count as u32,
            word_count: val.word_count as u32,
            character_count: val.character_count as u32,
            headers: val.headers.map(|v| v.into_iter().collect()),
            links: Default::default(),
            code_blocks: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TextMetadata> for TextMetadata {
    fn from(val: kreuzberg::TextMetadata) -> Self {
        Self {
            line_count: val.line_count as i32,
            word_count: val.word_count as i32,
            character_count: val.character_count as i32,
            headers: val.headers.map(|v| v.into_iter().collect()),
            links: val.links.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
            code_blocks: val.code_blocks.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HeaderMetadata> for kreuzberg::HeaderMetadata {
    fn from(val: HeaderMetadata) -> Self {
        Self {
            level: val.level as u8,
            text: val.text,
            id: val.id,
            depth: val.depth as u32,
            html_offset: val.html_offset as u32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeaderMetadata> for HeaderMetadata {
    fn from(val: kreuzberg::HeaderMetadata) -> Self {
        Self {
            level: val.level as i32,
            text: val.text,
            id: val.id,
            depth: val.depth as i32,
            html_offset: val.html_offset as i32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LinkMetadata> for kreuzberg::LinkMetadata {
    fn from(val: LinkMetadata) -> Self {
        Self {
            href: val.href,
            text: val.text,
            title: val.title,
            link_type: val.link_type.into(),
            rel: val.rel.into_iter().collect(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LinkMetadata> for LinkMetadata {
    fn from(val: kreuzberg::LinkMetadata) -> Self {
        Self {
            href: val.href,
            text: val.text,
            title: val.title,
            link_type: val.link_type.into(),
            rel: val.rel.into_iter().collect(),
            attributes: val
                .attributes
                .iter()
                .map(|(a, b)| vec![a.to_string(), b.to_string()])
                .collect::<Vec<Vec<String>>>(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImageMetadataType> for kreuzberg::ImageMetadataType {
    fn from(val: ImageMetadataType) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            dimensions: Default::default(),
            image_type: val.image_type.into(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageMetadataType> for ImageMetadataType {
    fn from(val: kreuzberg::ImageMetadataType) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            dimensions: val.dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            image_type: val.image_type.into(),
            attributes: val
                .attributes
                .iter()
                .map(|(a, b)| vec![a.to_string(), b.to_string()])
                .collect::<Vec<Vec<String>>>(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<StructuredData> for kreuzberg::StructuredData {
    fn from(val: StructuredData) -> Self {
        Self {
            data_type: val.data_type.into(),
            raw_json: val.raw_json,
            schema_type: val.schema_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::StructuredData> for StructuredData {
    fn from(val: kreuzberg::StructuredData) -> Self {
        Self {
            data_type: val.data_type.into(),
            raw_json: val.raw_json,
            schema_type: val.schema_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HtmlMetadata> for kreuzberg::HtmlMetadata {
    fn from(val: HtmlMetadata) -> Self {
        Self {
            title: val.title,
            description: val.description,
            keywords: val.keywords.into_iter().collect(),
            author: val.author,
            canonical_url: val.canonical_url,
            base_href: val.base_href,
            language: val.language,
            text_direction: val.text_direction.map(Into::into),
            open_graph: val.open_graph.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            twitter_card: val
                .twitter_card
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            meta_tags: val.meta_tags.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            headers: val.headers.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            structured_data: val.structured_data.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HtmlMetadata> for HtmlMetadata {
    fn from(val: kreuzberg::HtmlMetadata) -> Self {
        Self {
            title: val.title,
            description: val.description,
            keywords: val.keywords.into_iter().collect(),
            author: val.author,
            canonical_url: val.canonical_url,
            base_href: val.base_href,
            language: val.language,
            text_direction: val.text_direction.map(Into::into),
            open_graph: val.open_graph.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            twitter_card: val
                .twitter_card
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            meta_tags: val.meta_tags.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            headers: val.headers.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            structured_data: val.structured_data.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrMetadata> for kreuzberg::OcrMetadata {
    fn from(val: OcrMetadata) -> Self {
        Self {
            language: val.language,
            psm: val.psm,
            output_format: val.output_format,
            table_count: val.table_count as u32,
            table_rows: val.table_rows.map(|v| v as u32),
            table_cols: val.table_cols.map(|v| v as u32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrMetadata> for OcrMetadata {
    fn from(val: kreuzberg::OcrMetadata) -> Self {
        Self {
            language: val.language,
            psm: val.psm,
            output_format: val.output_format,
            table_count: val.table_count as i32,
            table_rows: val.table_rows.map(|v| v as i32),
            table_cols: val.table_cols.map(|v| v as i32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ErrorMetadata> for kreuzberg::ErrorMetadata {
    fn from(val: ErrorMetadata) -> Self {
        Self {
            error_type: val.error_type,
            message: val.message,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ErrorMetadata> for ErrorMetadata {
    fn from(val: kreuzberg::ErrorMetadata) -> Self {
        Self {
            error_type: val.error_type,
            message: val.message,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PptxMetadata> for kreuzberg::PptxMetadata {
    fn from(val: PptxMetadata) -> Self {
        Self {
            slide_count: val.slide_count as u32,
            slide_names: val.slide_names.into_iter().collect(),
            image_count: val.image_count.map(|v| v as u32),
            table_count: val.table_count.map(|v| v as u32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PptxMetadata> for PptxMetadata {
    fn from(val: kreuzberg::PptxMetadata) -> Self {
        Self {
            slide_count: val.slide_count as i32,
            slide_names: val.slide_names.into_iter().collect(),
            image_count: val.image_count.map(|v| v as i32),
            table_count: val.table_count.map(|v| v as i32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocxMetadata> for kreuzberg::DocxMetadata {
    fn from(val: DocxMetadata) -> Self {
        Self {
            core_properties: val.core_properties.map(Into::into),
            app_properties: val.app_properties.map(Into::into),
            custom_properties: val.custom_properties.map(|m| {
                m.into_iter()
                    .map(|(k, v)| (k.into(), serde_json::from_str(&v).unwrap_or_default()))
                    .collect()
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocxMetadata> for DocxMetadata {
    fn from(val: kreuzberg::DocxMetadata) -> Self {
        Self {
            core_properties: val.core_properties.map(Into::into),
            app_properties: val.app_properties.map(Into::into),
            custom_properties: val
                .custom_properties
                .map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<CsvMetadata> for kreuzberg::CsvMetadata {
    fn from(val: CsvMetadata) -> Self {
        Self {
            row_count: val.row_count as u32,
            column_count: val.column_count as u32,
            delimiter: val.delimiter,
            has_header: val.has_header,
            column_types: val.column_types.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CsvMetadata> for CsvMetadata {
    fn from(val: kreuzberg::CsvMetadata) -> Self {
        Self {
            row_count: val.row_count as i32,
            column_count: val.column_count as i32,
            delimiter: val.delimiter,
            has_header: val.has_header,
            column_types: val.column_types.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<BibtexMetadata> for kreuzberg::BibtexMetadata {
    fn from(val: BibtexMetadata) -> Self {
        Self {
            entry_count: val.entry_count as usize,
            citation_keys: val.citation_keys.into_iter().collect(),
            authors: val.authors.into_iter().collect(),
            year_range: val.year_range.map(Into::into),
            entry_types: val
                .entry_types
                .map(|m| m.into_iter().map(|(k, v)| (k, v as usize)).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BibtexMetadata> for BibtexMetadata {
    fn from(val: kreuzberg::BibtexMetadata) -> Self {
        Self {
            entry_count: val.entry_count as f64,
            citation_keys: val.citation_keys.into_iter().collect(),
            authors: val.authors.into_iter().collect(),
            year_range: val.year_range.map(Into::into),
            entry_types: val
                .entry_types
                .as_ref()
                .map(|m| m.iter().map(|(k, v)| (k.clone(), *v as f64)).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<CitationMetadata> for kreuzberg::CitationMetadata {
    fn from(val: CitationMetadata) -> Self {
        Self {
            citation_count: val.citation_count as usize,
            format: val.format,
            authors: val.authors.into_iter().collect(),
            year_range: val.year_range.map(Into::into),
            dois: val.dois.into_iter().collect(),
            keywords: val.keywords.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CitationMetadata> for CitationMetadata {
    fn from(val: kreuzberg::CitationMetadata) -> Self {
        Self {
            citation_count: val.citation_count as f64,
            format: val.format,
            authors: val.authors.into_iter().collect(),
            year_range: val.year_range.map(Into::into),
            dois: val.dois.into_iter().collect(),
            keywords: val.keywords.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<YearRange> for kreuzberg::YearRange {
    fn from(val: YearRange) -> Self {
        Self {
            min: val.min.map(|v| v as u32),
            max: val.max.map(|v| v as u32),
            years: val.years.into_iter().map(|v| v as u32).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::YearRange> for YearRange {
    fn from(val: kreuzberg::YearRange) -> Self {
        Self {
            min: val.min.map(|v| v as i32),
            max: val.max.map(|v| v as i32),
            years: val.years.iter().map(|&v| v as i32).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<FictionBookMetadata> for kreuzberg::FictionBookMetadata {
    fn from(val: FictionBookMetadata) -> Self {
        Self {
            genres: val.genres.into_iter().collect(),
            sequences: val.sequences.into_iter().collect(),
            annotation: val.annotation,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FictionBookMetadata> for FictionBookMetadata {
    fn from(val: kreuzberg::FictionBookMetadata) -> Self {
        Self {
            genres: val.genres.into_iter().collect(),
            sequences: val.sequences.into_iter().collect(),
            annotation: val.annotation,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DbfMetadata> for kreuzberg::DbfMetadata {
    fn from(val: DbfMetadata) -> Self {
        Self {
            record_count: val.record_count as usize,
            field_count: val.field_count as usize,
            fields: val.fields.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DbfMetadata> for DbfMetadata {
    fn from(val: kreuzberg::DbfMetadata) -> Self {
        Self {
            record_count: val.record_count as f64,
            field_count: val.field_count as f64,
            fields: val.fields.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DbfFieldInfo> for kreuzberg::DbfFieldInfo {
    fn from(val: DbfFieldInfo) -> Self {
        Self {
            name: val.name,
            field_type: val.field_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DbfFieldInfo> for DbfFieldInfo {
    fn from(val: kreuzberg::DbfFieldInfo) -> Self {
        Self {
            name: val.name,
            field_type: val.field_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JatsMetadata> for kreuzberg::JatsMetadata {
    fn from(val: JatsMetadata) -> Self {
        Self {
            copyright: val.copyright,
            license: val.license,
            history_dates: val
                .history_dates
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            contributor_roles: val.contributor_roles.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::JatsMetadata> for JatsMetadata {
    fn from(val: kreuzberg::JatsMetadata) -> Self {
        Self {
            copyright: val.copyright,
            license: val.license,
            history_dates: val
                .history_dates
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            contributor_roles: val.contributor_roles.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ContributorRole> for kreuzberg::ContributorRole {
    fn from(val: ContributorRole) -> Self {
        Self {
            name: val.name,
            role: val.role,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ContributorRole> for ContributorRole {
    fn from(val: kreuzberg::ContributorRole) -> Self {
        Self {
            name: val.name,
            role: val.role,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EpubMetadata> for kreuzberg::EpubMetadata {
    fn from(val: EpubMetadata) -> Self {
        Self {
            coverage: val.coverage,
            dc_format: val.dc_format,
            relation: val.relation,
            source: val.source,
            dc_type: val.dc_type,
            cover_image: val.cover_image,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EpubMetadata> for EpubMetadata {
    fn from(val: kreuzberg::EpubMetadata) -> Self {
        Self {
            coverage: val.coverage,
            dc_format: val.dc_format,
            relation: val.relation,
            source: val.source,
            dc_type: val.dc_type,
            cover_image: val.cover_image,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PstMetadata> for kreuzberg::PstMetadata {
    fn from(val: PstMetadata) -> Self {
        Self {
            message_count: val.message_count as usize,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PstMetadata> for PstMetadata {
    fn from(val: kreuzberg::PstMetadata) -> Self {
        Self {
            message_count: val.message_count as f64,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrConfidence> for kreuzberg::OcrConfidence {
    fn from(val: OcrConfidence) -> Self {
        Self {
            detection: val.detection,
            recognition: val.recognition,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrConfidence> for OcrConfidence {
    fn from(val: kreuzberg::OcrConfidence) -> Self {
        Self {
            detection: val.detection,
            recognition: val.recognition,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrRotation> for kreuzberg::OcrRotation {
    fn from(val: OcrRotation) -> Self {
        Self {
            angle_degrees: val.angle_degrees,
            confidence: val.confidence,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrRotation> for OcrRotation {
    fn from(val: kreuzberg::OcrRotation) -> Self {
        Self {
            angle_degrees: val.angle_degrees,
            confidence: val.confidence,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrElement> for kreuzberg::OcrElement {
    fn from(val: OcrElement) -> Self {
        Self {
            text: val.text,
            geometry: val.geometry.into(),
            confidence: val.confidence.into(),
            level: val.level.into(),
            rotation: val.rotation.map(Into::into),
            page_number: val.page_number as u32,
            parent_id: val.parent_id,
            backend_metadata: val
                .backend_metadata
                .into_iter()
                .map(|(k, v)| (k.into(), serde_json::from_str(&v).unwrap_or_default()))
                .collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrElement> for OcrElement {
    fn from(val: kreuzberg::OcrElement) -> Self {
        Self {
            text: val.text,
            geometry: val.geometry.into(),
            confidence: val.confidence.into(),
            level: val.level.into(),
            rotation: val.rotation.map(Into::into),
            page_number: val.page_number as i32,
            parent_id: val.parent_id,
            backend_metadata: val
                .backend_metadata
                .into_iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrElementConfig> for kreuzberg::OcrElementConfig {
    fn from(val: OcrElementConfig) -> Self {
        Self {
            include_elements: val.include_elements,
            min_level: val.min_level.into(),
            min_confidence: val.min_confidence,
            build_hierarchy: val.build_hierarchy,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrElementConfig> for OcrElementConfig {
    fn from(val: kreuzberg::OcrElementConfig) -> Self {
        Self {
            include_elements: val.include_elements,
            min_level: val.min_level.into(),
            min_confidence: val.min_confidence,
            build_hierarchy: val.build_hierarchy,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageStructure> for kreuzberg::PageStructure {
    fn from(val: PageStructure) -> Self {
        Self {
            total_count: val.total_count as u32,
            unit_type: val.unit_type.into(),
            boundaries: val.boundaries.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageStructure> for PageStructure {
    fn from(val: kreuzberg::PageStructure) -> Self {
        Self {
            total_count: val.total_count as i32,
            unit_type: val.unit_type.into(),
            boundaries: val.boundaries.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageBoundary> for kreuzberg::PageBoundary {
    fn from(val: PageBoundary) -> Self {
        Self {
            byte_start: val.byte_start as usize,
            byte_end: val.byte_end as usize,
            page_number: val.page_number as u32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageBoundary> for PageBoundary {
    fn from(val: kreuzberg::PageBoundary) -> Self {
        Self {
            byte_start: val.byte_start as f64,
            byte_end: val.byte_end as f64,
            page_number: val.page_number as i32,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageInfo> for kreuzberg::PageInfo {
    fn from(val: PageInfo) -> Self {
        Self {
            number: val.number as u32,
            title: val.title,
            dimensions: Default::default(),
            image_count: val.image_count.map(|v| v as u32),
            table_count: val.table_count.map(|v| v as u32),
            hidden: val.hidden,
            is_blank: val.is_blank,
            has_vector_graphics: val.has_vector_graphics,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageInfo> for PageInfo {
    fn from(val: kreuzberg::PageInfo) -> Self {
        Self {
            number: val.number as i32,
            title: val.title,
            dimensions: val.dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            image_count: val.image_count.map(|v| v as i32),
            table_count: val.table_count.map(|v| v as i32),
            hidden: val.hidden,
            is_blank: val.is_blank,
            has_vector_graphics: val.has_vector_graphics,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageContent> for kreuzberg::PageContent {
    fn from(val: PageContent) -> Self {
        Self {
            page_number: val.page_number as u32,
            content: val.content,
            tables: val.tables.into_iter().map(|v| std::sync::Arc::new(v.into())).collect(),
            image_indices: val.image_indices.into_iter().map(|v| v as u32).collect(),
            hierarchy: val.hierarchy.map(Into::into),
            is_blank: val.is_blank,
            layout_regions: val.layout_regions.map(|v| v.into_iter().map(Into::into).collect()),
            speaker_notes: val.speaker_notes,
            section_name: val.section_name,
            sheet_name: val.sheet_name,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageContent> for PageContent {
    fn from(val: kreuzberg::PageContent) -> Self {
        Self {
            page_number: val.page_number as i32,
            content: val.content,
            tables: val.tables.into_iter().map(|v| (*v).clone().into()).collect(),
            image_indices: val.image_indices.iter().map(|&v| v as i32).collect(),
            hierarchy: val.hierarchy.map(Into::into),
            is_blank: val.is_blank,
            layout_regions: val.layout_regions.map(|v| v.into_iter().map(Into::into).collect()),
            speaker_notes: val.speaker_notes,
            section_name: val.section_name,
            sheet_name: val.sheet_name,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LayoutRegion> for kreuzberg::LayoutRegion {
    fn from(val: LayoutRegion) -> Self {
        Self {
            class_name: val.class_name,
            confidence: val.confidence,
            bounding_box: val.bounding_box.into(),
            area_fraction: val.area_fraction,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LayoutRegion> for LayoutRegion {
    fn from(val: kreuzberg::LayoutRegion) -> Self {
        Self {
            class_name: val.class_name,
            confidence: val.confidence,
            bounding_box: val.bounding_box.into(),
            area_fraction: val.area_fraction,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageHierarchy> for kreuzberg::PageHierarchy {
    fn from(val: PageHierarchy) -> Self {
        Self {
            block_count: val.block_count as u32,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageHierarchy> for PageHierarchy {
    fn from(val: kreuzberg::PageHierarchy) -> Self {
        Self {
            block_count: val.block_count as i32,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HierarchicalBlock> for kreuzberg::HierarchicalBlock {
    fn from(val: HierarchicalBlock) -> Self {
        Self {
            text: val.text,
            font_size: val.font_size as f32,
            level: val.level,
            bbox: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HierarchicalBlock> for HierarchicalBlock {
    fn from(val: kreuzberg::HierarchicalBlock) -> Self {
        Self {
            text: val.text,
            font_size: val.font_size as f64,
            level: val.level,
            bbox: val.bbox.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<CellChange> for kreuzberg::CellChange {
    fn from(val: CellChange) -> Self {
        Self {
            row: val.row as usize,
            col: val.col as usize,
            from: val.from,
            to: val.to,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CellChange> for CellChange {
    fn from(val: kreuzberg::CellChange) -> Self {
        Self {
            row: val.row as f64,
            col: val.col as f64,
            from: val.from,
            to: val.to,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocumentRevision> for kreuzberg::DocumentRevision {
    fn from(val: DocumentRevision) -> Self {
        Self {
            revision_id: val.revision_id,
            author: val.author,
            timestamp: val.timestamp,
            kind: val.kind.into(),
            anchor: val.anchor.map(Into::into),
            delta: val.delta.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentRevision> for DocumentRevision {
    fn from(val: kreuzberg::DocumentRevision) -> Self {
        Self {
            revision_id: val.revision_id,
            author: val.author,
            timestamp: val.timestamp,
            kind: val.kind.into(),
            anchor: val.anchor.map(Into::into),
            delta: val.delta.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<RevisionDelta> for kreuzberg::RevisionDelta {
    fn from(val: RevisionDelta) -> Self {
        Self {
            content: val.content.into_iter().map(Into::into).collect(),
            table_changes: val.table_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::RevisionDelta> for RevisionDelta {
    fn from(val: kreuzberg::RevisionDelta) -> Self {
        Self {
            content: val.content.into_iter().map(Into::into).collect(),
            table_changes: val.table_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Table> for kreuzberg::Table {
    fn from(val: Table) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number as u32,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Table> for Table {
    fn from(val: kreuzberg::Table) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number as i32,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TableCell> for TableCell {
    fn from(val: kreuzberg::TableCell) -> Self {
        Self {
            content: val.content,
            row_span: val.row_span as i32,
            col_span: val.col_span as i32,
            is_header: val.is_header,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractedUri> for kreuzberg::ExtractedUri {
    fn from(val: ExtractedUri) -> Self {
        Self {
            url: val.url,
            label: val.label,
            page: val.page.map(|v| v as u32),
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractedUri> for ExtractedUri {
    fn from(val: kreuzberg::ExtractedUri) -> Self {
        Self {
            url: val.url,
            label: val.label,
            page: val.page.map(|v| v as i32),
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::api::DetectResponse> for DetectResponse {
    fn from(val: kreuzberg::api::DetectResponse) -> Self {
        Self {
            mime_type: val.mime_type,
            filename: val.filename,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DiffOptions> for kreuzberg::DiffOptions {
    fn from(val: DiffOptions) -> Self {
        Self {
            include_metadata: val.include_metadata,
            include_embedded: val.include_embedded,
            max_content_chars: val.max_content_chars.map(|v| v as usize),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DiffOptions> for DiffOptions {
    fn from(val: kreuzberg::DiffOptions) -> Self {
        Self {
            include_metadata: val.include_metadata,
            include_embedded: val.include_embedded,
            max_content_chars: val.max_content_chars.map(|v| v as f64),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractionDiff> for kreuzberg::ExtractionDiff {
    fn from(val: ExtractionDiff) -> Self {
        Self {
            content_diff: val.content_diff.into_iter().map(Into::into).collect(),
            tables_added: val.tables_added.into_iter().map(Into::into).collect(),
            tables_removed: val.tables_removed.into_iter().map(Into::into).collect(),
            tables_changed: val.tables_changed.into_iter().map(Into::into).collect(),
            metadata_changed: serde_json::from_str(&val.metadata_changed).unwrap_or_default(),
            embedded_changes: val.embedded_changes.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionDiff> for ExtractionDiff {
    fn from(val: kreuzberg::ExtractionDiff) -> Self {
        Self {
            content_diff: val.content_diff.into_iter().map(Into::into).collect(),
            tables_added: val.tables_added.into_iter().map(Into::into).collect(),
            tables_removed: val.tables_removed.into_iter().map(Into::into).collect(),
            tables_changed: val.tables_changed.into_iter().map(Into::into).collect(),
            metadata_changed: val.metadata_changed.to_string(),
            embedded_changes: val.embedded_changes.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DiffHunk> for kreuzberg::DiffHunk {
    fn from(val: DiffHunk) -> Self {
        Self {
            from_line: val.from_line as usize,
            from_count: val.from_count as usize,
            to_line: val.to_line as usize,
            to_count: val.to_count as usize,
            lines: val.lines.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DiffHunk> for DiffHunk {
    fn from(val: kreuzberg::DiffHunk) -> Self {
        Self {
            from_line: val.from_line as f64,
            from_count: val.from_count as f64,
            to_line: val.to_line as f64,
            to_count: val.to_count as f64,
            lines: val.lines.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TableDiff> for kreuzberg::TableDiff {
    fn from(val: TableDiff) -> Self {
        Self {
            from_index: val.from_index as usize,
            to_index: val.to_index as usize,
            cell_changes: val.cell_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TableDiff> for TableDiff {
    fn from(val: kreuzberg::TableDiff) -> Self {
        Self {
            from_index: val.from_index as f64,
            to_index: val.to_index as f64,
            cell_changes: val.cell_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmbeddedChanges> for kreuzberg::EmbeddedChanges {
    fn from(val: EmbeddedChanges) -> Self {
        Self {
            added: val.added.into_iter().map(Into::into).collect(),
            removed: val.removed.into_iter().map(Into::into).collect(),
            changed: val.changed.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddedChanges> for EmbeddedChanges {
    fn from(val: kreuzberg::EmbeddedChanges) -> Self {
        Self {
            added: val.added.into_iter().map(Into::into).collect(),
            removed: val.removed.into_iter().map(Into::into).collect(),
            changed: val.changed.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmbeddedDiff> for kreuzberg::EmbeddedDiff {
    fn from(val: EmbeddedDiff) -> Self {
        Self {
            path: val.path,
            diff: Box::new(val.diff.into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddedDiff> for EmbeddedDiff {
    fn from(val: kreuzberg::EmbeddedDiff) -> Self {
        Self {
            path: val.path,
            diff: (*val.diff).into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmbeddingPreset> for kreuzberg::EmbeddingPreset {
    fn from(val: EmbeddingPreset) -> Self {
        Self {
            name: val.name,
            chunk_size: val.chunk_size as usize,
            overlap: val.overlap as usize,
            model_repo: val.model_repo,
            pooling: val.pooling,
            model_file: val.model_file,
            dimensions: val.dimensions as usize,
            description: val.description,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddingPreset> for EmbeddingPreset {
    fn from(val: kreuzberg::EmbeddingPreset) -> Self {
        Self {
            name: val.name,
            chunk_size: val.chunk_size as f64,
            overlap: val.overlap as f64,
            model_repo: val.model_repo,
            pooling: val.pooling,
            model_file: val.model_file,
            dimensions: val.dimensions as f64,
            description: val.description,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<YakeParams> for kreuzberg::YakeParams {
    fn from(val: YakeParams) -> Self {
        Self {
            window_size: val.window_size as usize,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::YakeParams> for YakeParams {
    fn from(val: kreuzberg::YakeParams) -> Self {
        Self {
            window_size: val.window_size as f64,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<RakeParams> for kreuzberg::RakeParams {
    fn from(val: RakeParams) -> Self {
        Self {
            min_word_length: val.min_word_length as usize,
            max_words_per_phrase: val.max_words_per_phrase as usize,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::RakeParams> for RakeParams {
    fn from(val: kreuzberg::RakeParams) -> Self {
        Self {
            min_word_length: val.min_word_length as f64,
            max_words_per_phrase: val.max_words_per_phrase as f64,
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<KeywordConfig> for kreuzberg::KeywordConfig {
    fn from(val: KeywordConfig) -> Self {
        Self {
            algorithm: val.algorithm.into(),
            max_keywords: val.max_keywords as usize,
            min_score: val.min_score as f32,
            ngram_range: Default::default(),
            language: val.language,
            ..Default::default()
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::KeywordConfig> for KeywordConfig {
    fn from(val: kreuzberg::KeywordConfig) -> Self {
        Self {
            algorithm: val.algorithm.into(),
            max_keywords: val.max_keywords as f64,
            min_score: val.min_score as f64,
            ngram_range: vec![val.ngram_range.0 as _, val.ngram_range.1 as _],
            language: val.language,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Keyword> for kreuzberg::Keyword {
    fn from(val: Keyword) -> Self {
        Self {
            text: val.text,
            score: val.score as f32,
            algorithm: val.algorithm.into(),
            positions: val.positions.map(|v| v.into_iter().map(|x| x as usize).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Keyword> for Keyword {
    fn from(val: kreuzberg::Keyword) -> Self {
        Self {
            text: val.text,
            score: val.score as f64,
            algorithm: val.algorithm.into(),
            positions: val.positions.as_ref().map(|v| v.iter().map(|&x| x as f64).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PaddleOcrConfig> for kreuzberg::PaddleOcrConfig {
    fn from(val: PaddleOcrConfig) -> Self {
        Self {
            language: val.language,
            cache_dir: val.cache_dir.map(Into::into),
            use_angle_cls: val.use_angle_cls,
            enable_table_detection: val.enable_table_detection,
            det_db_thresh: val.det_db_thresh as f32,
            det_db_box_thresh: val.det_db_box_thresh as f32,
            det_db_unclip_ratio: val.det_db_unclip_ratio as f32,
            det_limit_side_len: val.det_limit_side_len as u32,
            rec_batch_num: val.rec_batch_num as u32,
            padding: val.padding as u32,
            drop_score: val.drop_score as f32,
            model_tier: val.model_tier,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PaddleOcrConfig> for PaddleOcrConfig {
    fn from(val: kreuzberg::PaddleOcrConfig) -> Self {
        Self {
            language: val.language,
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            use_angle_cls: val.use_angle_cls,
            enable_table_detection: val.enable_table_detection,
            det_db_thresh: val.det_db_thresh as f64,
            det_db_box_thresh: val.det_db_box_thresh as f64,
            det_db_unclip_ratio: val.det_db_unclip_ratio as f64,
            det_limit_side_len: val.det_limit_side_len as i32,
            rec_batch_num: val.rec_batch_num as i32,
            padding: val.padding as i32,
            drop_score: val.drop_score as f64,
            model_tier: val.model_tier,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ModelPaths> for ModelPaths {
    fn from(val: kreuzberg::ModelPaths) -> Self {
        Self {
            det_model: val.det_model.to_string_lossy().to_string(),
            cls_model: val.cls_model.to_string_lossy().to_string(),
            rec_model: val.rec_model.to_string_lossy().to_string(),
            dict_file: val.dict_file.to_string_lossy().to_string(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OrientationResult> for OrientationResult {
    fn from(val: kreuzberg::OrientationResult) -> Self {
        Self {
            degrees: val.degrees as i32,
            confidence: val.confidence as f64,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BBox> for BBox {
    fn from(val: kreuzberg::BBox) -> Self {
        Self {
            x1: val.x1 as f64,
            y1: val.y1 as f64,
            x2: val.x2 as f64,
            y2: val.y2 as f64,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LayoutDetection> for LayoutDetection {
    fn from(val: kreuzberg::LayoutDetection) -> Self {
        Self {
            class_name: val.class_name.into(),
            confidence: val.confidence as f64,
            bbox: val.bbox.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::RecognizedTable> for RecognizedTable {
    fn from(val: kreuzberg::RecognizedTable) -> Self {
        Self {
            detection_bbox: val.detection_bbox.into(),
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DetectionResult> for DetectionResult {
    fn from(val: kreuzberg::DetectionResult) -> Self {
        Self {
            page_width: val.page_width as i32,
            page_height: val.page_height as i32,
            detections: val.detections.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::pdf::embedded_files::EmbeddedFile> for EmbeddedFile {
    fn from(val: kreuzberg::pdf::embedded_files::EmbeddedFile) -> Self {
        Self {
            name: val.name,
            data: val.data.to_vec().into(),
            compressed_size: val.compressed_size as f64,
            mime_type: val.mime_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PdfMetadata> for kreuzberg::pdf::metadata::PdfMetadata {
    fn from(val: PdfMetadata) -> Self {
        Self {
            pdf_version: val.pdf_version,
            producer: val.producer,
            is_encrypted: val.is_encrypted,
            width: val.width.map(|v| v as i64),
            height: val.height.map(|v| v as i64),
            page_count: val.page_count.map(|v| v as u32),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::pdf::metadata::PdfMetadata> for PdfMetadata {
    fn from(val: kreuzberg::pdf::metadata::PdfMetadata) -> Self {
        Self {
            pdf_version: val.pdf_version,
            producer: val.producer,
            is_encrypted: val.is_encrypted,
            width: val.width.map(|v| v as f64),
            height: val.height.map(|v| v as f64),
            page_count: val.page_count.map(|v| v as i32),
        }
    }
}

impl From<ExecutionProviderType> for kreuzberg::ExecutionProviderType {
    fn from(val: ExecutionProviderType) -> Self {
        match val {
            ExecutionProviderType::Auto => Self::Auto,
            ExecutionProviderType::Cpu => Self::Cpu,
            ExecutionProviderType::CoreMl => Self::CoreMl,
            ExecutionProviderType::Cuda => Self::Cuda,
            ExecutionProviderType::TensorRt => Self::TensorRt,
        }
    }
}

impl From<kreuzberg::ExecutionProviderType> for ExecutionProviderType {
    fn from(val: kreuzberg::ExecutionProviderType) -> Self {
        match val {
            kreuzberg::ExecutionProviderType::Auto => Self::Auto,
            kreuzberg::ExecutionProviderType::Cpu => Self::Cpu,
            kreuzberg::ExecutionProviderType::CoreMl => Self::CoreMl,
            kreuzberg::ExecutionProviderType::Cuda => Self::Cuda,
            kreuzberg::ExecutionProviderType::TensorRt => Self::TensorRt,
        }
    }
}

impl From<kreuzberg::OutputFormat> for OutputFormat {
    fn from(val: kreuzberg::OutputFormat) -> Self {
        match val {
            kreuzberg::OutputFormat::Plain => Self {
                format_type: "Plain".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Markdown => Self {
                format_type: "Markdown".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Djot => Self {
                format_type: "Djot".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Html => Self {
                format_type: "Html".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Json => Self {
                format_type: "Json".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Structured => Self {
                format_type: "Structured".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Custom(_0) => Self {
                format_type: "Custom".to_string(),
                custom: Some(_0.into()),
                ..Default::default()
            },
        }
    }
}

impl From<OutputFormat> for kreuzberg::OutputFormat {
    fn from(val: OutputFormat) -> Self {
        match val.format_type.as_str() {
            "Plain" => Self::Plain,
            "Markdown" => Self::Markdown,
            "Djot" => Self::Djot,
            "Html" => Self::Html,
            "Json" => Self::Json,
            "Structured" => Self::Structured,
            "Custom" => Self::Custom(val.custom.unwrap_or_default().into()),
            _ => Self::default(),
        }
    }
}

impl From<HtmlTheme> for kreuzberg::HtmlTheme {
    fn from(val: HtmlTheme) -> Self {
        match val {
            HtmlTheme::Default => Self::Default,
            HtmlTheme::GitHub => Self::GitHub,
            HtmlTheme::Dark => Self::Dark,
            HtmlTheme::Light => Self::Light,
            HtmlTheme::Unstyled => Self::Unstyled,
        }
    }
}

impl From<kreuzberg::HtmlTheme> for HtmlTheme {
    fn from(val: kreuzberg::HtmlTheme) -> Self {
        match val {
            kreuzberg::HtmlTheme::Default => Self::Default,
            kreuzberg::HtmlTheme::GitHub => Self::GitHub,
            kreuzberg::HtmlTheme::Dark => Self::Dark,
            kreuzberg::HtmlTheme::Light => Self::Light,
            kreuzberg::HtmlTheme::Unstyled => Self::Unstyled,
        }
    }
}

impl From<TableModel> for kreuzberg::TableModel {
    fn from(val: TableModel) -> Self {
        match val {
            TableModel::Tatr => Self::Tatr,
            TableModel::SlanetWired => Self::SlanetWired,
            TableModel::SlanetWireless => Self::SlanetWireless,
            TableModel::SlanetPlus => Self::SlanetPlus,
            TableModel::SlanetAuto => Self::SlanetAuto,
            TableModel::Disabled => Self::Disabled,
        }
    }
}

impl From<kreuzberg::TableModel> for TableModel {
    fn from(val: kreuzberg::TableModel) -> Self {
        match val {
            kreuzberg::TableModel::Tatr => Self::Tatr,
            kreuzberg::TableModel::SlanetWired => Self::SlanetWired,
            kreuzberg::TableModel::SlanetWireless => Self::SlanetWireless,
            kreuzberg::TableModel::SlanetPlus => Self::SlanetPlus,
            kreuzberg::TableModel::SlanetAuto => Self::SlanetAuto,
            kreuzberg::TableModel::Disabled => Self::Disabled,
        }
    }
}

impl From<ChunkerType> for kreuzberg::ChunkerType {
    fn from(val: ChunkerType) -> Self {
        match val {
            ChunkerType::Text => Self::Text,
            ChunkerType::Markdown => Self::Markdown,
            ChunkerType::Yaml => Self::Yaml,
            ChunkerType::Semantic => Self::Semantic,
        }
    }
}

impl From<kreuzberg::ChunkerType> for ChunkerType {
    fn from(val: kreuzberg::ChunkerType) -> Self {
        match val {
            kreuzberg::ChunkerType::Text => Self::Text,
            kreuzberg::ChunkerType::Markdown => Self::Markdown,
            kreuzberg::ChunkerType::Yaml => Self::Yaml,
            kreuzberg::ChunkerType::Semantic => Self::Semantic,
        }
    }
}

impl From<CodeContentMode> for kreuzberg::CodeContentMode {
    fn from(val: CodeContentMode) -> Self {
        match val {
            CodeContentMode::Chunks => Self::Chunks,
            CodeContentMode::Raw => Self::Raw,
            CodeContentMode::Structure => Self::Structure,
        }
    }
}

impl From<kreuzberg::CodeContentMode> for CodeContentMode {
    fn from(val: kreuzberg::CodeContentMode) -> Self {
        match val {
            kreuzberg::CodeContentMode::Chunks => Self::Chunks,
            kreuzberg::CodeContentMode::Raw => Self::Raw,
            kreuzberg::CodeContentMode::Structure => Self::Structure,
        }
    }
}

impl From<kreuzberg::extraction::transform::ListType> for ListType {
    fn from(val: kreuzberg::extraction::transform::ListType) -> Self {
        match val {
            kreuzberg::extraction::transform::ListType::Bullet => Self::Bullet,
            kreuzberg::extraction::transform::ListType::Numbered => Self::Numbered,
            kreuzberg::extraction::transform::ListType::Lettered => Self::Lettered,
            kreuzberg::extraction::transform::ListType::Indented => Self::Indented,
        }
    }
}

impl From<kreuzberg::OcrBackendType> for OcrBackendType {
    fn from(val: kreuzberg::OcrBackendType) -> Self {
        match val {
            kreuzberg::OcrBackendType::Tesseract => Self::Tesseract,
            kreuzberg::OcrBackendType::EasyOCR => Self::EasyOCR,
            kreuzberg::OcrBackendType::PaddleOCR => Self::PaddleOCR,
            kreuzberg::OcrBackendType::Custom => Self::Custom,
        }
    }
}

impl From<kreuzberg::ProcessingStage> for ProcessingStage {
    fn from(val: kreuzberg::ProcessingStage) -> Self {
        match val {
            kreuzberg::ProcessingStage::Early => Self::Early,
            kreuzberg::ProcessingStage::Middle => Self::Middle,
            kreuzberg::ProcessingStage::Late => Self::Late,
        }
    }
}

impl From<ReductionLevel> for kreuzberg::ReductionLevel {
    fn from(val: ReductionLevel) -> Self {
        match val {
            ReductionLevel::Off => Self::Off,
            ReductionLevel::Light => Self::Light,
            ReductionLevel::Moderate => Self::Moderate,
            ReductionLevel::Aggressive => Self::Aggressive,
            ReductionLevel::Maximum => Self::Maximum,
        }
    }
}

impl From<kreuzberg::ReductionLevel> for ReductionLevel {
    fn from(val: kreuzberg::ReductionLevel) -> Self {
        match val {
            kreuzberg::ReductionLevel::Off => Self::Off,
            kreuzberg::ReductionLevel::Light => Self::Light,
            kreuzberg::ReductionLevel::Moderate => Self::Moderate,
            kreuzberg::ReductionLevel::Aggressive => Self::Aggressive,
            kreuzberg::ReductionLevel::Maximum => Self::Maximum,
        }
    }
}

impl From<PdfAnnotationType> for kreuzberg::PdfAnnotationType {
    fn from(val: PdfAnnotationType) -> Self {
        match val {
            PdfAnnotationType::Text => Self::Text,
            PdfAnnotationType::Highlight => Self::Highlight,
            PdfAnnotationType::Link => Self::Link,
            PdfAnnotationType::Stamp => Self::Stamp,
            PdfAnnotationType::Underline => Self::Underline,
            PdfAnnotationType::StrikeOut => Self::StrikeOut,
            PdfAnnotationType::Other => Self::Other,
        }
    }
}

impl From<kreuzberg::PdfAnnotationType> for PdfAnnotationType {
    fn from(val: kreuzberg::PdfAnnotationType) -> Self {
        match val {
            kreuzberg::PdfAnnotationType::Text => Self::Text,
            kreuzberg::PdfAnnotationType::Highlight => Self::Highlight,
            kreuzberg::PdfAnnotationType::Link => Self::Link,
            kreuzberg::PdfAnnotationType::Stamp => Self::Stamp,
            kreuzberg::PdfAnnotationType::Underline => Self::Underline,
            kreuzberg::PdfAnnotationType::StrikeOut => Self::StrikeOut,
            kreuzberg::PdfAnnotationType::Other => Self::Other,
        }
    }
}

impl From<BlockType> for kreuzberg::BlockType {
    fn from(val: BlockType) -> Self {
        match val {
            BlockType::Paragraph => Self::Paragraph,
            BlockType::Heading => Self::Heading,
            BlockType::Blockquote => Self::Blockquote,
            BlockType::CodeBlock => Self::CodeBlock,
            BlockType::ListItem => Self::ListItem,
            BlockType::OrderedList => Self::OrderedList,
            BlockType::BulletList => Self::BulletList,
            BlockType::TaskList => Self::TaskList,
            BlockType::DefinitionList => Self::DefinitionList,
            BlockType::DefinitionTerm => Self::DefinitionTerm,
            BlockType::DefinitionDescription => Self::DefinitionDescription,
            BlockType::Div => Self::Div,
            BlockType::Section => Self::Section,
            BlockType::ThematicBreak => Self::ThematicBreak,
            BlockType::RawBlock => Self::RawBlock,
            BlockType::MathDisplay => Self::MathDisplay,
        }
    }
}

impl From<kreuzberg::BlockType> for BlockType {
    fn from(val: kreuzberg::BlockType) -> Self {
        match val {
            kreuzberg::BlockType::Paragraph => Self::Paragraph,
            kreuzberg::BlockType::Heading => Self::Heading,
            kreuzberg::BlockType::Blockquote => Self::Blockquote,
            kreuzberg::BlockType::CodeBlock => Self::CodeBlock,
            kreuzberg::BlockType::ListItem => Self::ListItem,
            kreuzberg::BlockType::OrderedList => Self::OrderedList,
            kreuzberg::BlockType::BulletList => Self::BulletList,
            kreuzberg::BlockType::TaskList => Self::TaskList,
            kreuzberg::BlockType::DefinitionList => Self::DefinitionList,
            kreuzberg::BlockType::DefinitionTerm => Self::DefinitionTerm,
            kreuzberg::BlockType::DefinitionDescription => Self::DefinitionDescription,
            kreuzberg::BlockType::Div => Self::Div,
            kreuzberg::BlockType::Section => Self::Section,
            kreuzberg::BlockType::ThematicBreak => Self::ThematicBreak,
            kreuzberg::BlockType::RawBlock => Self::RawBlock,
            kreuzberg::BlockType::MathDisplay => Self::MathDisplay,
        }
    }
}

impl From<InlineType> for kreuzberg::InlineType {
    fn from(val: InlineType) -> Self {
        match val {
            InlineType::Text => Self::Text,
            InlineType::Strong => Self::Strong,
            InlineType::Emphasis => Self::Emphasis,
            InlineType::Highlight => Self::Highlight,
            InlineType::Subscript => Self::Subscript,
            InlineType::Superscript => Self::Superscript,
            InlineType::Insert => Self::Insert,
            InlineType::Delete => Self::Delete,
            InlineType::Code => Self::Code,
            InlineType::Link => Self::Link,
            InlineType::Image => Self::Image,
            InlineType::Span => Self::Span,
            InlineType::Math => Self::Math,
            InlineType::RawInline => Self::RawInline,
            InlineType::FootnoteRef => Self::FootnoteRef,
            InlineType::Symbol => Self::Symbol,
        }
    }
}

impl From<kreuzberg::InlineType> for InlineType {
    fn from(val: kreuzberg::InlineType) -> Self {
        match val {
            kreuzberg::InlineType::Text => Self::Text,
            kreuzberg::InlineType::Strong => Self::Strong,
            kreuzberg::InlineType::Emphasis => Self::Emphasis,
            kreuzberg::InlineType::Highlight => Self::Highlight,
            kreuzberg::InlineType::Subscript => Self::Subscript,
            kreuzberg::InlineType::Superscript => Self::Superscript,
            kreuzberg::InlineType::Insert => Self::Insert,
            kreuzberg::InlineType::Delete => Self::Delete,
            kreuzberg::InlineType::Code => Self::Code,
            kreuzberg::InlineType::Link => Self::Link,
            kreuzberg::InlineType::Image => Self::Image,
            kreuzberg::InlineType::Span => Self::Span,
            kreuzberg::InlineType::Math => Self::Math,
            kreuzberg::InlineType::RawInline => Self::RawInline,
            kreuzberg::InlineType::FootnoteRef => Self::FootnoteRef,
            kreuzberg::InlineType::Symbol => Self::Symbol,
        }
    }
}

impl From<RelationshipKind> for kreuzberg::RelationshipKind {
    fn from(val: RelationshipKind) -> Self {
        match val {
            RelationshipKind::FootnoteReference => Self::FootnoteReference,
            RelationshipKind::CitationReference => Self::CitationReference,
            RelationshipKind::InternalLink => Self::InternalLink,
            RelationshipKind::Caption => Self::Caption,
            RelationshipKind::Label => Self::Label,
            RelationshipKind::TocEntry => Self::TocEntry,
            RelationshipKind::CrossReference => Self::CrossReference,
        }
    }
}

impl From<kreuzberg::RelationshipKind> for RelationshipKind {
    fn from(val: kreuzberg::RelationshipKind) -> Self {
        match val {
            kreuzberg::RelationshipKind::FootnoteReference => Self::FootnoteReference,
            kreuzberg::RelationshipKind::CitationReference => Self::CitationReference,
            kreuzberg::RelationshipKind::InternalLink => Self::InternalLink,
            kreuzberg::RelationshipKind::Caption => Self::Caption,
            kreuzberg::RelationshipKind::Label => Self::Label,
            kreuzberg::RelationshipKind::TocEntry => Self::TocEntry,
            kreuzberg::RelationshipKind::CrossReference => Self::CrossReference,
        }
    }
}

impl From<ContentLayer> for kreuzberg::ContentLayer {
    fn from(val: ContentLayer) -> Self {
        match val {
            ContentLayer::Body => Self::Body,
            ContentLayer::Header => Self::Header,
            ContentLayer::Footer => Self::Footer,
            ContentLayer::Footnote => Self::Footnote,
        }
    }
}

impl From<kreuzberg::ContentLayer> for ContentLayer {
    fn from(val: kreuzberg::ContentLayer) -> Self {
        match val {
            kreuzberg::ContentLayer::Body => Self::Body,
            kreuzberg::ContentLayer::Header => Self::Header,
            kreuzberg::ContentLayer::Footer => Self::Footer,
            kreuzberg::ContentLayer::Footnote => Self::Footnote,
        }
    }
}

impl From<ExtractionMethod> for kreuzberg::ExtractionMethod {
    fn from(val: ExtractionMethod) -> Self {
        match val {
            ExtractionMethod::Native => Self::Native,
            ExtractionMethod::Ocr => Self::Ocr,
            ExtractionMethod::Mixed => Self::Mixed,
        }
    }
}

impl From<kreuzberg::ExtractionMethod> for ExtractionMethod {
    fn from(val: kreuzberg::ExtractionMethod) -> Self {
        match val {
            kreuzberg::ExtractionMethod::Native => Self::Native,
            kreuzberg::ExtractionMethod::Ocr => Self::Ocr,
            kreuzberg::ExtractionMethod::Mixed => Self::Mixed,
        }
    }
}

impl From<ChunkType> for kreuzberg::ChunkType {
    fn from(val: ChunkType) -> Self {
        match val {
            ChunkType::Heading => Self::Heading,
            ChunkType::PartyList => Self::PartyList,
            ChunkType::Definitions => Self::Definitions,
            ChunkType::OperativeClause => Self::OperativeClause,
            ChunkType::SignatureBlock => Self::SignatureBlock,
            ChunkType::Schedule => Self::Schedule,
            ChunkType::TableLike => Self::TableLike,
            ChunkType::Formula => Self::Formula,
            ChunkType::CodeBlock => Self::CodeBlock,
            ChunkType::Image => Self::Image,
            ChunkType::OrgChart => Self::OrgChart,
            ChunkType::Diagram => Self::Diagram,
            ChunkType::Unknown => Self::Unknown,
        }
    }
}

impl From<kreuzberg::ChunkType> for ChunkType {
    fn from(val: kreuzberg::ChunkType) -> Self {
        match val {
            kreuzberg::ChunkType::Heading => Self::Heading,
            kreuzberg::ChunkType::PartyList => Self::PartyList,
            kreuzberg::ChunkType::Definitions => Self::Definitions,
            kreuzberg::ChunkType::OperativeClause => Self::OperativeClause,
            kreuzberg::ChunkType::SignatureBlock => Self::SignatureBlock,
            kreuzberg::ChunkType::Schedule => Self::Schedule,
            kreuzberg::ChunkType::TableLike => Self::TableLike,
            kreuzberg::ChunkType::Formula => Self::Formula,
            kreuzberg::ChunkType::CodeBlock => Self::CodeBlock,
            kreuzberg::ChunkType::Image => Self::Image,
            kreuzberg::ChunkType::OrgChart => Self::OrgChart,
            kreuzberg::ChunkType::Diagram => Self::Diagram,
            kreuzberg::ChunkType::Unknown => Self::Unknown,
        }
    }
}

impl From<ImageKind> for kreuzberg::ImageKind {
    fn from(val: ImageKind) -> Self {
        match val {
            ImageKind::Photograph => Self::Photograph,
            ImageKind::Diagram => Self::Diagram,
            ImageKind::Chart => Self::Chart,
            ImageKind::Drawing => Self::Drawing,
            ImageKind::TextBlock => Self::TextBlock,
            ImageKind::Decoration => Self::Decoration,
            ImageKind::Logo => Self::Logo,
            ImageKind::Icon => Self::Icon,
            ImageKind::TileFragment => Self::TileFragment,
            ImageKind::Mask => Self::Mask,
            ImageKind::PageRaster => Self::PageRaster,
            ImageKind::Unknown => Self::Unknown,
        }
    }
}

impl From<kreuzberg::ImageKind> for ImageKind {
    fn from(val: kreuzberg::ImageKind) -> Self {
        match val {
            kreuzberg::ImageKind::Photograph => Self::Photograph,
            kreuzberg::ImageKind::Diagram => Self::Diagram,
            kreuzberg::ImageKind::Chart => Self::Chart,
            kreuzberg::ImageKind::Drawing => Self::Drawing,
            kreuzberg::ImageKind::TextBlock => Self::TextBlock,
            kreuzberg::ImageKind::Decoration => Self::Decoration,
            kreuzberg::ImageKind::Logo => Self::Logo,
            kreuzberg::ImageKind::Icon => Self::Icon,
            kreuzberg::ImageKind::TileFragment => Self::TileFragment,
            kreuzberg::ImageKind::Mask => Self::Mask,
            kreuzberg::ImageKind::PageRaster => Self::PageRaster,
            kreuzberg::ImageKind::Unknown => Self::Unknown,
        }
    }
}

impl From<ResultFormat> for kreuzberg::ResultFormat {
    fn from(val: ResultFormat) -> Self {
        match val {
            ResultFormat::Unified => Self::Unified,
            ResultFormat::ElementBased => Self::ElementBased,
        }
    }
}

impl From<kreuzberg::ResultFormat> for ResultFormat {
    fn from(val: kreuzberg::ResultFormat) -> Self {
        match val {
            kreuzberg::ResultFormat::Unified => Self::Unified,
            kreuzberg::ResultFormat::ElementBased => Self::ElementBased,
        }
    }
}

impl From<ElementType> for kreuzberg::ElementType {
    fn from(val: ElementType) -> Self {
        match val {
            ElementType::Title => Self::Title,
            ElementType::NarrativeText => Self::NarrativeText,
            ElementType::Heading => Self::Heading,
            ElementType::ListItem => Self::ListItem,
            ElementType::Table => Self::Table,
            ElementType::Image => Self::Image,
            ElementType::PageBreak => Self::PageBreak,
            ElementType::CodeBlock => Self::CodeBlock,
            ElementType::BlockQuote => Self::BlockQuote,
            ElementType::Footer => Self::Footer,
            ElementType::Header => Self::Header,
        }
    }
}

impl From<kreuzberg::ElementType> for ElementType {
    fn from(val: kreuzberg::ElementType) -> Self {
        match val {
            kreuzberg::ElementType::Title => Self::Title,
            kreuzberg::ElementType::NarrativeText => Self::NarrativeText,
            kreuzberg::ElementType::Heading => Self::Heading,
            kreuzberg::ElementType::ListItem => Self::ListItem,
            kreuzberg::ElementType::Table => Self::Table,
            kreuzberg::ElementType::Image => Self::Image,
            kreuzberg::ElementType::PageBreak => Self::PageBreak,
            kreuzberg::ElementType::CodeBlock => Self::CodeBlock,
            kreuzberg::ElementType::BlockQuote => Self::BlockQuote,
            kreuzberg::ElementType::Footer => Self::Footer,
            kreuzberg::ElementType::Header => Self::Header,
        }
    }
}

impl From<kreuzberg::FormatMetadata> for FormatMetadata {
    fn from(val: kreuzberg::FormatMetadata) -> Self {
        match val {
            kreuzberg::FormatMetadata::Pdf(_0) => Self {
                format_type: "pdf".to_string(),
                pdf: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Docx(_0) => Self {
                format_type: "docx".to_string(),
                docx: Some((*_0).into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Excel(_0) => Self {
                format_type: "excel".to_string(),
                excel: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Email(_0) => Self {
                format_type: "email".to_string(),
                email: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Pptx(_0) => Self {
                format_type: "pptx".to_string(),
                pptx: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Archive(_0) => Self {
                format_type: "archive".to_string(),
                archive: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Image(_0) => Self {
                format_type: "image".to_string(),
                image: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Xml(_0) => Self {
                format_type: "xml".to_string(),
                xml: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Text(_0) => Self {
                format_type: "text".to_string(),
                text: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Html(_0) => Self {
                format_type: "html".to_string(),
                html: Some((*_0).into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Ocr(_0) => Self {
                format_type: "ocr".to_string(),
                ocr: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Csv(_0) => Self {
                format_type: "csv".to_string(),
                csv: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Bibtex(_0) => Self {
                format_type: "bibtex".to_string(),
                bibtex: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Citation(_0) => Self {
                format_type: "citation".to_string(),
                citation: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::FictionBook(_0) => Self {
                format_type: "fiction_book".to_string(),
                fiction_book: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Dbf(_0) => Self {
                format_type: "dbf".to_string(),
                dbf: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Jats(_0) => Self {
                format_type: "jats".to_string(),
                jats: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Epub(_0) => Self {
                format_type: "epub".to_string(),
                epub: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Pst(_0) => Self {
                format_type: "pst".to_string(),
                pst: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Code(_0) => Self {
                format_type: "code".to_string(),
                code: Some(format!("{:?}", _0)),
                ..Default::default()
            },
        }
    }
}

impl From<TextDirection> for kreuzberg::TextDirection {
    fn from(val: TextDirection) -> Self {
        match val {
            TextDirection::LeftToRight => Self::LeftToRight,
            TextDirection::RightToLeft => Self::RightToLeft,
            TextDirection::Auto => Self::Auto,
        }
    }
}

impl From<kreuzberg::TextDirection> for TextDirection {
    fn from(val: kreuzberg::TextDirection) -> Self {
        match val {
            kreuzberg::TextDirection::LeftToRight => Self::LeftToRight,
            kreuzberg::TextDirection::RightToLeft => Self::RightToLeft,
            kreuzberg::TextDirection::Auto => Self::Auto,
        }
    }
}

impl From<LinkType> for kreuzberg::LinkType {
    fn from(val: LinkType) -> Self {
        match val {
            LinkType::Anchor => Self::Anchor,
            LinkType::Internal => Self::Internal,
            LinkType::External => Self::External,
            LinkType::Email => Self::Email,
            LinkType::Phone => Self::Phone,
            LinkType::Other => Self::Other,
        }
    }
}

impl From<kreuzberg::LinkType> for LinkType {
    fn from(val: kreuzberg::LinkType) -> Self {
        match val {
            kreuzberg::LinkType::Anchor => Self::Anchor,
            kreuzberg::LinkType::Internal => Self::Internal,
            kreuzberg::LinkType::External => Self::External,
            kreuzberg::LinkType::Email => Self::Email,
            kreuzberg::LinkType::Phone => Self::Phone,
            kreuzberg::LinkType::Other => Self::Other,
        }
    }
}

impl From<ImageType> for kreuzberg::ImageType {
    fn from(val: ImageType) -> Self {
        match val {
            ImageType::DataUri => Self::DataUri,
            ImageType::InlineSvg => Self::InlineSvg,
            ImageType::External => Self::External,
            ImageType::Relative => Self::Relative,
        }
    }
}

impl From<kreuzberg::ImageType> for ImageType {
    fn from(val: kreuzberg::ImageType) -> Self {
        match val {
            kreuzberg::ImageType::DataUri => Self::DataUri,
            kreuzberg::ImageType::InlineSvg => Self::InlineSvg,
            kreuzberg::ImageType::External => Self::External,
            kreuzberg::ImageType::Relative => Self::Relative,
        }
    }
}

impl From<StructuredDataType> for kreuzberg::StructuredDataType {
    fn from(val: StructuredDataType) -> Self {
        match val {
            StructuredDataType::JsonLd => Self::JsonLd,
            StructuredDataType::Microdata => Self::Microdata,
            StructuredDataType::RDFa => Self::RDFa,
        }
    }
}

impl From<kreuzberg::StructuredDataType> for StructuredDataType {
    fn from(val: kreuzberg::StructuredDataType) -> Self {
        match val {
            kreuzberg::StructuredDataType::JsonLd => Self::JsonLd,
            kreuzberg::StructuredDataType::Microdata => Self::Microdata,
            kreuzberg::StructuredDataType::RDFa => Self::RDFa,
        }
    }
}

impl From<OcrElementLevel> for kreuzberg::OcrElementLevel {
    fn from(val: OcrElementLevel) -> Self {
        match val {
            OcrElementLevel::Word => Self::Word,
            OcrElementLevel::Line => Self::Line,
            OcrElementLevel::Block => Self::Block,
            OcrElementLevel::Page => Self::Page,
        }
    }
}

impl From<kreuzberg::OcrElementLevel> for OcrElementLevel {
    fn from(val: kreuzberg::OcrElementLevel) -> Self {
        match val {
            kreuzberg::OcrElementLevel::Word => Self::Word,
            kreuzberg::OcrElementLevel::Line => Self::Line,
            kreuzberg::OcrElementLevel::Block => Self::Block,
            kreuzberg::OcrElementLevel::Page => Self::Page,
        }
    }
}

impl From<PageUnitType> for kreuzberg::PageUnitType {
    fn from(val: PageUnitType) -> Self {
        match val {
            PageUnitType::Page => Self::Page,
            PageUnitType::Slide => Self::Slide,
            PageUnitType::Sheet => Self::Sheet,
        }
    }
}

impl From<kreuzberg::PageUnitType> for PageUnitType {
    fn from(val: kreuzberg::PageUnitType) -> Self {
        match val {
            kreuzberg::PageUnitType::Page => Self::Page,
            kreuzberg::PageUnitType::Slide => Self::Slide,
            kreuzberg::PageUnitType::Sheet => Self::Sheet,
        }
    }
}

impl From<kreuzberg::DiffLine> for DiffLine {
    fn from(val: kreuzberg::DiffLine) -> Self {
        match val {
            kreuzberg::DiffLine::Context(_0) => Self {
                kind: "context".to_string(),
                context: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::DiffLine::Added(_0) => Self {
                kind: "added".to_string(),
                added: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::DiffLine::Removed(_0) => Self {
                kind: "removed".to_string(),
                removed: Some(_0.into()),
                ..Default::default()
            },
        }
    }
}

impl From<DiffLine> for kreuzberg::DiffLine {
    fn from(val: DiffLine) -> Self {
        match val.kind.as_str() {
            "context" => Self::Context(val.context.unwrap_or_default().into()),
            "added" => Self::Added(val.added.unwrap_or_default().into()),
            "removed" => Self::Removed(val.removed.unwrap_or_default().into()),
            _ => Self::default(),
        }
    }
}

impl From<RevisionKind> for kreuzberg::RevisionKind {
    fn from(val: RevisionKind) -> Self {
        match val {
            RevisionKind::Insertion => Self::Insertion,
            RevisionKind::Deletion => Self::Deletion,
            RevisionKind::FormatChange => Self::FormatChange,
            RevisionKind::Comment => Self::Comment,
        }
    }
}

impl From<kreuzberg::RevisionKind> for RevisionKind {
    fn from(val: kreuzberg::RevisionKind) -> Self {
        match val {
            kreuzberg::RevisionKind::Insertion => Self::Insertion,
            kreuzberg::RevisionKind::Deletion => Self::Deletion,
            kreuzberg::RevisionKind::FormatChange => Self::FormatChange,
            kreuzberg::RevisionKind::Comment => Self::Comment,
        }
    }
}

impl From<UriKind> for kreuzberg::UriKind {
    fn from(val: UriKind) -> Self {
        match val {
            UriKind::Hyperlink => Self::Hyperlink,
            UriKind::Image => Self::Image,
            UriKind::Anchor => Self::Anchor,
            UriKind::Citation => Self::Citation,
            UriKind::Reference => Self::Reference,
            UriKind::Email => Self::Email,
        }
    }
}

impl From<kreuzberg::UriKind> for UriKind {
    fn from(val: kreuzberg::UriKind) -> Self {
        match val {
            kreuzberg::UriKind::Hyperlink => Self::Hyperlink,
            kreuzberg::UriKind::Image => Self::Image,
            kreuzberg::UriKind::Anchor => Self::Anchor,
            kreuzberg::UriKind::Citation => Self::Citation,
            kreuzberg::UriKind::Reference => Self::Reference,
            kreuzberg::UriKind::Email => Self::Email,
        }
    }
}

impl From<KeywordAlgorithm> for kreuzberg::KeywordAlgorithm {
    fn from(val: KeywordAlgorithm) -> Self {
        match val {
            KeywordAlgorithm::Yake => Self::Yake,
            KeywordAlgorithm::Rake => Self::Rake,
        }
    }
}

impl From<kreuzberg::KeywordAlgorithm> for KeywordAlgorithm {
    fn from(val: kreuzberg::KeywordAlgorithm) -> Self {
        match val {
            kreuzberg::KeywordAlgorithm::Yake => Self::Yake,
            kreuzberg::KeywordAlgorithm::Rake => Self::Rake,
        }
    }
}

impl From<kreuzberg::PSMMode> for PSMMode {
    fn from(val: kreuzberg::PSMMode) -> Self {
        match val {
            kreuzberg::PSMMode::OsdOnly => Self::OsdOnly,
            kreuzberg::PSMMode::AutoOsd => Self::AutoOsd,
            kreuzberg::PSMMode::AutoOnly => Self::AutoOnly,
            kreuzberg::PSMMode::Auto => Self::Auto,
            kreuzberg::PSMMode::SingleColumn => Self::SingleColumn,
            kreuzberg::PSMMode::SingleBlockVertical => Self::SingleBlockVertical,
            kreuzberg::PSMMode::SingleBlock => Self::SingleBlock,
            kreuzberg::PSMMode::SingleLine => Self::SingleLine,
            kreuzberg::PSMMode::SingleWord => Self::SingleWord,
            kreuzberg::PSMMode::CircleWord => Self::CircleWord,
            kreuzberg::PSMMode::SingleChar => Self::SingleChar,
        }
    }
}

impl From<kreuzberg::PaddleLanguage> for PaddleLanguage {
    fn from(val: kreuzberg::PaddleLanguage) -> Self {
        match val {
            kreuzberg::PaddleLanguage::English => Self::English,
            kreuzberg::PaddleLanguage::Chinese => Self::Chinese,
            kreuzberg::PaddleLanguage::Japanese => Self::Japanese,
            kreuzberg::PaddleLanguage::Korean => Self::Korean,
            kreuzberg::PaddleLanguage::German => Self::German,
            kreuzberg::PaddleLanguage::French => Self::French,
            kreuzberg::PaddleLanguage::Latin => Self::Latin,
            kreuzberg::PaddleLanguage::Cyrillic => Self::Cyrillic,
            kreuzberg::PaddleLanguage::TraditionalChinese => Self::TraditionalChinese,
            kreuzberg::PaddleLanguage::Thai => Self::Thai,
            kreuzberg::PaddleLanguage::Greek => Self::Greek,
            kreuzberg::PaddleLanguage::EastSlavic => Self::EastSlavic,
            kreuzberg::PaddleLanguage::Arabic => Self::Arabic,
            kreuzberg::PaddleLanguage::Devanagari => Self::Devanagari,
            kreuzberg::PaddleLanguage::Tamil => Self::Tamil,
            kreuzberg::PaddleLanguage::Telugu => Self::Telugu,
        }
    }
}

impl From<kreuzberg::LayoutClass> for LayoutClass {
    fn from(val: kreuzberg::LayoutClass) -> Self {
        match val {
            kreuzberg::LayoutClass::Caption => Self::Caption,
            kreuzberg::LayoutClass::Footnote => Self::Footnote,
            kreuzberg::LayoutClass::Formula => Self::Formula,
            kreuzberg::LayoutClass::ListItem => Self::ListItem,
            kreuzberg::LayoutClass::PageFooter => Self::PageFooter,
            kreuzberg::LayoutClass::PageHeader => Self::PageHeader,
            kreuzberg::LayoutClass::Picture => Self::Picture,
            kreuzberg::LayoutClass::SectionHeader => Self::SectionHeader,
            kreuzberg::LayoutClass::Table => Self::Table,
            kreuzberg::LayoutClass::Text => Self::Text,
            kreuzberg::LayoutClass::Title => Self::Title,
            kreuzberg::LayoutClass::DocumentIndex => Self::DocumentIndex,
            kreuzberg::LayoutClass::Code => Self::Code,
            kreuzberg::LayoutClass::CheckboxSelected => Self::CheckboxSelected,
            kreuzberg::LayoutClass::CheckboxUnselected => Self::CheckboxUnselected,
            kreuzberg::LayoutClass::Form => Self::Form,
            kreuzberg::LayoutClass::KeyValueRegion => Self::KeyValueRegion,
        }
    }
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn extract_bytes(content: Vec<u8>, mime_type: String, config: &ExtractionConfig) -> Result<String> {
    let config_core: kreuzberg::ExtractionConfig = config.clone().into();
    let rt = tokio::runtime::Runtime::new().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let result = rt.block_on(async {
        kreuzberg::extract_bytes(&content, &mime_type, &config_core)
            .await
            .map_err(|e| {
                extendr_api::Error::Other(
                    e.to_string()
                        .replace(":", "_")
                        .replace("/", "_")
                        .replace("-", "_")
                        .chars()
                        .take(255)
                        .collect::<String>(),
                )
            })
    })?;
    let result: ExtractionResult = result.into();
    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn extract_file(path: String, mime_type: Option<String>, config: &ExtractionConfig) -> Result<String> {
    let config_core: kreuzberg::ExtractionConfig = config.clone().into();
    let rt = tokio::runtime::Runtime::new().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let result = rt.block_on(async {
        kreuzberg::extract_file(std::path::PathBuf::from(path), mime_type.as_deref(), &config_core)
            .await
            .map_err(|e| {
                extendr_api::Error::Other(
                    e.to_string()
                        .replace(":", "_")
                        .replace("/", "_")
                        .replace("-", "_")
                        .chars()
                        .take(255)
                        .collect::<String>(),
                )
            })
    })?;
    let result: ExtractionResult = result.into();
    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn extract_file_sync(path: String, mime_type: Option<String>, config: &ExtractionConfig) -> Result<String> {
    let config_core: kreuzberg::ExtractionConfig = config.clone().into();
    let result = kreuzberg::extract_file_sync(std::path::PathBuf::from(path), mime_type.as_deref(), &config_core)
        .map_err(|e| {
            extendr_api::Error::Other(
                e.to_string()
                    .replace(":", "_")
                    .replace("/", "_")
                    .replace("-", "_")
                    .chars()
                    .take(255)
                    .collect::<String>(),
            )
        })?;
    let result: ExtractionResult = result.into();
    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn extract_bytes_sync(content: Vec<u8>, mime_type: String, config: &ExtractionConfig) -> Result<String> {
    let config_core: kreuzberg::ExtractionConfig = config.clone().into();
    let result = kreuzberg::extract_bytes_sync(&content, &mime_type, &config_core).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let result: ExtractionResult = result.into();
    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn batch_extract_files_sync(items: String, config: &ExtractionConfig) -> Result<String> {
    let items_core: Vec<kreuzberg::BatchFileItem> = serde_json::from_str(&items).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let config_core: kreuzberg::ExtractionConfig = config.clone().into();
    let result = kreuzberg::batch_extract_files_sync(items_core, &config_core).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let result: Vec<ExtractionResult> = result.into_iter().map(Into::into).collect();
    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn batch_extract_bytes_sync(items: String, config: &ExtractionConfig) -> Result<String> {
    let items_core: Vec<kreuzberg::BatchBytesItem> = serde_json::from_str(&items).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let config_core: kreuzberg::ExtractionConfig = config.clone().into();
    let result = kreuzberg::batch_extract_bytes_sync(items_core, &config_core).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let result: Vec<ExtractionResult> = result.into_iter().map(Into::into).collect();
    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn batch_extract_files(items: String, config: &ExtractionConfig) -> Result<String> {
    let items_core: Vec<kreuzberg::BatchFileItem> = serde_json::from_str(&items).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let config_core: kreuzberg::ExtractionConfig = config.clone().into();
    let rt = tokio::runtime::Runtime::new().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let result = rt.block_on(async {
        kreuzberg::batch_extract_files(items_core, &config_core)
            .await
            .map_err(|e| {
                extendr_api::Error::Other(
                    e.to_string()
                        .replace(":", "_")
                        .replace("/", "_")
                        .replace("-", "_")
                        .chars()
                        .take(255)
                        .collect::<String>(),
                )
            })
    })?;
    let result: Vec<ExtractionResult> = result.into_iter().map(Into::into).collect();
    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn batch_extract_bytes(items: String, config: &ExtractionConfig) -> Result<String> {
    let items_core: Vec<kreuzberg::BatchBytesItem> = serde_json::from_str(&items).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let config_core: kreuzberg::ExtractionConfig = config.clone().into();
    let rt = tokio::runtime::Runtime::new().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let result = rt.block_on(async {
        kreuzberg::batch_extract_bytes(items_core, &config_core)
            .await
            .map_err(|e| {
                extendr_api::Error::Other(
                    e.to_string()
                        .replace(":", "_")
                        .replace("/", "_")
                        .replace("-", "_")
                        .chars()
                        .take(255)
                        .collect::<String>(),
                )
            })
    })?;
    let result: Vec<ExtractionResult> = result.into_iter().map(Into::into).collect();
    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn detect_mime_type_from_bytes(content: Vec<u8>) -> Result<String> {
    kreuzberg::detect_mime_type_from_bytes(&content).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn get_extensions_for_mime(mime_type: String) -> Result<Vec<String>> {
    kreuzberg::get_extensions_for_mime(&mime_type).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn list_embedding_backends() -> Result<Vec<String>> {
    kreuzberg::list_embedding_backends().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn list_document_extractors() -> Result<Vec<String>> {
    kreuzberg::list_document_extractors().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn list_ocr_backends() -> Result<Vec<String>> {
    kreuzberg::list_ocr_backends().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn list_post_processors() -> Result<Vec<String>> {
    kreuzberg::list_post_processors().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn list_renderers() -> Result<Vec<String>> {
    kreuzberg::list_renderers().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn list_validators() -> Result<Vec<String>> {
    kreuzberg::list_validators().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[extendr]
pub fn compare(a: &ExtractionResult, b: &ExtractionResult, opts: &DiffOptions) -> String {
    let a_core: kreuzberg::ExtractionResult = a.clone().into();
    let b_core: kreuzberg::ExtractionResult = b.clone().into();
    let opts_core: kreuzberg::DiffOptions = opts.clone().into();
    let result = kreuzberg::compare(&a_core, &b_core, &opts_core);
    let result: ExtractionDiff = result.into();
    serde_json::to_string(&result).expect("serialization failed")
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn embed_texts_async(texts: Vec<String>, config: &EmbeddingConfig) -> Result<String> {
    let config_core: kreuzberg::EmbeddingConfig = config.clone().into();
    let rt = tokio::runtime::Runtime::new().map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;
    let result = rt.block_on(async {
        kreuzberg::embed_texts_async(texts, &config_core).await.map_err(|e| {
            extendr_api::Error::Other(
                e.to_string()
                    .replace(":", "_")
                    .replace("/", "_")
                    .replace("-", "_")
                    .chars()
                    .take(255)
                    .collect::<String>(),
            )
        })
    })?;

    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn render_pdf_page_to_png(
    pdf_bytes: Vec<u8>,
    page_index: f64,
    dpi: Option<i32>,
    password: Option<String>,
) -> Result<Vec<u8>> {
    kreuzberg::render_pdf_page_to_png(&pdf_bytes, page_index as usize, dpi, password.as_deref()).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn detect_mime_type(path: String, check_exists: bool) -> Result<String> {
    kreuzberg::detect_mime_type(path, check_exists).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[allow(clippy::missing_errors_doc)]
#[extendr]
pub fn embed_texts(texts: Vec<String>, config: &EmbeddingConfig) -> Result<String> {
    let config_core: kreuzberg::EmbeddingConfig = config.clone().into();
    let result = kreuzberg::embed_texts(texts, &config_core).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })?;

    serde_json::to_string(&result).map_err(|e| {
        extendr_api::Error::Other(
            e.to_string()
                .replace(":", "_")
                .replace("/", "_")
                .replace("-", "_")
                .chars()
                .take(255)
                .collect::<String>(),
        )
    })
}

#[extendr]
pub fn get_embedding_preset(name: String) -> Option<String> {
    let result = kreuzberg::get_embedding_preset(&name);

    result.map(|v| serde_json::to_string(&v).expect("serialization failed"))
}

#[extendr]
pub fn list_embedding_presets() -> Vec<String> {
    kreuzberg::list_embedding_presets()
}

/// Newtype wrapper around `extendr_api::Robj` that asserts `Send + Sync`.
///
/// # Safety
///
/// R is single-threaded; user-supplied R callbacks must only be invoked from the R main
/// thread. This wrapper exists to satisfy the `Send`/`Sync` bounds required by the Rust
/// plugin trait system and by `tokio::spawn_blocking`. Misuse from a background thread
/// triggers R-runtime undefined behaviour.
#[repr(transparent)]
#[derive(Clone)]
pub(crate) struct SendRobj(pub extendr_api::Robj);
// SAFETY: see SendRobj docs.
unsafe impl Send for SendRobj {}
// SAFETY: see SendRobj docs.
unsafe impl Sync for SendRobj {}
impl SendRobj {
    /// Consume the wrapper and yield the inner `Robj`. Used inside `spawn_blocking`
    /// closures so that the closure captures the whole `SendRobj` (which is `Send`)
    /// rather than the inner `Robj` field (which is `!Send`) under 2021+ disjoint
    /// capture rules.
    #[inline]
    pub(crate) fn into_inner(self) -> extendr_api::Robj {
        self.0
    }
}

/// Wrapper that bridges a foreign R object to the `OcrBackend` trait.
pub struct ROcrBackendBridge {
    inner: extendr_api::Robj,
    cached_name: String,
}

impl std::fmt::Debug for ROcrBackendBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ROcrBackendBridge")
    }
}

impl ROcrBackendBridge {
    /// Create a new bridge wrapping an R object (named list of functions).
    ///
    /// Validates that the R object provides all required methods.
    pub fn new(r_obj: extendr_api::Robj) -> std::result::Result<Self, String> {
        match r_obj.dollar("process_image") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: process_image".to_string()),
        }
        match r_obj.dollar("supports_language") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: supports_language".to_string()),
        }
        match r_obj.dollar("backend_type") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: backend_type".to_string()),
        }

        let cached_name: String = match r_obj.dollar("name") {
            Ok(v) if !v.is_null() && !v.is_na() => {
                if let Some(s) = v.as_str() {
                    s.to_string()
                } else {
                    "unknown".to_string()
                }
            }
            _ => "unknown".to_string(),
        };

        Ok(Self {
            inner: r_obj,
            cached_name,
        })
    }
}

impl kreuzberg::plugins::Plugin for ROcrBackendBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let maybe_fn = self.inner.dollar("version");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                if let Some(s) = val.as_str() {
                    s.to_string()
                } else {
                    Default::default()
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("initialize");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'initialize'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("shutdown");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'shutdown'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::OcrBackend for ROcrBackendBridge {
    async fn process_image(
        &self,
        image_bytes: &[u8],
        config: &kreuzberg::OcrConfig,
    ) -> std::result::Result<kreuzberg::ExtractionResult, kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();
        let cached_name_inner = cached_name.clone();
        let send_r_obj = SendRobj(self.inner.clone());

        let image_bytes = image_bytes.to_vec();

        let config_json = serde_json::to_string(config).unwrap_or_default();

        tokio::task::spawn_blocking(move || {
            // Call `into_inner()` on the whole `SendRobj` value so the closure captures the whole
            // `SendRobj` (which is `Send`) rather than just the inner `Robj` field (which is `!Send`)
            // under 2021+ disjoint-capture rules. The method takes `self` by value, forcing the
            // closure to move the whole wrapper.
            let r_obj = send_r_obj.into_inner();
            let maybe_fn = r_obj.dollar("process_image");
            let fn_robj = match maybe_fn {
                Ok(v) if !v.is_null() && !v.is_na() => v,
                _ => {
                    return Err(kreuzberg::KreuzbergError::Other(format!(
                        "Plugin '{}' missing method 'process_image'",
                        cached_name_inner
                    )));
                }
            };

            let args = extendr_api::Pairlist::from_pairs(&[
                ("image_bytes", extendr_api::Robj::from(&image_bytes[..])),
                ("config", extendr_api::Robj::from(config_json.as_str())),
            ]);
            let result = fn_robj.call(args);

            // R callbacks return JSON-encoded strings (e.g. via `jsonlite::toJSON`). Parse the string
            // payload back into the trait return type. Note: `extendr_api::Robj` does not implement
            // `serde::Serialize`, so we cannot serialize it directly — the caller must serialize on
            // the R side.
            match result {
                Err(_) => Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' method 'process_image' failed",
                    cached_name_inner
                ))),
                Ok(val) => {
                    let payload = val.as_str().unwrap_or("");
                    serde_json::from_str::<kreuzberg::ExtractionResult>(payload).map_err(|_| {
                        kreuzberg::KreuzbergError::Other(format!(
                            "Plugin '{}' method 'process_image' deserialization failed",
                            cached_name_inner
                        ))
                    })
                }
            }
        })
        .await
        .map_err(|e| kreuzberg::KreuzbergError::Other(format!("spawn_blocking failed: {}", e)))?
    }

    fn supports_language(&self, lang: &str) -> bool {
        let maybe_fn = self.inner.dollar("supports_language");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let args = extendr_api::Pairlist::from_pairs(&[("lang", extendr_api::Robj::from(lang))]);
        let result = fn_robj.call(args);

        // Primitive return: extract directly from Robj without JSON deserialization
        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                let int_val = val
                    .as_integer()
                    .map(|i| i != 0)
                    .or_else(|| val.as_real().map(|r| r != 0.0))
                    .unwrap_or(false);
                int_val
            }
        }
    }

    fn backend_type(&self) -> kreuzberg::OcrBackendType {
        let maybe_fn = self.inner.dollar("backend_type");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        // R callbacks return JSON-encoded strings (e.g. via `jsonlite::toJSON`). Parse the string
        // payload back into the trait return type. Note: `extendr_api::Robj` does not implement
        // `serde::Serialize`, so we cannot serialize it directly — the caller must serialize on
        // the R side.
        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                let payload = val.as_str().unwrap_or("");
                match serde_json::from_str::<kreuzberg::OcrBackendType>(payload) {
                    Ok(parsed) => parsed,
                    Err(_) => Default::default(),
                }
            }
        }
    }
}

#[extendr]
pub fn register_ocr_backend(r_backend: extendr_api::Robj) -> std::result::Result<(), String> {
    let required_methods = ["process_image", "supports_language", "backend_type"];
    for method in &required_methods {
        match r_backend.dollar(method) {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err(format!("Backend missing required method: {}", method)),
        }
    }

    let wrapper = ROcrBackendBridge::new(r_backend)?;
    let arc: Arc<dyn kreuzberg::OcrBackend> = Arc::new(wrapper);

    let registry = kreuzberg::plugins::registry::get_ocr_backend_registry();
    let mut registry = registry.write();
    registry
        .register(arc)
        .map_err(|e| format!("Failed to register backend: {}", e))
}

#[extendr]
pub fn unregister_ocr_backend(name: String) -> std::result::Result<(), String> {
    kreuzberg::plugins::ocr_backend::unregister_ocr_backend(&name).map_err(|e| format!("{}", e))
}

#[extendr]
pub fn clear_ocr_backends() -> std::result::Result<(), String> {
    kreuzberg::plugins::ocr_backend::clear_ocr_backends().map_err(|e| format!("{}", e))
}

#[allow(clippy::non_send_fields_in_send_ty)]
// SAFETY: R is single-threaded; the user must invoke plugins from the R main thread.
unsafe impl Send for ROcrBackendBridge {}
// SAFETY: see Send impl.
unsafe impl Sync for ROcrBackendBridge {}

/// Wrapper that bridges a foreign R object to the `PostProcessor` trait.
pub struct RPostProcessorBridge {
    inner: extendr_api::Robj,
    cached_name: String,
}

impl std::fmt::Debug for RPostProcessorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RPostProcessorBridge")
    }
}

impl RPostProcessorBridge {
    /// Create a new bridge wrapping an R object (named list of functions).
    ///
    /// Validates that the R object provides all required methods.
    pub fn new(r_obj: extendr_api::Robj) -> std::result::Result<Self, String> {
        match r_obj.dollar("process") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: process".to_string()),
        }
        match r_obj.dollar("processing_stage") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: processing_stage".to_string()),
        }

        let cached_name: String = match r_obj.dollar("name") {
            Ok(v) if !v.is_null() && !v.is_na() => {
                if let Some(s) = v.as_str() {
                    s.to_string()
                } else {
                    "unknown".to_string()
                }
            }
            _ => "unknown".to_string(),
        };

        Ok(Self {
            inner: r_obj,
            cached_name,
        })
    }
}

impl kreuzberg::plugins::Plugin for RPostProcessorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let maybe_fn = self.inner.dollar("version");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                if let Some(s) = val.as_str() {
                    s.to_string()
                } else {
                    Default::default()
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("initialize");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'initialize'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("shutdown");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'shutdown'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::PostProcessor for RPostProcessorBridge {
    async fn process(
        &self,
        result: &mut kreuzberg::ExtractionResult,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();
        let cached_name_inner = cached_name.clone();
        let send_r_obj = SendRobj(self.inner.clone());

        let result_json = serde_json::to_string(result).unwrap_or_default();

        let config_json = serde_json::to_string(config).unwrap_or_default();

        tokio::task::spawn_blocking(move || {
            // Call `into_inner()` on the whole `SendRobj` value so the closure captures the whole
            // `SendRobj` (which is `Send`) rather than just the inner `Robj` field (which is `!Send`)
            // under 2021+ disjoint-capture rules. The method takes `self` by value, forcing the
            // closure to move the whole wrapper.
            let r_obj = send_r_obj.into_inner();
            let maybe_fn = r_obj.dollar("process");
            let fn_robj = match maybe_fn {
                Ok(v) if !v.is_null() && !v.is_na() => v,
                _ => {
                    return Err(kreuzberg::KreuzbergError::Other(format!(
                        "Plugin '{}' missing method 'process'",
                        cached_name_inner
                    )));
                }
            };

            let args = extendr_api::Pairlist::from_pairs(&[
                ("result", extendr_api::Robj::from(result_json.as_str())),
                ("config", extendr_api::Robj::from(config_json.as_str())),
            ]);
            let result = fn_robj.call(args);

            match result {
                Err(_) => Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' method 'process' failed",
                    cached_name_inner
                ))),
                Ok(_) => Ok(()),
            }
        })
        .await
        .map_err(|e| kreuzberg::KreuzbergError::Other(format!("spawn_blocking failed: {}", e)))?
    }

    fn processing_stage(&self) -> kreuzberg::ProcessingStage {
        let maybe_fn = self.inner.dollar("processing_stage");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        // R callbacks return JSON-encoded strings (e.g. via `jsonlite::toJSON`). Parse the string
        // payload back into the trait return type. Note: `extendr_api::Robj` does not implement
        // `serde::Serialize`, so we cannot serialize it directly — the caller must serialize on
        // the R side.
        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                let payload = val.as_str().unwrap_or("");
                match serde_json::from_str::<kreuzberg::ProcessingStage>(payload) {
                    Ok(parsed) => parsed,
                    Err(_) => Default::default(),
                }
            }
        }
    }
}

#[extendr]
pub fn register_post_processor(r_backend: extendr_api::Robj) -> std::result::Result<(), String> {
    let required_methods = ["process", "processing_stage"];
    for method in &required_methods {
        match r_backend.dollar(method) {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err(format!("Backend missing required method: {}", method)),
        }
    }

    let wrapper = RPostProcessorBridge::new(r_backend)?;
    let arc: Arc<dyn kreuzberg::PostProcessor> = Arc::new(wrapper);

    let registry = kreuzberg::plugins::registry::get_post_processor_registry();
    let mut registry = registry.write();
    registry
        .register(arc)
        .map_err(|e| format!("Failed to register backend: {}", e))
}

#[extendr]
pub fn unregister_post_processor(name: String) -> std::result::Result<(), String> {
    kreuzberg::plugins::post_processor::unregister_post_processor(&name).map_err(|e| format!("{}", e))
}

#[extendr]
pub fn clear_post_processors() -> std::result::Result<(), String> {
    kreuzberg::plugins::post_processor::clear_post_processors().map_err(|e| format!("{}", e))
}

#[allow(clippy::non_send_fields_in_send_ty)]
// SAFETY: R is single-threaded; the user must invoke plugins from the R main thread.
unsafe impl Send for RPostProcessorBridge {}
// SAFETY: see Send impl.
unsafe impl Sync for RPostProcessorBridge {}

/// Wrapper that bridges a foreign R object to the `Validator` trait.
pub struct RValidatorBridge {
    inner: extendr_api::Robj,
    cached_name: String,
}

impl std::fmt::Debug for RValidatorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RValidatorBridge")
    }
}

impl RValidatorBridge {
    /// Create a new bridge wrapping an R object (named list of functions).
    ///
    /// Validates that the R object provides all required methods.
    pub fn new(r_obj: extendr_api::Robj) -> std::result::Result<Self, String> {
        match r_obj.dollar("validate") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: validate".to_string()),
        }

        let cached_name: String = match r_obj.dollar("name") {
            Ok(v) if !v.is_null() && !v.is_na() => {
                if let Some(s) = v.as_str() {
                    s.to_string()
                } else {
                    "unknown".to_string()
                }
            }
            _ => "unknown".to_string(),
        };

        Ok(Self {
            inner: r_obj,
            cached_name,
        })
    }
}

impl kreuzberg::plugins::Plugin for RValidatorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let maybe_fn = self.inner.dollar("version");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                if let Some(s) = val.as_str() {
                    s.to_string()
                } else {
                    Default::default()
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("initialize");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'initialize'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("shutdown");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'shutdown'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::Validator for RValidatorBridge {
    async fn validate(
        &self,
        result: &kreuzberg::ExtractionResult,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();
        let cached_name_inner = cached_name.clone();
        let send_r_obj = SendRobj(self.inner.clone());

        let result_json = serde_json::to_string(result).unwrap_or_default();

        let config_json = serde_json::to_string(config).unwrap_or_default();

        tokio::task::spawn_blocking(move || {
            // Call `into_inner()` on the whole `SendRobj` value so the closure captures the whole
            // `SendRobj` (which is `Send`) rather than just the inner `Robj` field (which is `!Send`)
            // under 2021+ disjoint-capture rules. The method takes `self` by value, forcing the
            // closure to move the whole wrapper.
            let r_obj = send_r_obj.into_inner();
            let maybe_fn = r_obj.dollar("validate");
            let fn_robj = match maybe_fn {
                Ok(v) if !v.is_null() && !v.is_na() => v,
                _ => {
                    return Err(kreuzberg::KreuzbergError::Other(format!(
                        "Plugin '{}' missing method 'validate'",
                        cached_name_inner
                    )));
                }
            };

            let args = extendr_api::Pairlist::from_pairs(&[
                ("result", extendr_api::Robj::from(result_json.as_str())),
                ("config", extendr_api::Robj::from(config_json.as_str())),
            ]);
            let result = fn_robj.call(args);

            match result {
                Err(_) => Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' method 'validate' failed",
                    cached_name_inner
                ))),
                Ok(_) => Ok(()),
            }
        })
        .await
        .map_err(|e| kreuzberg::KreuzbergError::Other(format!("spawn_blocking failed: {}", e)))?
    }
}

#[extendr]
pub fn register_validator(r_backend: extendr_api::Robj) -> std::result::Result<(), String> {
    let required_methods = ["validate"];
    for method in &required_methods {
        match r_backend.dollar(method) {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err(format!("Backend missing required method: {}", method)),
        }
    }

    let wrapper = RValidatorBridge::new(r_backend)?;
    let arc: Arc<dyn kreuzberg::Validator> = Arc::new(wrapper);

    let registry = kreuzberg::plugins::registry::get_validator_registry();
    let mut registry = registry.write();
    registry
        .register(arc)
        .map_err(|e| format!("Failed to register backend: {}", e))
}

#[extendr]
pub fn unregister_validator(name: String) -> std::result::Result<(), String> {
    kreuzberg::plugins::validator::unregister_validator(&name).map_err(|e| format!("{}", e))
}

#[extendr]
pub fn clear_validators() -> std::result::Result<(), String> {
    kreuzberg::plugins::validator::clear_validators().map_err(|e| format!("{}", e))
}

#[allow(clippy::non_send_fields_in_send_ty)]
// SAFETY: R is single-threaded; the user must invoke plugins from the R main thread.
unsafe impl Send for RValidatorBridge {}
// SAFETY: see Send impl.
unsafe impl Sync for RValidatorBridge {}

/// Wrapper that bridges a foreign R object to the `EmbeddingBackend` trait.
pub struct REmbeddingBackendBridge {
    inner: extendr_api::Robj,
    cached_name: String,
}

impl std::fmt::Debug for REmbeddingBackendBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "REmbeddingBackendBridge")
    }
}

impl REmbeddingBackendBridge {
    /// Create a new bridge wrapping an R object (named list of functions).
    ///
    /// Validates that the R object provides all required methods.
    pub fn new(r_obj: extendr_api::Robj) -> std::result::Result<Self, String> {
        match r_obj.dollar("dimensions") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: dimensions".to_string()),
        }
        match r_obj.dollar("embed") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: embed".to_string()),
        }

        let cached_name: String = match r_obj.dollar("name") {
            Ok(v) if !v.is_null() && !v.is_na() => {
                if let Some(s) = v.as_str() {
                    s.to_string()
                } else {
                    "unknown".to_string()
                }
            }
            _ => "unknown".to_string(),
        };

        Ok(Self {
            inner: r_obj,
            cached_name,
        })
    }
}

impl kreuzberg::plugins::Plugin for REmbeddingBackendBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let maybe_fn = self.inner.dollar("version");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                if let Some(s) = val.as_str() {
                    s.to_string()
                } else {
                    Default::default()
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("initialize");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'initialize'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("shutdown");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'shutdown'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::EmbeddingBackend for REmbeddingBackendBridge {
    fn dimensions(&self) -> usize {
        let maybe_fn = self.inner.dollar("dimensions");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        // Primitive return: extract directly from Robj without JSON deserialization
        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                let int_val = val
                    .as_integer()
                    .map(|i| i as usize)
                    .or_else(|| val.as_real().map(|r| r as usize))
                    .unwrap_or_default();
                int_val
            }
        }
    }

    async fn embed(&self, texts: Vec<String>) -> std::result::Result<Vec<Vec<f32>>, kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();
        let cached_name_inner = cached_name.clone();
        let send_r_obj = SendRobj(self.inner.clone());

        let texts = texts.clone();

        tokio::task::spawn_blocking(move || {
            // Call `into_inner()` on the whole `SendRobj` value so the closure captures the whole
            // `SendRobj` (which is `Send`) rather than just the inner `Robj` field (which is `!Send`)
            // under 2021+ disjoint-capture rules. The method takes `self` by value, forcing the
            // closure to move the whole wrapper.
            let r_obj = send_r_obj.into_inner();
            let maybe_fn = r_obj.dollar("embed");
            let fn_robj = match maybe_fn {
                Ok(v) if !v.is_null() && !v.is_na() => v,
                _ => {
                    return Err(kreuzberg::KreuzbergError::Other(format!(
                        "Plugin '{}' missing method 'embed'",
                        cached_name_inner
                    )));
                }
            };

            let args = extendr_api::Pairlist::from_pairs(&[("texts", extendr_api::Robj::from(texts))]);
            let result = fn_robj.call(args);

            // R callbacks return JSON-encoded strings (e.g. via `jsonlite::toJSON`). Parse the string
            // payload back into the trait return type. Note: `extendr_api::Robj` does not implement
            // `serde::Serialize`, so we cannot serialize it directly — the caller must serialize on
            // the R side.
            match result {
                Err(_) => Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' method 'embed' failed",
                    cached_name_inner
                ))),
                Ok(val) => {
                    let payload = val.as_str().unwrap_or("");
                    serde_json::from_str::<Vec<Vec<f32>>>(payload).map_err(|_| {
                        kreuzberg::KreuzbergError::Other(format!(
                            "Plugin '{}' method 'embed' deserialization failed",
                            cached_name_inner
                        ))
                    })
                }
            }
        })
        .await
        .map_err(|e| kreuzberg::KreuzbergError::Other(format!("spawn_blocking failed: {}", e)))?
    }
}

#[extendr]
pub fn register_embedding_backend(r_backend: extendr_api::Robj) -> std::result::Result<(), String> {
    let required_methods = ["dimensions", "embed"];
    for method in &required_methods {
        match r_backend.dollar(method) {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err(format!("Backend missing required method: {}", method)),
        }
    }

    let wrapper = REmbeddingBackendBridge::new(r_backend)?;
    let arc: Arc<dyn kreuzberg::EmbeddingBackend> = Arc::new(wrapper);

    let registry = kreuzberg::plugins::registry::get_embedding_backend_registry();
    let mut registry = registry.write();
    registry
        .register(arc)
        .map_err(|e| format!("Failed to register backend: {}", e))
}

#[extendr]
pub fn unregister_embedding_backend(name: String) -> std::result::Result<(), String> {
    kreuzberg::plugins::embedding_backend::unregister_embedding_backend(&name).map_err(|e| format!("{}", e))
}

#[extendr]
pub fn clear_embedding_backends() -> std::result::Result<(), String> {
    kreuzberg::plugins::embedding_backend::clear_embedding_backends().map_err(|e| format!("{}", e))
}

#[allow(clippy::non_send_fields_in_send_ty)]
// SAFETY: R is single-threaded; the user must invoke plugins from the R main thread.
unsafe impl Send for REmbeddingBackendBridge {}
// SAFETY: see Send impl.
unsafe impl Sync for REmbeddingBackendBridge {}

/// Wrapper that bridges a foreign R object to the `DocumentExtractor` trait.
pub struct RDocumentExtractorBridge {
    inner: extendr_api::Robj,
    cached_name: String,
}

impl std::fmt::Debug for RDocumentExtractorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RDocumentExtractorBridge")
    }
}

impl RDocumentExtractorBridge {
    /// Create a new bridge wrapping an R object (named list of functions).
    ///
    /// Validates that the R object provides all required methods.
    pub fn new(r_obj: extendr_api::Robj) -> std::result::Result<Self, String> {
        match r_obj.dollar("extract_bytes") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: extract_bytes".to_string()),
        }
        match r_obj.dollar("supported_mime_types") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: supported_mime_types".to_string()),
        }

        let cached_name: String = match r_obj.dollar("name") {
            Ok(v) if !v.is_null() && !v.is_na() => {
                if let Some(s) = v.as_str() {
                    s.to_string()
                } else {
                    "unknown".to_string()
                }
            }
            _ => "unknown".to_string(),
        };

        Ok(Self {
            inner: r_obj,
            cached_name,
        })
    }
}

impl kreuzberg::plugins::Plugin for RDocumentExtractorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let maybe_fn = self.inner.dollar("version");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                if let Some(s) = val.as_str() {
                    s.to_string()
                } else {
                    Default::default()
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("initialize");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'initialize'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("shutdown");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'shutdown'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::DocumentExtractor for RDocumentExtractorBridge {
    async fn extract_bytes(
        &self,
        content: &[u8],
        mime_type: &str,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<kreuzberg::InternalDocument, kreuzberg::KreuzbergError> {
        let cached_name = self.cached_name.clone();
        let cached_name_inner = cached_name.clone();
        let send_r_obj = SendRobj(self.inner.clone());

        let content = content.to_vec();

        let mime_type = mime_type.to_owned();

        let config_json = serde_json::to_string(config).unwrap_or_default();

        tokio::task::spawn_blocking(move || {
            // Call `into_inner()` on the whole `SendRobj` value so the closure captures the whole
            // `SendRobj` (which is `Send`) rather than just the inner `Robj` field (which is `!Send`)
            // under 2021+ disjoint-capture rules. The method takes `self` by value, forcing the
            // closure to move the whole wrapper.
            let r_obj = send_r_obj.into_inner();
            let maybe_fn = r_obj.dollar("extract_bytes");
            let fn_robj = match maybe_fn {
                Ok(v) if !v.is_null() && !v.is_na() => v,
                _ => {
                    return Err(kreuzberg::KreuzbergError::Other(format!(
                        "Plugin '{}' missing method 'extract_bytes'",
                        cached_name_inner
                    )));
                }
            };

            let args = extendr_api::Pairlist::from_pairs(&[
                ("content", extendr_api::Robj::from(&content[..])),
                ("mime_type", extendr_api::Robj::from(mime_type)),
                ("config", extendr_api::Robj::from(config_json.as_str())),
            ]);
            let result = fn_robj.call(args);

            // R callbacks return JSON-encoded strings (e.g. via `jsonlite::toJSON`). Parse the string
            // payload back into the trait return type. Note: `extendr_api::Robj` does not implement
            // `serde::Serialize`, so we cannot serialize it directly — the caller must serialize on
            // the R side.
            match result {
                Err(_) => Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' method 'extract_bytes' failed",
                    cached_name_inner
                ))),
                Ok(val) => {
                    let payload = val.as_str().unwrap_or("");
                    serde_json::from_str::<kreuzberg::InternalDocument>(payload).map_err(|_| {
                        kreuzberg::KreuzbergError::Other(format!(
                            "Plugin '{}' method 'extract_bytes' deserialization failed",
                            cached_name_inner
                        ))
                    })
                }
            }
        })
        .await
        .map_err(|e| kreuzberg::KreuzbergError::Other(format!("spawn_blocking failed: {}", e)))?
    }

    fn supported_mime_types(&self) -> &[&str] {
        let __types: Vec<String> = {
            let maybe_fn = self.inner.dollar("supported_mime_types");
            let fn_robj = match maybe_fn {
                Ok(v) if !v.is_null() && !v.is_na() => v,
                _ => return Default::default(),
            };

            let result = fn_robj.call(extendr_api::Pairlist::new());

            // R callbacks return JSON-encoded strings (e.g. via `jsonlite::toJSON`). Parse the string
            // payload back into the trait return type. Note: `extendr_api::Robj` does not implement
            // `serde::Serialize`, so we cannot serialize it directly — the caller must serialize on
            // the R side.
            match result {
                Err(_) => Default::default(),
                Ok(val) => {
                    let payload = val.as_str().unwrap_or("");
                    match serde_json::from_str::<Vec<String>>(payload) {
                        Ok(parsed) => parsed,
                        Err(_) => Default::default(),
                    }
                }
            }
        };
        let __strs: Vec<&'static str> = __types
            .into_iter()
            .map(|s| -> &'static str { Box::leak(s.into_boxed_str()) })
            .collect();
        Box::leak(__strs.into_boxed_slice())
    }
}

#[extendr]
pub fn register_document_extractor(r_backend: extendr_api::Robj) -> std::result::Result<(), String> {
    let required_methods = ["extract_bytes", "supported_mime_types"];
    for method in &required_methods {
        match r_backend.dollar(method) {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err(format!("Backend missing required method: {}", method)),
        }
    }

    let wrapper = RDocumentExtractorBridge::new(r_backend)?;
    let arc: Arc<dyn kreuzberg::DocumentExtractor> = Arc::new(wrapper);

    let registry = kreuzberg::plugins::registry::get_document_extractor_registry();
    let mut registry = registry.write();
    registry
        .register(arc)
        .map_err(|e| format!("Failed to register backend: {}", e))
}

#[extendr]
pub fn unregister_document_extractor(name: String) -> std::result::Result<(), String> {
    kreuzberg::plugins::document_extractor::unregister_document_extractor(&name).map_err(|e| format!("{}", e))
}

#[extendr]
pub fn clear_document_extractors() -> std::result::Result<(), String> {
    kreuzberg::plugins::document_extractor::clear_document_extractors().map_err(|e| format!("{}", e))
}

#[allow(clippy::non_send_fields_in_send_ty)]
// SAFETY: R is single-threaded; the user must invoke plugins from the R main thread.
unsafe impl Send for RDocumentExtractorBridge {}
// SAFETY: see Send impl.
unsafe impl Sync for RDocumentExtractorBridge {}

/// Wrapper that bridges a foreign R object to the `Renderer` trait.
pub struct RRendererBridge {
    inner: extendr_api::Robj,
    cached_name: String,
}

impl std::fmt::Debug for RRendererBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RRendererBridge")
    }
}

impl RRendererBridge {
    /// Create a new bridge wrapping an R object (named list of functions).
    ///
    /// Validates that the R object provides all required methods.
    pub fn new(r_obj: extendr_api::Robj) -> std::result::Result<Self, String> {
        match r_obj.dollar("render") {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err("R object missing required method: render".to_string()),
        }

        let cached_name: String = match r_obj.dollar("name") {
            Ok(v) if !v.is_null() && !v.is_na() => {
                if let Some(s) = v.as_str() {
                    s.to_string()
                } else {
                    "unknown".to_string()
                }
            }
            _ => "unknown".to_string(),
        };

        Ok(Self {
            inner: r_obj,
            cached_name,
        })
    }
}

impl kreuzberg::plugins::Plugin for RRendererBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let maybe_fn = self.inner.dollar("version");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => return Default::default(),
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) => Default::default(),
            Ok(val) => {
                if let Some(s) = val.as_str() {
                    s.to_string()
                } else {
                    Default::default()
                }
            }
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("initialize");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'initialize'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'initialize' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("shutdown");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'shutdown'",
                    self.cached_name
                )));
            }
        };

        let result = fn_robj.call(extendr_api::Pairlist::new());

        match result {
            Err(_) if true => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'shutdown' failed",
                self.cached_name
            ))),
            _ => Ok(()),
        }
    }
}

impl kreuzberg::Renderer for RRendererBridge {
    fn render(&self, doc: &kreuzberg::InternalDocument) -> std::result::Result<String, kreuzberg::KreuzbergError> {
        let maybe_fn = self.inner.dollar("render");
        let fn_robj = match maybe_fn {
            Ok(v) if !v.is_null() && !v.is_na() => v,
            _ => {
                return Err(kreuzberg::KreuzbergError::Other(format!(
                    "Plugin '{}' missing method 'render'",
                    self.cached_name
                )));
            }
        };

        let args = extendr_api::Pairlist::from_pairs(&[(
            "doc",
            extendr_api::Robj::from(serde_json::to_string(doc).unwrap_or_default().as_str()),
        )]);
        let result = fn_robj.call(args);

        match result {
            Err(_) => Err(kreuzberg::KreuzbergError::Other(format!(
                "Plugin '{}' method 'render' failed",
                self.cached_name
            ))),
            Ok(val) => {
                if let Some(s) = val.as_str() {
                    Ok(s.to_string())
                } else {
                    Err(kreuzberg::KreuzbergError::Other(format!(
                        "Plugin '{}' method 'render' returned invalid type",
                        self.cached_name
                    )))
                }
            }
        }
    }
}

#[extendr]
pub fn register_renderer(r_backend: extendr_api::Robj) -> std::result::Result<(), String> {
    let required_methods = ["render"];
    for method in &required_methods {
        match r_backend.dollar(method) {
            Ok(v) if !v.is_null() && !v.is_na() => {}
            _ => return Err(format!("Backend missing required method: {}", method)),
        }
    }

    let wrapper = RRendererBridge::new(r_backend)?;
    let arc: Arc<dyn kreuzberg::Renderer> = Arc::new(wrapper);

    let registry = kreuzberg::plugins::registry::get_renderer_registry();
    let mut registry = registry.write();
    registry
        .register(arc)
        .map_err(|e| format!("Failed to register backend: {}", e))
}

#[extendr]
pub fn unregister_renderer(name: String) -> std::result::Result<(), String> {
    kreuzberg::plugins::renderer::unregister_renderer(&name).map_err(|e| format!("{}", e))
}

#[extendr]
pub fn clear_renderers() -> std::result::Result<(), String> {
    kreuzberg::plugins::renderer::clear_renderers().map_err(|e| format!("{}", e))
}

#[allow(clippy::non_send_fields_in_send_ty)]
// SAFETY: R is single-threaded; the user must invoke plugins from the R main thread.
unsafe impl Send for RRendererBridge {}
// SAFETY: see Send impl.
unsafe impl Sync for RRendererBridge {}

extendr_module! {
    mod kreuzberg;
    impl CacheStats;
    impl AccelerationConfig;
    impl ContentFilterConfig;
    impl EmailConfig;
    impl ExtractionConfig;
    impl FileExtractionConfig;
    impl BatchBytesItem;
    impl BatchFileItem;
    impl ImageExtractionConfig;
    impl TokenReductionOptions;
    impl LanguageDetectionConfig;
    impl HtmlOutputConfig;
    impl LayoutDetectionConfig;
    impl LlmConfig;
    impl StructuredExtractionConfig;
    impl OcrQualityThresholds;
    impl OcrPipelineStage;
    impl OcrConfig;
    impl PageConfig;
    impl PdfConfig;
    impl HierarchyConfig;
    impl PostProcessorConfig;
    impl ChunkingConfig;
    impl EmbeddingConfig;
    impl TreeSitterConfig;
    impl TreeSitterProcessConfig;
    impl SupportedFormat;
    impl ServerConfig;
    impl StructuredDataResult;
    impl DocxAppProperties;
    impl XlsxAppProperties;
    impl PptxAppProperties;
    impl CoreProperties;
    impl SecurityLimits;
    impl TokenReductionConfig;
    impl PdfAnnotation;
    impl InlineElement;
    impl DjotImage;
    impl DjotLink;
    impl DocumentRelationship;
    impl GridCell;
    impl TextAnnotation;
    impl ArchiveEntry;
    impl ProcessingWarning;
    impl LlmUsage;
    impl Chunk;
    impl HeadingLevel;
    impl ChunkMetadata;
    impl ExtractedImage;
    impl BoundingBox;
    impl ElementMetadata;
    impl Element;
    impl XmlExtractionResult;
    impl EmailAttachment;
    impl OcrTableBoundingBox;
    impl ImagePreprocessingConfig;
    impl TesseractConfig;
    impl ImagePreprocessingMetadata;
    impl Metadata;
    impl ExcelMetadata;
    impl EmailMetadata;
    impl ArchiveMetadata;
    impl ImageMetadata;
    impl XmlMetadata;
    impl HeaderMetadata;
    impl StructuredData;
    impl OcrMetadata;
    impl ErrorMetadata;
    impl PptxMetadata;
    impl DocxMetadata;
    impl CsvMetadata;
    impl BibtexMetadata;
    impl CitationMetadata;
    impl YearRange;
    impl FictionBookMetadata;
    impl DbfFieldInfo;
    impl ContributorRole;
    impl EpubMetadata;
    impl PstMetadata;
    impl OcrConfidence;
    impl OcrRotation;
    impl OcrElement;
    impl OcrElementConfig;
    impl PageBoundary;
    impl PageInfo;
    impl LayoutRegion;
    impl HierarchicalBlock;
    impl CellChange;
    impl DocumentRevision;
    impl TableCell;
    impl ExtractedUri;
    impl DetectResponse;
    impl DiffOptions;
    impl DiffHunk;
    impl EmbeddedDiff;
    impl EmbeddingPreset;
    impl YakeParams;
    impl RakeParams;
    impl KeywordConfig;
    impl Keyword;
    impl PaddleOcrConfig;
    impl ModelPaths;
    impl OrientationResult;
    impl BBox;
    impl LayoutDetection;
    impl EmbeddedFile;
    impl PdfMetadata;
    impl OutputFormat;
    impl FormatMetadata;
    impl DiffLine;
    impl ChunkSizing;
    impl EmbeddingModelType;
    impl NodeContent;
    impl AnnotationKind;
    impl OcrBoundingGeometry;
    impl RevisionAnchor;
    fn extract_bytes;
    fn extract_file;
    fn extract_file_sync;
    fn extract_bytes_sync;
    fn batch_extract_files_sync;
    fn batch_extract_bytes_sync;
    fn batch_extract_files;
    fn batch_extract_bytes;
    fn detect_mime_type_from_bytes;
    fn get_extensions_for_mime;
    fn list_embedding_backends;
    fn list_document_extractors;
    fn list_ocr_backends;
    fn list_post_processors;
    fn list_renderers;
    fn list_validators;
    fn compare;
    fn embed_texts_async;
    fn render_pdf_page_to_png;
    fn detect_mime_type;
    fn embed_texts;
    fn get_embedding_preset;
    fn list_embedding_presets;
    fn register_ocr_backend;
    fn unregister_ocr_backend;
    fn clear_ocr_backends;
    fn register_post_processor;
    fn unregister_post_processor;
    fn clear_post_processors;
    fn register_validator;
    fn unregister_validator;
    fn clear_validators;
    fn register_embedding_backend;
    fn unregister_embedding_backend;
    fn clear_embedding_backends;
    fn register_document_extractor;
    fn unregister_document_extractor;
    fn clear_document_extractors;
    fn register_renderer;
    fn unregister_renderer;
    fn clear_renderers;
}
