//! XFA (XML Forms Architecture) support for hybrid PDFs
//!
//! Provides detection and data extraction from XFA forms. Full XFA rendering
//! is not supported (it requires HTML/CSS-like layout); this module focuses
//! on reading field values from the XFA Data packet.
//!
//! XFA data is stored in the /AcroForm dictionary under the /XFA key, either
//! as a single stream or as an array of name/stream pairs.

use crate::fitz::error::{Error, Result};
use crate::fitz::geometry::Rect;
use crate::pdf::filter::flate::decode_flate;
use crate::pdf::form::{Form, FormField, WidgetType};
use std::collections::HashMap;

// ============================================================================
// Error Type
// ============================================================================

/// XFA-specific errors
#[derive(Debug, thiserror::Error)]
pub enum XfaError {
    #[error("XFA not present in document")]
    NotPresent,
    #[error("XFA parsing failed: {0}")]
    Parse(String),
    #[error("XFA stream decompression failed: {0}")]
    Decompress(String),
    #[error("XFA XML parsing failed: {0}")]
    Xml(String),
    #[error("XFA rendering not supported")]
    RenderingNotSupported,
}

impl From<XfaError> for Error {
    fn from(e: XfaError) -> Self {
        Error::Pdf(e.to_string())
    }
}

// ============================================================================
// XFA Packet Types
// ============================================================================

/// XFA packet types as defined in the XFA specification
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XfaPacket {
    /// Form template (layout, fields, calculations)
    Template,
    /// Form data (field values)
    Data,
    /// Configuration
    Config,
    /// Connection set
    ConnectionSet,
    /// Source set
    SourceSet,
    /// Locale set
    LocaleSet,
    /// XDC (XML Data Configuration)
    Xdc,
    /// Unknown packet type
    Unknown,
}

impl XfaPacket {
    pub fn from_name(name: &str) -> Self {
        match name.to_lowercase().as_str() {
            "template" => Self::Template,
            "datasets" | "data" => Self::Data,
            "config" => Self::Config,
            "connectionset" => Self::ConnectionSet,
            "sourceset" => Self::SourceSet,
            "localeset" => Self::LocaleSet,
            "xdc" => Self::Xdc,
            _ => Self::Unknown,
        }
    }
}

// ============================================================================
// XFA Form
// ============================================================================

/// Represents an XFA form with its XML data streams
#[derive(Debug, Clone, Default)]
pub struct XfaForm {
    /// Packet name -> XML content
    packets: HashMap<String, String>,
}

impl XfaForm {
    /// Create a new empty XFA form
    pub fn new() -> Self {
        Self {
            packets: HashMap::new(),
        }
    }

    /// Add or replace a packet
    pub fn add_packet(&mut self, name: impl Into<String>, xml: impl Into<String>) {
        self.packets.insert(name.into(), xml.into());
    }

    /// Get a packet by name
    pub fn get_packet(&self, name: &str) -> Option<&str> {
        self.packets.get(name).map(|s| s.as_str())
    }

    /// Get the Data packet XML (contains field values)
    pub fn data_xml(&self) -> Option<&str> {
        self.get_packet("datasets")
            .or_else(|| self.get_packet("data"))
            .or_else(|| self.get_packet("xdp")) // Single stream contains full XDP with datasets
    }

    /// Check if this form has any packets
    pub fn is_empty(&self) -> bool {
        self.packets.is_empty()
    }

    /// Get all packet names
    pub fn packet_names(&self) -> impl Iterator<Item = &str> {
        self.packets.keys().map(|s| s.as_str())
    }
}

// ============================================================================
// Detection and Parsing
// ============================================================================

/// Check if a PDF has XFA data in the /AcroForm dictionary (/XFA key)
pub fn detect_xfa(data: &[u8]) -> bool {
    let text = String::from_utf8_lossy(data);
    // Must have AcroForm with XFA
    if text.find("/AcroForm").is_none() {
        return false;
    }
    if text.find("/XFA").is_none() {
        return false;
    }
    // Ensure XFA is within AcroForm context (simple heuristic: XFA appears after AcroForm)
    if let (Some(af_pos), Some(xfa_pos)) = (text.find("/AcroForm"), text.find("/XFA")) {
        return xfa_pos > af_pos;
    }
    false
}

/// Simple boolean check - alias for detect_xfa
pub fn has_xfa(data: &[u8]) -> bool {
    detect_xfa(data)
}

