//! Data model traits and lightweight extraction helpers.

use crate::extractor::ExtractorError;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{fs, path::Path};
use validator::Validate;

pub mod emails;
pub use emails::Emails;
pub mod validate;

/// Basic email regex used by built-in post-processing helpers.
pub const EMAIL_REGEX: &str = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}";
/// Basic phone regex used by downstream `Info` implementations.
pub const PHONE_REGEX: &str = r"(\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}";

/// Structured output model for extracted data.
pub trait Info: Validate + Serialize + for<'de> Deserialize<'de> + schemars::JsonSchema {
    /// JSON schema supplied to extractors that support structured output.
    ///
    /// Prefer deriving `schemars::JsonSchema` on the type and using Rust doc
    /// comments or `#[schemars(description = "...")]` / `#[schemars(title =
    /// "...")]` annotations to control schema descriptions consumed by
    /// structured-output backends.
    fn json_schema() -> serde_json::Value;
    /// Additional extractor instructions appended to the structured-output
    /// prompt for this type.
    fn extraction_instructions() -> Option<&'static str> {
        None
    }
    /// Compact continuity hints derived from previously accepted outputs in the
    /// same extraction run.
    fn continuity_instructions(_items: &[Self]) -> Option<String> {
        None
    }
    /// Save one record as pretty JSON.
    fn save(&self, path: &Path) -> Result<(), ExtractorError> {
        fs::write(path, serde_json::to_string_pretty(self)?)?;
        Ok(())
    }
    /// Save multiple records as pretty JSON.
    fn save_multiple(items: &[Self], path: &Path) -> Result<(), ExtractorError> {
        let json = serde_json::to_string_pretty(items)?;
        fs::write(path, json)?;
        Ok(())
    }
    /// Refine extracted data using original source text.
    fn post_process(&mut self, _original_text: &str) -> Result<(), ExtractorError> {
        Ok(())
    }
}

/// Return the first regex match in `original_text`.
pub fn extract_regex(original_text: &str, regex_str: &str) -> Result<String, ExtractorError> {
    Regex::new(regex_str)?
        .find(original_text)
        .map(|m| m.as_str().to_string())
        .ok_or(ExtractorError::RegexExtractionError)
}

/// Return all regex matches in `text`.
pub fn extract_all_regex(text: &str, regex_str: &str) -> Result<Vec<String>, ExtractorError> {
    let re = Regex::new(regex_str)?;
    let matches: Vec<String> = re.find_iter(text).map(|m| m.as_str().to_string()).collect();
    if matches.is_empty() {
        Err(ExtractorError::RegexExtractionError)
    } else {
        Ok(matches)
    }
}

#[cfg(test)]
mod tests;