/// Extract a balanced dictionary starting at "<<"
fn extract_balanced_dict(text: &str, start: usize) -> Option<String> {
    let bytes = text.as_bytes();
    if start + 1 >= bytes.len() || bytes[start] != b'<' || bytes[start + 1] != b'<' {
        return None;
    }
    let mut depth = 0i32;
    let mut i = start;
    while i < bytes.len().saturating_sub(1) {
        if bytes[i] == b'<' && bytes[i + 1] == b'<' {
            depth += 1;
            i += 2;
        } else if bytes[i] == b'>' && bytes[i + 1] == b'>' {
            depth -= 1;
            i += 2;
            if depth == 0 {
                return Some(text[start..i].to_string());
            }
        } else {
            i += 1;
        }
    }
    Some(text[start..].to_string())
}

/// Extract stream data for a PDF object by number
fn extract_stream_for_object(data: &[u8], obj_num: i32) -> Option<Vec<u8>> {
    let text = String::from_utf8_lossy(data);
    let obj_header = format!("{} 0 obj", obj_num);
    let obj_start = text.find(&obj_header)?;
    let after_obj = &text[obj_start + obj_header.len()..];

    let dict_start = after_obj.find("<<")?;
    let dict_text = extract_balanced_dict(&text[obj_start + obj_header.len()..], dict_start)?;

    // Find "stream" keyword
    let stream_kw = after_obj.find("stream")?;
    let stream_data_start = obj_start + obj_header.len() + stream_kw + 6;
    // Skip newline after "stream"
    let mut data_start = stream_data_start;
    while data_start < data.len() && (data[data_start] == b'\n' || data[data_start] == b'\r') {
        data_start += 1;
    }

    let after_stream = &text[stream_data_start..];
    let endstream_pos = after_stream.find("endstream")?;
    let stream_len = endstream_pos;
    let raw_stream = &data[data_start..data_start + stream_len];

    // Check for FlateDecode
    if dict_text.contains("/FlateDecode") || dict_text.contains("/Fl ") {
        decode_flate(raw_stream, None).ok()
    } else {
        Some(raw_stream.to_vec())
    }
}

/// Extract object reference (N 0 R) after key
fn extract_obj_ref_after_key(dict: &str, key: &str) -> Option<i32> {
    let pos = dict.find(key)?;
    let after = dict[pos + key.len()..].trim_start();
    let end = after
        .find(|c: char| !c.is_ascii_digit() && c != ' ' && c != '\t')
        .unwrap_or(after.len());
    let num_str = after[..end].trim();
    num_str.split_whitespace().next()?.parse::<i32>().ok()
}

/// Parse XFA XML data from PDF bytes.
/// Handles both single stream and array of name/stream pairs.
pub fn parse_xfa_streams(data: &[u8]) -> Result<XfaForm> {
    if !detect_xfa(data) {
        return Err(XfaError::NotPresent.into());
    }

    let text = String::from_utf8_lossy(data);
    let acroform_key_pos = text.find("/AcroForm").ok_or(XfaError::NotPresent)?;
    let after_acroform = text[acroform_key_pos + 9..].trim_start(); // 9 = len("/AcroForm")

    let dict_text = if after_acroform.starts_with("<<") {
        // Inline AcroForm dict
        let dict_start = acroform_key_pos + 9 + after_acroform.find("<<").unwrap_or(0);
        extract_balanced_dict(&text, dict_start)
            .ok_or_else(|| XfaError::Parse("Invalid AcroForm dict".into()))?
    } else {
        // Indirect reference: /AcroForm N 0 R - find object N and get its dict
        let obj_num: i32 = after_acroform
            .split_whitespace()
            .next()
            .and_then(|s| s.parse().ok())
            .ok_or_else(|| XfaError::Parse("Invalid AcroForm reference".into()))?;
        let obj_header = format!("{} 0 obj", obj_num);
        let obj_start = text
            .find(&obj_header)
            .ok_or_else(|| XfaError::Parse(format!("AcroForm object {} not found", obj_num)))?;
        let after_obj = &text[obj_start + obj_header.len()..];
        let dict_offset = after_obj
            .find("<<")
            .ok_or_else(|| XfaError::Parse("AcroForm object has no dict".into()))?;
        extract_balanced_dict(&text, obj_start + obj_header.len() + dict_offset)
            .ok_or_else(|| XfaError::Parse("Invalid AcroForm dict".into()))?
    };

    let xfa_pos = dict_text
        .find("/XFA")
        .ok_or_else(|| XfaError::Parse("/XFA key not found".into()))?;
    let after_xfa = &dict_text[xfa_pos + 4..].trim_start();

    let mut form = XfaForm::new();

    if after_xfa.starts_with('[') {
        // Array of name/stream pairs: [ (template) 4 0 R (datasets) 5 0 R ... ]
        let mut i = 1usize; // skip '['
        let arr_content = &after_xfa[1..];
        let mut depth = 1i32;
        let mut arr_end = 0;
        for (j, ch) in arr_content.chars().enumerate() {
            match ch {
                '[' => depth += 1,
                ']' => {
                    depth -= 1;
                    if depth == 0 {
                        arr_end = j;
                        break;
                    }
                }
                _ => {}
            }
        }
        let arr_text = &arr_content[..arr_end];

        // Parse pairs: (name) N 0 R - collect name, then on obj ref extract stream and add
        let mut pending_name: Option<String> = None;
        let mut pos = 0;
        while pos < arr_text.len() {
            let rest = arr_text[pos..].trim_start();
            pos += (arr_text.len() - pos) - rest.len(); // Skip leading whitespace
            if rest.is_empty() {
                break;
            }
            if rest.starts_with('(') {
                let mut depth = 0i32;
                let mut end = 0;
                for (k, ch) in rest.chars().enumerate() {
                    match ch {
                        '(' => depth += 1,
                        ')' => {
                            depth -= 1;
                            if depth == 0 {
                                end = k;
                                break;
                            }
                        }
                        _ => {}
                    }
                }
                if end > 1 {
                    pending_name = Some(rest[1..end].to_string());
                    pos += end + 1;
                } else {
                    pos += 1;
                }
            } else if rest
                .chars()
                .next()
                .map(|c| c.is_ascii_digit())
                .unwrap_or(false)
            {
                let Some(obj_num) = rest
                    .split_whitespace()
                    .next()
                    .and_then(|s| s.parse::<i32>().ok())
                else {
                    pos += 1;
                    continue;
                };
                if let Some(stream_data) = extract_stream_for_object(data, obj_num) {
                    if let Ok(xml) = String::from_utf8(stream_data) {
                        if let Some(name) = pending_name.take() {
                            form.add_packet(name, xml);
                        }
                    }
                }
                pos += rest
                    .find(|c: char| c == 'R')
                    .map(|i| i + 1)
                    .unwrap_or(rest.len());
            } else {
                pos += 1;
            }
        }
    } else {
        // Single stream reference: N 0 R
        let obj_num = extract_obj_ref_after_key(&dict_text, "/XFA")
            .ok_or_else(|| XfaError::Parse("XFA stream reference not found".into()))?;
        let stream_data = extract_stream_for_object(data, obj_num)
            .ok_or_else(|| XfaError::Parse("Failed to extract XFA stream".into()))?;
        let xml = String::from_utf8(stream_data)
            .map_err(|e| XfaError::Parse(format!("Invalid UTF-8 in XFA stream: {}", e)))?;
        form.add_packet("xdp", xml);
    }

    if form.is_empty() {
        return Err(XfaError::Parse("No XFA packets extracted".into()).into());
    }

    Ok(form)
}

// ============================================================================
// Field Extraction
// ============================================================================

/// Extract field name→value mappings from XFA Data packet XML.
/// The Data packet contains <xfa:data> with a tree of elements; leaf elements
/// with text content are treated as field values, keyed by the element path/name.
pub fn extract_xfa_field_values(xml: &str) -> HashMap<String, String> {
    let mut fields = HashMap::new();

    // XFA Data packet structure: <xfa:data><Form><FieldName>value</FieldName>...</Form></xfa:data>
    // We use a simple approach: find <ElementName>value</ElementName> patterns
    // and collect leaf text nodes. This handles common structures like:
    // <FirstName>John</FirstName><LastName>Doe</LastName>

    let mut i = 0;
    let bytes = xml.as_bytes();
    while i < bytes.len() {
        if bytes[i] == b'<' && i + 1 < bytes.len() && bytes[i + 1] != b'/' && bytes[i + 1] != b'!' {
            let tag_start = i + 1;
            let mut tag_end = tag_start;
            while tag_end < bytes.len() && bytes[tag_end] != b'>' && bytes[tag_end] != b' ' {
                tag_end += 1;
            }
            let tag_name = String::from_utf8_lossy(&bytes[tag_start..tag_end]);
            let closing = format!("</{}>", tag_name);
            let closing_bytes = closing.as_bytes();
            if let Some(close_pos) = xml[i..].find(&closing) {
                let content_start = i + tag_end - tag_start + 2; // skip <tagname>
                let mut content_end = i + close_pos;
                while content_end > content_start
                    && (bytes[content_end - 1] == b' ' || bytes[content_end - 1] == b'\n')
                {
                    content_end -= 1;
                }
                let content_slice = String::from_utf8_lossy(&bytes[content_start..content_end]);
                let content = content_slice.trim();
                if !content.is_empty() && !content.contains('<') {
                    // Leaf element with text content
                    let key = tag_name.to_string();
                    if !key.starts_with("xfa:") && !key.starts_with("xdp:") {
                        fields.insert(key, content.to_string());
                    }
                }
            }
            i += 1;
        } else {
            i += 1;
        }
    }

    fields
}

/// Convert XFA field values to standard AcroForm FormField objects for interop
pub fn xfa_to_acroform_fields(field_values: &HashMap<String, String>) -> Vec<FormField> {
    let mut fields = Vec::new();
    let rect = Rect {
        x0: 0.0,
        y0: 0.0,
        x1: 0.0,
        y1: 0.0,
    };
    for (name, value) in field_values {
        let mut field = FormField::new(name.clone(), WidgetType::Text, rect);
        let _ = field.set_value(value.clone());
        fields.push(field);
    }
    fields
}

/// Convenience: parse XFA from PDF data and return form fields as AcroForm-compatible Form
pub fn xfa_fields_to_form(data: &[u8]) -> Result<Form> {
    let xfa = parse_xfa_streams(data)?;
    let data_xml = xfa
        .data_xml()
        .ok_or_else(|| XfaError::Parse("No Data packet in XFA".into()))?;
    let values = extract_xfa_field_values(data_xml);
    let form_fields = xfa_to_acroform_fields(&values);
    let mut form = Form::new();
    for f in form_fields {
        form.add_field(f);
    }
    Ok(form)
}

/// Return a clear error for XFA rendering attempts
pub fn xfa_rendering_error() -> XfaError {
    XfaError::RenderingNotSupported
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    fn make_pdf_with_xfa_array() -> Vec<u8> {
        // Minimal PDF with AcroForm containing XFA as array of name/stream pairs
        let xfa_data = r#"<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><form1><FirstName>John</FirstName><LastName>Doe</LastName><Email>john@example.com</Email></form1></xfa:data></xfa:datasets>"#;
        let data_len = xfa_data.len();
        format!(
            r#"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 3 0 R >> endobj
2 0 obj << /Type /Pages /Count 1 /Kids [4 0 R] >> endobj
3 0 obj << /Fields [] /XFA [ (template) 5 0 R (datasets) 6 0 R ] >> endobj
4 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
5 0 obj << /Length 10 >> stream
<xdp:xdp></xdp:xdp>
endstream
endobj
6 0 obj << /Length {} >> stream
{}
endstream
endobj
xref
0 7
0000000000 65535 f 
0000000099 00000 n 
0000000165 00000 n 
0000000220 00000 n 
0000000275 00000 n 
0000000320 00000 n 
0000000370 00000 n 
trailer << /Size 7 /Root 1 0 R >>
startxref
{}
%%EOF"#,
            data_len,
            xfa_data,
            370 + data_len + 100
        )
        .into_bytes()
    }

    fn make_pdf_with_xfa_single_stream() -> Vec<u8> {
        let xfa_data = r#"<xdp:xdp><xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><form1><Name>Test</Name></form1></xfa:data></xfa:datasets></xdp:xdp>"#;
        let data_len = xfa_data.len();
        format!(
            r#"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 3 0 R >> endobj
2 0 obj << /Type /Pages /Count 1 /Kids [4 0 R] >> endobj
3 0 obj << /Fields [] /XFA 5 0 R >> endobj
4 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
5 0 obj << /Length {} >> stream
{}
endstream
endobj
xref
0 6
0000000000 65535 f 
0000000099 00000 n 
0000000165 00000 n 
0000000220 00000 n 
0000000275 00000 n 
0000000320 00000 n 
trailer << /Size 6 /Root 1 0 R >>
startxref
{}
%%EOF"#,
            data_len,
            xfa_data,
            320 + data_len + 80
        )
        .into_bytes()
    }

    fn make_pdf_without_xfa() -> Vec<u8> {
        b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
xref
0 4
0000000000 65535 f 
0000000058 00000 n 
0000000115 00000 n 
0000000172 00000 n 
trailer << /Size 4 /Root 1 0 R >>
startxref
219
%%EOF"
            .to_vec()
    }

    fn make_pdf_with_acroform_no_xfa() -> Vec<u8> {
        b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 3 0 R >> endobj
2 0 obj << /Type /Pages /Count 1 /Kids [4 0 R] >> endobj
3 0 obj << /Fields [] >> endobj
4 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
xref
0 5
0000000000 65535 f 
0000000099 00000 n 
0000000165 00000 n 
0000000205 00000 n 
0000000260 00000 n 
trailer << /Size 5 /Root 1 0 R >>
startxref
307
%%EOF"
            .to_vec()
    }

    #[test]
    fn test_detect_xfa_present() {
        let pdf = make_pdf_with_xfa_array();
        assert!(detect_xfa(&pdf));
        assert!(has_xfa(&pdf));
    }

    #[test]
    fn test_detect_xfa_absent() {
        let pdf = make_pdf_without_xfa();
        assert!(!detect_xfa(&pdf));
        assert!(!has_xfa(&pdf));
    }

    #[test]
    fn test_detect_xfa_acroform_without_xfa() {
        let pdf = make_pdf_with_acroform_no_xfa();
        assert!(!detect_xfa(&pdf));
    }

    #[test]
    fn test_xfa_packet_from_name() {
        assert_eq!(XfaPacket::from_name("template"), XfaPacket::Template);
        assert_eq!(XfaPacket::from_name("datasets"), XfaPacket::Data);
        assert_eq!(XfaPacket::from_name("config"), XfaPacket::Config);
        assert_eq!(
            XfaPacket::from_name("ConnectionSet"),
            XfaPacket::ConnectionSet
        );
        assert_eq!(XfaPacket::from_name("unknown"), XfaPacket::Unknown);
    }

    #[test]
    fn test_extract_xfa_field_values() {
        let xml = r#"<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><form1><FirstName>John</FirstName><LastName>Doe</LastName><Email>john@example.com</Email></form1></xfa:data></xfa:datasets>"#;
        let values = extract_xfa_field_values(xml);
        assert_eq!(values.get("FirstName"), Some(&"John".to_string()));
        assert_eq!(values.get("LastName"), Some(&"Doe".to_string()));
        assert_eq!(values.get("Email"), Some(&"john@example.com".to_string()));
        assert_eq!(values.len(), 3);
    }

    #[test]
    fn test_extract_xfa_field_values_nested() {
        let xml =
            r#"<form><Section1><Field1>Value1</Field1></Section1><Field2>Value2</Field2></form>"#;
        let values = extract_xfa_field_values(xml);
        assert_eq!(values.get("Field1"), Some(&"Value1".to_string()));
        assert_eq!(values.get("Field2"), Some(&"Value2".to_string()));
    }

    #[test]
    fn test_xfa_to_acroform_fields() {
        let mut values = HashMap::new();
        values.insert("name".to_string(), "Alice".to_string());
        values.insert("email".to_string(), "alice@test.com".to_string());
        let fields = xfa_to_acroform_fields(&values);
        assert_eq!(fields.len(), 2);
        let names: Vec<_> = fields.iter().map(|f| f.name()).collect();
        assert!(names.contains(&"name"));
        assert!(names.contains(&"email"));
        assert_eq!(
            fields.iter().find(|f| f.name() == "name").unwrap().value(),
            "Alice"
        );
    }

    #[test]
    fn test_parse_xfa_streams_array() {
        let pdf = make_pdf_with_xfa_array();
        let xfa = parse_xfa_streams(&pdf).unwrap();
        assert!(!xfa.is_empty());
        assert!(xfa.get_packet("datasets").is_some());
        let data = xfa.data_xml().unwrap();
        let values = extract_xfa_field_values(data);
        assert_eq!(values.get("FirstName"), Some(&"John".to_string()));
    }

    #[test]
    fn test_parse_xfa_streams_single() {
        let pdf = make_pdf_with_xfa_single_stream();
        let xfa = parse_xfa_streams(&pdf).unwrap();
        assert!(!xfa.is_empty());
        let data = xfa.data_xml();
        assert!(data.is_some());
    }

    #[test]
    fn test_parse_xfa_not_present() {
        let pdf = make_pdf_without_xfa();
        let result = parse_xfa_streams(&pdf);
        assert!(result.is_err());
    }

    #[test]
    fn test_xfa_fields_to_form() {
        let pdf = make_pdf_with_xfa_array();
        let form = xfa_fields_to_form(&pdf).unwrap();
        assert_eq!(form.len(), 3);
        assert_eq!(form.get_field_value("FirstName"), Some("John"));
        assert_eq!(form.get_field_value("LastName"), Some("Doe"));
    }

    #[test]
    fn test_xfa_rendering_error() {
        let err = xfa_rendering_error();
        assert!(matches!(err, XfaError::RenderingNotSupported));
    }
}
