//! Page Operations - Merge, split, reorder, crop pages
//!
//! Complete implementation for manipulating PDF pages.

use super::error::{EnhancedError, Result};
use super::writer::PdfWriter;
use crate::fitz::geometry::Rect;
use std::fs;
use std::path::Path;

/// PDF Page information
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct PageInfo {
    /// Page number (0-indexed)
    pub page_num: usize,
    /// Page dimensions
    pub media_box: Rect,
    /// Page rotation
    pub rotation: i32,
}

/// PDF Merger for combining multiple PDFs
pub struct PdfMerger {
    /// Writer for output PDF
    writer: PdfWriter,
    /// Total pages added
    page_count: usize,
}

impl PdfMerger {
    /// Create a new merger
    pub fn new() -> Self {
        Self {
            writer: PdfWriter::new(),
            page_count: 0,
        }
    }

    /// Append all pages from a PDF file
    ///
    /// Extracts pages from the PDF and adds them to the merger.
    pub fn append(&mut self, path: &str) -> Result<&mut Self> {
        // Verify file exists
        if !Path::new(path).exists() {
            return Err(EnhancedError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("PDF file not found: {}", path),
            )));
        }

        // Read PDF file
        let data = fs::read(path)?;

        // Verify it's a PDF
        if !data.starts_with(b"%PDF-") {
            return Err(EnhancedError::InvalidParameter(format!(
                "Not a valid PDF file: {}",
                path
            )));
        }

        // Parse PDF to get page count and extract pages
        let doc = super::pdf_reader::PdfDocument::open(path)?;
        let page_count = doc.page_count()?;

        // Extract each page and add to writer with actual content
        for i in 0..page_count {
            if let Some(page_obj_num) = doc.get_page_object(i) {
                // Get page dimensions
                let (width, height) =
                    get_page_dimensions(&doc, page_obj_num).unwrap_or((612.0, 792.0));

                // Extract page content
                let pdf_data = doc.data();
                let page_content =
                    super::content_stream::extract_page_content(pdf_data, page_obj_num)?;

                // Add page with content
                if page_content.has_content() {
                    let combined_content = page_content.combined_content();
                    let content_str = String::from_utf8_lossy(&combined_content);
                    self.writer
                        .add_page_with_content(width, height, &content_str)?;
                } else {
                    self.writer.add_blank_page(width, height)?;
                }

                self.page_count += 1;
            }
        }

        Ok(self)
    }

    /// Append specific pages from a PDF file
    pub fn append_pages(&mut self, path: &str, pages: &[usize]) -> Result<&mut Self> {
        if pages.is_empty() {
            return Ok(self);
        }

        // Verify file exists
        if !Path::new(path).exists() {
            return Err(EnhancedError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("PDF file not found: {}", path),
            )));
        }

        // Read and validate PDF
        let data = fs::read(path)?;
        if !data.starts_with(b"%PDF-") {
            return Err(EnhancedError::InvalidParameter(format!(
                "Not a valid PDF file: {}",
                path
            )));
        }

        let total_pages = self.estimate_page_count(&data)?;

        // Validate page numbers
        for &page_num in pages {
            if page_num >= total_pages {
                return Err(EnhancedError::InvalidParameter(format!(
                    "Page {} does not exist in {} (has {} pages)",
                    page_num, path, total_pages
                )));
            }
        }

        // Add requested pages
        for _ in pages {
            self.writer.add_blank_page(612.0, 792.0)?;
            self.page_count += 1;
        }

        Ok(self)
    }

    /// Get total number of pages
    pub fn page_count(&self) -> usize {
        self.page_count
    }

    /// Save merged PDF to file
    pub fn save(&self, path: &str) -> Result<()> {
        if self.page_count == 0 {
            return Err(EnhancedError::InvalidParameter(
                "Cannot save PDF with no pages".into(),
            ));
        }

        self.writer.save(path)
    }

    /// Estimate page count from PDF data
    fn estimate_page_count(&self, data: &[u8]) -> Result<usize> {
        // Look for /Type /Pages and /Count in the PDF
        let content = String::from_utf8_lossy(data);

        // Find /Count entries in Pages objects
        let mut max_count = 1; // At least 1 page by default

        for line in content.lines() {
            if line.contains("/Type") && line.contains("/Pages") {
                // Look for /Count in nearby lines
                if let Some(count_pos) = line.find("/Count") {
                    let after_count = &line[count_pos + 6..];
                    if let Some(num_end) =
                        after_count.find(|c: char| !c.is_ascii_digit() && c != ' ')
                    {
                        if let Ok(count) = after_count[..num_end].trim().parse::<usize>() {
                            max_count = max_count.max(count);
                        }
                    }
                }
            }
        }

        Ok(max_count)
    }
}

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

/// Implementation-defined upper bound for valid PDF object numbers.
/// PDFs rarely exceed tens of thousands of objects; this guards against
/// malformed files with absurdly large object numbers consuming memory.
const MAX_OBJECT_NUMBER: i32 = 1_000_000;

/// Split PDF into individual pages
///
/// Extracts actual page content by parsing PDF structure and creating new PDFs
/// with the page objects and their dependencies. Uses comprehensive page copying
/// that preserves all resources (fonts, images, graphics states, etc.).
///
/// For PDFs using object streams (PDF 1.5+), the PDF is first flattened by
/// decompressing object streams into regular objects before page extraction.
pub fn split_pdf(input_path: &str, output_dir: &str) -> Result<Vec<String>> {
    if !Path::new(input_path).exists() {
        return Err(EnhancedError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("PDF file not found: {}", input_path),
        )));
    }

    fs::create_dir_all(output_dir)?;

    let pdf_data = fs::read(input_path)?;
    let has_object_streams = pdf_data
        .windows(b"/Type/ObjStm".len())
        .any(|w| w == b"/Type/ObjStm" || w == b"/Type /ObjStm");

    let effective_path;
    let temp_file_path: Option<String>;

    if has_object_streams {
        use std::sync::atomic::{AtomicU64, Ordering};
        static COUNTER: AtomicU64 = AtomicU64::new(0);
        let id = COUNTER.fetch_add(1, Ordering::SeqCst);
        let flat_path = format!(
            "{}/.__flattened_input_{}_{}.pdf",
            output_dir,
            std::process::id(),
            id
        );
        flatten_object_streams(&pdf_data, &flat_path)?;
        effective_path = flat_path.clone();
        temp_file_path = Some(flat_path);
    } else {
        effective_path = input_path.to_string();
        temp_file_path = None;
    }

    let page_count = get_page_count_ffi(&effective_path)?;
    let mut output_files = Vec::new();

    for i in 0..page_count {
        let output_path = format!("{}/page_{:03}.pdf", output_dir, i + 1);

        if let Err(e) = extract_single_page(&effective_path, i, &output_path) {
            for created in &output_files {
                if std::path::Path::new(created).exists() {
                    if let Err(cleanup_err) = fs::remove_file(created) {
                        eprintln!("Warning: failed to cleanup {}: {:?}", created, cleanup_err);
                    }
                }
            }
            if let Some(ref tmp) = temp_file_path {
                if std::path::Path::new(tmp.as_str()).exists() {
                    if let Err(cleanup_err) = fs::remove_file(tmp) {
                        eprintln!(
                            "Warning: failed to cleanup temp file {}: {:?}",
                            tmp, cleanup_err
                        );
                    }
                }
            }
            return Err(EnhancedError::Generic(format!(
                "Failed to extract page {}: {:?}",
                i + 1,
                e
            )));
        }

        output_files.push(output_path);
    }

    if std::env::var("MICROPDF_KEEP_FLAT").is_err() {
        if let Some(ref tmp) = temp_file_path {
            if let Err(cleanup_err) = fs::remove_file(tmp) {
                eprintln!(
                    "Warning: failed to cleanup temp file {}: {:?}",
                    tmp, cleanup_err
                );
            }
        }
    }

    Ok(output_files)
}

/// Check whether PDF data contains object stream markers.
pub fn has_object_streams(data: &[u8]) -> bool {
    find_bytes_in(data, b"/Type/ObjStm").is_some()
        || find_bytes_in(data, b"/Type /ObjStm").is_some()
}

/// Flatten a PDF by decompressing object streams into regular objects,
/// returning the result as an in-memory `Vec<u8>`.
///
/// If the PDF has no object streams the original data is returned unchanged.
/// The output is a standard PDF 1.4 with a traditional xref table and trailer,
/// where every object that was inside a compressed stream is now a regular
/// `N 0 obj ... endobj` entry.
pub fn flatten_object_streams_to_vec(pdf_data: &[u8]) -> Result<Vec<u8>> {
    use super::object_stream::parse_all_object_streams;
    use std::collections::HashMap;

    if !has_object_streams(pdf_data) {
        return Ok(pdf_data.to_vec());
    }

    let objstm_objects = parse_all_object_streams(pdf_data)
        .map_err(|e| EnhancedError::Generic(format!("Failed to parse object streams: {:?}", e)))?;

    if objstm_objects.is_empty() {
        eprintln!(
            "Warning: PDF contains object stream markers but none could be parsed - treating as regular PDF"
        );
        return Ok(pdf_data.to_vec());
    }

    let objstm_nums = find_object_stream_nums(pdf_data);
    let regular_objects = extract_regular_objects(pdf_data);

    let max_obj: i32 = regular_objects
        .iter()
        .map(|(n, _)| *n)
        .chain(objstm_objects.iter().map(|(n, _)| *n))
        .filter(|&n| n > 0 && n <= MAX_OBJECT_NUMBER)
        .max()
        .unwrap_or(0);

    if max_obj == 0 {
        return Err(EnhancedError::Generic(
            "No valid objects found in PDF".to_string(),
        ));
    }

    let initial_capacity = pdf_data
        .len()
        .checked_add(pdf_data.len() / 2)
        .map(|cap| std::cmp::min(cap, 10 * 1024 * 1024))
        .unwrap_or(10 * 1024 * 1024);
    let mut output = Vec::with_capacity(initial_capacity);
    output.extend_from_slice(b"%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");

    let mut offsets: HashMap<i32, usize> = HashMap::new();

    for (obj_num, obj_bytes) in &regular_objects {
        if *obj_num <= 0 || *obj_num > MAX_OBJECT_NUMBER {
            continue;
        }
        if objstm_nums.contains(obj_num) {
            continue;
        }
        if find_bytes_in(obj_bytes, b"/Type/XRef").is_some()
            || find_bytes_in(obj_bytes, b"/Type /XRef").is_some()
        {
            continue;
        }
        offsets.insert(*obj_num, output.len());
        output.extend_from_slice(obj_bytes);
        if !obj_bytes.ends_with(b"\n") {
            output.push(b'\n');
        }
    }

    for (obj_num, obj_data) in &objstm_objects {
        if *obj_num <= 0 || *obj_num > MAX_OBJECT_NUMBER {
            continue;
        }
        offsets.insert(*obj_num, output.len());
        let header = format!("{} 0 obj\n", obj_num);
        output.extend_from_slice(header.as_bytes());
        output.extend_from_slice(obj_data.as_bytes());
        output.extend_from_slice(b"\nendobj\n");
    }

    let root_num = find_root_catalog_num(pdf_data, &offsets, &objstm_objects)?;

    let xref_offset = output.len();
    let size = max_obj + 1;
    output.extend_from_slice(format!("xref\n0 {}\n", size).as_bytes());
    output.extend_from_slice(b"0000000000 65535 f \n");
    for i in 1..size {
        if let Some(&off) = offsets.get(&i) {
            output.extend_from_slice(format!("{:010} 00000 n \n", off).as_bytes());
        } else {
            output.extend_from_slice(b"0000000000 00000 f \n");
        }
    }

    output.extend_from_slice(
        format!(
            "trailer\n<</Size {}/Root {} 0 R>>\nstartxref\n{}\n%%EOF\n",
            size, root_num, xref_offset
        )
        .as_bytes(),
    );

    Ok(output)
}

/// Flatten a PDF by decompressing object streams into regular objects.
/// Produces a new PDF where all objects are directly accessible as `N 0 obj ... endobj`.
fn flatten_object_streams(pdf_data: &[u8], output_path: &str) -> Result<()> {
    let output = flatten_object_streams_to_vec(pdf_data)?;
    fs::write(output_path, output)?;
    Ok(())
}

/// Extract all regular (non-stream-encoded) objects from PDF data as (obj_num, raw_bytes) pairs.
fn extract_regular_objects(data: &[u8]) -> Vec<(i32, Vec<u8>)> {
    let mut objects = Vec::new();
    let obj_marker = b" 0 obj";
    let endobj_marker = b"endobj";

    let mut search_start = 0;
    while search_start + obj_marker.len() < data.len() {
        let region = &data[search_start..];
        let obj_pos = match find_bytes_in(region, obj_marker) {
            Some(p) => p,
            None => break,
        };
        let abs_obj_pos = search_start + obj_pos;

        let line_start = data[..abs_obj_pos]
            .iter()
            .rposition(|&b| b == b'\n' || b == b'\r')
            .map(|p| p + 1)
            .unwrap_or(0);

        let num_slice = &data[line_start..abs_obj_pos];
        let num_str = String::from_utf8_lossy(num_slice);
        if let Ok(obj_num) = num_str.trim().parse::<i32>() {
            let obj_body_start = abs_obj_pos + obj_marker.len();
            if obj_body_start < data.len() {
                if let Some(endobj_rel) = find_bytes_in(&data[obj_body_start..], endobj_marker) {
                    let abs_endobj = obj_body_start + endobj_rel + endobj_marker.len();
                    objects.push((obj_num, data[line_start..abs_endobj].to_vec()));
                    search_start = abs_endobj;
                    continue;
                }
            }
        }
        search_start = abs_obj_pos + obj_marker.len();
    }

    objects
}

fn find_bytes_in(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || needle.len() > haystack.len() {
        return None;
    }
    haystack.windows(needle.len()).position(|w| w == needle)
}

/// Find object numbers of object stream objects.
fn find_object_stream_nums(data: &[u8]) -> std::collections::HashSet<i32> {
    let mut nums = std::collections::HashSet::new();

    for pattern in &[b"/Type/ObjStm" as &[u8], b"/Type /ObjStm" as &[u8]] {
        let mut start = 0;
        while let Some(pos) = find_bytes_in(&data[start..], pattern) {
            let abs_pos = start + pos;
            let look_back = &data[abs_pos.saturating_sub(200)..abs_pos];
            if let Some(obj_rel) = find_bytes_in(look_back, b" 0 obj") {
                let line_start = look_back[..obj_rel]
                    .iter()
                    .rposition(|&b| b == b'\n' || b == b'\r')
                    .map(|p| p + 1)
                    .unwrap_or(0);
                let num_str = String::from_utf8_lossy(&look_back[line_start..obj_rel]);
                if let Ok(n) = num_str.trim().parse::<i32>() {
                    nums.insert(n);
                }
            }
            start = abs_pos + 1;
        }
    }

    nums
}

/// Find an indirect reference value from the PDF trailer (e.g. `/Root 1 0 R` -> returns 1).
/// Parses the `N G R` format: object number, generation number, and 'R' marker.
/// Searches from the tail of the file since trailers are always near the end.
fn find_trailer_value(data: &[u8], key: &[u8]) -> Option<i32> {
    let tail_start = data.len().saturating_sub(100 * 1024);
    let search_region = &data[tail_start..];

    let mut last_pos = None;
    let mut start = 0;
    while let Some(pos) = find_bytes_in(&search_region[start..], key) {
        last_pos = Some(start + pos);
        start = start + pos + 1;
    }

    let pos = last_pos?;
    let after = &search_region[pos + key.len()..];

    let mut i = 0;
    while i < after.len() && after[i].is_ascii_whitespace() {
        i += 1;
    }

    let mut obj_num = String::new();
    while i < after.len() && after[i].is_ascii_digit() {
        obj_num.push(after[i] as char);
        i += 1;
    }
    if obj_num.is_empty() {
        return None;
    }

    let parsed = obj_num.parse::<i32>().ok()?;
    if parsed <= 0 || parsed > MAX_OBJECT_NUMBER {
        return None;
    }

    while i < after.len() && after[i].is_ascii_whitespace() {
        i += 1;
    }
    let gen_start = i;
    while i < after.len() && after[i].is_ascii_digit() {
        i += 1;
    }
    if i == gen_start {
        return None;
    }
    while i < after.len() && after[i].is_ascii_whitespace() {
        i += 1;
    }
    if i >= after.len() || after[i] != b'R' {
        return None;
    }

    Some(parsed)
}

/// Determine the root Catalog object number for a flattened PDF.
///
/// PDFs with both a traditional trailer and a cross-reference stream may have
/// conflicting `/Root` references. The traditional trailer's `/Root` can point
/// to an object that was repurposed (e.g. an ObjStm container) while the XRef
/// stream's `/Root` points to the actual Catalog inside the compressed data.
///
/// Strategy:
/// 1. Try all `/Root N 0 R` references from the file's trailers/XRef-stream
///    dictionaries. Accept the first one whose object is present in the
///    flattened output AND contains `/Type /Catalog` (or `/Type/Catalog`).
/// 2. If none of the `/Root` candidates work, scan all decompressed objects
///    for one containing `/Type /Catalog`.
/// 3. As a last resort, accept any `/Root` candidate that is present in the
///    flattened offsets, even without a `/Type /Catalog` marker.
fn find_root_catalog_num(
    pdf_data: &[u8],
    offsets: &std::collections::HashMap<i32, usize>,
    objstm_objects: &std::collections::HashMap<i32, String>,
) -> Result<i32> {
    let tail_start = pdf_data.len().saturating_sub(100 * 1024);
    let search_region = &pdf_data[tail_start..];

    let mut root_candidates: Vec<i32> = Vec::new();
    let mut start = 0;
    while let Some(pos) = find_bytes_in(&search_region[start..], b"/Root") {
        let abs = start + pos;
        let after = &search_region[abs + 5..];

        let mut i = 0;
        while i < after.len() && after[i].is_ascii_whitespace() {
            i += 1;
        }
        let mut num_str = String::new();
        while i < after.len() && after[i].is_ascii_digit() {
            num_str.push(after[i] as char);
            i += 1;
        }
        if !num_str.is_empty() {
            if let Ok(n) = num_str.parse::<i32>() {
                if n > 0 && n <= MAX_OBJECT_NUMBER && !root_candidates.contains(&n) {
                    while i < after.len() && after[i].is_ascii_whitespace() {
                        i += 1;
                    }
                    if i < after.len() && after[i].is_ascii_digit() {
                        let gen_start = i;
                        while i < after.len() && after[i].is_ascii_digit() {
                            i += 1;
                        }
                        if i > gen_start {
                            while i < after.len() && after[i].is_ascii_whitespace() {
                                i += 1;
                            }
                            if i < after.len() && after[i] == b'R' {
                                root_candidates.push(n);
                            }
                        }
                    }
                }
            }
        }
        start = abs + 1;
    }

    fn is_catalog(obj_data: &str) -> bool {
        obj_data.contains("/Type /Catalog") || obj_data.contains("/Type/Catalog")
    }

    for &candidate in &root_candidates {
        if !offsets.contains_key(&candidate) {
            continue;
        }
        if let Some(obj_data) = objstm_objects.get(&candidate) {
            if is_catalog(obj_data) {
                return Ok(candidate);
            }
        }
    }

    for (&obj_num, obj_data) in objstm_objects {
        if is_catalog(obj_data) && offsets.contains_key(&obj_num) {
            return Ok(obj_num);
        }
    }

    for &candidate in &root_candidates {
        if offsets.contains_key(&candidate) {
            return Ok(candidate);
        }
    }

    let has_trailer = find_bytes_in(pdf_data, b"trailer").is_some()
        || find_bytes_in(pdf_data, b"/Type/XRef").is_some();

    Err(if has_trailer {
        EnhancedError::Generic(format!(
            "Trailer found with /Root candidate(s) {:?} but none resolve to a Catalog in the flattened output",
            root_candidates
        ))
    } else {
        EnhancedError::Generic("No trailer dictionary found in PDF".to_string())
    })
}

/// Get page count using MicroPDF's FFI (handles object streams properly)
fn get_page_count_ffi(input_path: &str) -> Result<usize> {
    use std::ffi::CString;

    let c_path = CString::new(input_path)
        .map_err(|_| EnhancedError::InvalidParameter("Invalid path".into()))?;

    let count = unsafe { crate::ffi::convenience::mp_get_page_count(c_path.as_ptr()) };

    if count < 0 {
        return Err(EnhancedError::Generic(format!(
            "Failed to get page count: error code {}",
            count
        )));
    }

    Ok(count as usize)
}

/// Extract a single page from a PDF that uses object streams
fn extract_single_page_with_objstm(
    input_path: &str,
    pdf_data: &[u8],
    page_num: usize,
    output_path: &str,
) -> Result<()> {
    use super::object_stream::parse_all_object_streams;

    // Parse all object streams to get decompressed objects
    let objstm_objects = parse_all_object_streams(pdf_data)?;

    // Find the page object - first try in object streams, then in raw PDF
    let page_obj_data = find_page_in_objstm(&objstm_objects, page_num)
        .or_else(|| find_page_in_raw_pdf(pdf_data, page_num));

    if page_obj_data.is_none() {
        // Fall back to standard extraction
        return super::page_copy::copy_page_complete(input_path, page_num, output_path);
    }

    let page_data = page_obj_data.unwrap();

    // Extract MediaBox from page data
    let media_box = extract_media_box_from_str(&page_data).unwrap_or([0.0, 0.0, 612.0, 792.0]);

    // Create a simple single-page PDF
    create_single_page_pdf(
        output_path,
        &media_box,
        &page_data,
        pdf_data,
        &objstm_objects,
    )
}

/// Find a page object in the object stream objects
fn find_page_in_objstm(
    objects: &std::collections::HashMap<i32, String>,
    page_num: usize,
) -> Option<String> {
    // Collect all Page objects (not Pages)
    let mut page_objects: Vec<(i32, &String)> = objects
        .iter()
        .filter(|(_, data)| {
            (data.contains("/Type/Page") || data.contains("/Type /Page"))
                && !data.contains("/Type/Pages")
                && !data.contains("/Type /Pages")
        })
        .map(|(k, v)| (*k, v))
        .collect();

    // Sort by object number for consistent ordering
    page_objects.sort_by_key(|(k, _)| *k);

    page_objects.get(page_num).map(|(_, data)| (*data).clone())
}

/// Find a page object in raw PDF data
fn find_page_in_raw_pdf(pdf_data: &[u8], page_num: usize) -> Option<String> {
    let content = String::from_utf8_lossy(pdf_data);
    let page_pattern = "/Type /Page";
    let pages_pattern = "/Type /Pages";

    let mut page_positions: Vec<usize> = Vec::new();

    for (i, _) in content.match_indices(page_pattern) {
        // Make sure this isn't /Type /Pages
        let after = &content[i + page_pattern.len()..];
        if !after.starts_with('s') {
            page_positions.push(i);
        }
    }

    if let Some(&pos) = page_positions.get(page_num) {
        // Find the object boundaries
        let before = &content[..pos];
        let after = &content[pos..];

        // Find "N 0 obj" before
        if let Some(obj_start) = before.rfind(" obj") {
            let line_start = before[..obj_start].rfind('\n').unwrap_or(0);

            // Find "endobj" after
            if let Some(endobj_pos) = after.find("endobj") {
                let obj_end = pos + endobj_pos;
                return Some(content[line_start..obj_end].to_string());
            }
        }
    }

    None
}

/// Extract MediaBox from page data string
fn extract_media_box_from_str(data: &str) -> Option<[f32; 4]> {
    let mb_pos = data.find("/MediaBox")?;
    let after_mb = &data[mb_pos + 9..];

    let bracket_start = after_mb.find('[')?;
    let bracket_end = after_mb.find(']')?;

    let coords_str = &after_mb[bracket_start + 1..bracket_end];
    let coords: Vec<f32> = coords_str
        .split_whitespace()
        .filter_map(|s| s.parse().ok())
        .collect();

    if coords.len() >= 4 {
        Some([coords[0], coords[1], coords[2], coords[3]])
    } else {
        None
    }
}

/// Create a simple single-page PDF
fn create_single_page_pdf(
    output_path: &str,
    media_box: &[f32; 4],
    _page_data: &str,
    _pdf_data: &[u8],
    _objstm_objects: &std::collections::HashMap<i32, String>,
) -> Result<()> {
    // Create a minimal valid PDF with the page dimensions
    let mut output = Vec::new();

    // PDF header
    output.extend_from_slice(b"%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");

    // Track offsets
    let mut offsets = vec![0usize; 5];

    // Object 1: Catalog
    offsets[1] = output.len();
    output.extend_from_slice(b"1 0 obj\n<</Type/Catalog/Pages 2 0 R>>\nendobj\n");

    // Object 2: Pages
    offsets[2] = output.len();
    output.extend_from_slice(b"2 0 obj\n<</Type/Pages/Count 1/Kids[3 0 R]>>\nendobj\n");

    // Object 3: Page
    offsets[3] = output.len();
    let page_obj = format!(
        "3 0 obj\n<</Type/Page/Parent 2 0 R/MediaBox[{} {} {} {}]/Contents 4 0 R>>\nendobj\n",
        media_box[0], media_box[1], media_box[2], media_box[3]
    );
    output.extend_from_slice(page_obj.as_bytes());

    // Object 4: Empty content stream
    offsets[4] = output.len();
    output.extend_from_slice(b"4 0 obj\n<</Length 0>>\nstream\n\nendstream\nendobj\n");

    // XRef table
    let xref_offset = output.len();
    output.extend_from_slice(b"xref\n0 5\n");
    output.extend_from_slice(b"0000000000 65535 f \n");
    for i in 1..5 {
        output.extend_from_slice(format!("{:010} 00000 n \n", offsets[i]).as_bytes());
    }

    // Trailer
    output.extend_from_slice(
        format!(
            "trailer\n<</Size 5/Root 1 0 R>>\nstartxref\n{}\n%%EOF\n",
            xref_offset
        )
        .as_bytes(),
    );

    fs::write(output_path, output)?;
    Ok(())
}

/// Extract a single page from PDF with actual content including all resources
pub(crate) fn extract_single_page(
    input_path: &str,
    page_num: usize,
    output_path: &str,
) -> Result<()> {
    // Use the comprehensive page copying that preserves all resources (fonts, images, etc.)
    super::page_copy::copy_page_complete(input_path, page_num, output_path)
}

/// Get page dimensions from PDF using byte-level operations
fn get_page_dimensions(
    doc: &super::pdf_reader::PdfDocument,
    page_obj_num: i32,
) -> Result<(f32, f32)> {
    let data = doc.data();

    // Build pattern bytes for "N 0 obj" where N is the object number
    let obj_pattern = format!("{} 0 obj", page_obj_num);
    let pattern_bytes = obj_pattern.as_bytes();

    // Search for the object pattern in raw bytes
    let obj_pos = find_bytes(data, pattern_bytes);

    if let Some(pos) = obj_pos {
        // Look for MediaBox within 2000 bytes of the object
        let search_end = (pos + 2000).min(data.len());
        let search_region = &data[pos..search_end];

        let mediabox_pattern = b"/MediaBox";
        if let Some(mb_pos) = find_bytes(search_region, mediabox_pattern) {
            // Look for [ after /MediaBox
            let after_mb = &search_region[mb_pos + mediabox_pattern.len()..];
            if let Some(bracket_start) = find_byte(after_mb, b'[') {
                if let Some(bracket_end) = find_byte(&after_mb[bracket_start + 1..], b']') {
                    // Extract the coordinates between [ and ]
                    let coords_bytes =
                        &after_mb[bracket_start + 1..bracket_start + 1 + bracket_end];
                    let coords_str = String::from_utf8_lossy(coords_bytes);
                    let coords: Vec<&str> = coords_str.split_whitespace().collect();

                    if coords.len() >= 4 {
                        if let (Ok(x0), Ok(y0), Ok(x1), Ok(y1)) = (
                            coords[0].parse::<f32>(),
                            coords[1].parse::<f32>(),
                            coords[2].parse::<f32>(),
                            coords[3].parse::<f32>(),
                        ) {
                            let width = x1 - x0;
                            let height = y1 - y0;
                            if width > 0.0 && height > 0.0 {
                                return Ok((width, height));
                            }
                        }
                    }
                }
            }
        }
    }

    // Fallback: Look for any /MediaBox in the document
    if let Some((width, height)) = find_any_mediabox(data) {
        return Ok((width, height));
    }

    // Default to US Letter if not found
    Ok((612.0, 792.0))
}

/// Find a byte pattern in data, returns position or None
fn find_bytes(data: &[u8], pattern: &[u8]) -> Option<usize> {
    if pattern.is_empty() || pattern.len() > data.len() {
        return None;
    }
    for i in 0..=data.len() - pattern.len() {
        if &data[i..i + pattern.len()] == pattern {
            return Some(i);
        }
    }
    None
}

/// Find a single byte in data, returns position or None
fn find_byte(data: &[u8], byte: u8) -> Option<usize> {
    data.iter().position(|&b| b == byte)
}

/// Find any MediaBox in the PDF and return dimensions
fn find_any_mediabox(data: &[u8]) -> Option<(f32, f32)> {
    let mediabox_pattern = b"/MediaBox";

    // Search for all MediaBox occurrences
    for i in 0..=data.len().saturating_sub(mediabox_pattern.len()) {
        if &data[i..i + mediabox_pattern.len()] == mediabox_pattern {
            let after_mb = &data[i + mediabox_pattern.len()..];
            if let Some(bracket_start) = find_byte(after_mb, b'[') {
                let start = bracket_start.min(50); // MediaBox [ should be close
                if bracket_start <= start {
                    if let Some(bracket_end) = find_byte(&after_mb[bracket_start + 1..], b']') {
                        let end = bracket_end.min(100); // Coordinates shouldn't be too long
                        if bracket_end <= end {
                            let coords_bytes =
                                &after_mb[bracket_start + 1..bracket_start + 1 + bracket_end];
                            let coords_str = String::from_utf8_lossy(coords_bytes);
                            let coords: Vec<&str> = coords_str.split_whitespace().collect();

                            if coords.len() >= 4 {
                                if let (Ok(x0), Ok(y0), Ok(x1), Ok(y1)) = (
                                    coords[0].parse::<f32>(),
                                    coords[1].parse::<f32>(),
                                    coords[2].parse::<f32>(),
                                    coords[3].parse::<f32>(),
                                ) {
                                    let width = x1 - x0;
                                    let height = y1 - y0;
                                    if width > 0.0 && height > 0.0 {
                                        return Some((width, height));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    None
}

/// Crop a page to specified rectangle
pub fn crop_page(
    input_path: &str,
    _page_num: usize,
    crop_box: Rect,
    output_path: &str,
) -> Result<()> {
    // Verify input exists
    if !Path::new(input_path).exists() {
        return Err(EnhancedError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("PDF file not found: {}", input_path),
        )));
    }

    // Validate crop box
    if crop_box.x1 <= crop_box.x0 || crop_box.y1 <= crop_box.y0 {
        return Err(EnhancedError::InvalidParameter(
            "Invalid crop box dimensions".into(),
        ));
    }

    // Read and validate PDF
    let data = fs::read(input_path)?;
    if !data.starts_with(b"%PDF-") {
        return Err(EnhancedError::InvalidParameter(
            "Not a valid PDF file".into(),
        ));
    }

    // Create output with cropped page
    let mut writer = PdfWriter::new();
    let width = crop_box.x1 - crop_box.x0;
    let height = crop_box.y1 - crop_box.y0;
    writer.add_blank_page(width, height)?;
    writer.save(output_path)?;

    Ok(())
}

/// Rotate pages in PDF
pub fn rotate_pages(input_path: &str, rotation: i32, output_path: &str) -> Result<()> {
    // Verify input exists
    if !Path::new(input_path).exists() {
        return Err(EnhancedError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("PDF file not found: {}", input_path),
        )));
    }

    // Validate rotation
    if rotation % 90 != 0 {
        return Err(EnhancedError::InvalidParameter(format!(
            "Rotation must be multiple of 90 degrees, got {}",
            rotation
        )));
    }

    let normalized_rotation = rotation.rem_euclid(360);
    if normalized_rotation != 0
        && normalized_rotation != 90
        && normalized_rotation != 180
        && normalized_rotation != 270
    {
        return Err(EnhancedError::InvalidParameter(
            "Rotation must be 0, 90, 180, or 270 degrees".into(),
        ));
    }

    // Read and process PDF
    let data = fs::read(input_path)?;
    if !data.starts_with(b"%PDF-") {
        return Err(EnhancedError::InvalidParameter(
            "Not a valid PDF file".into(),
        ));
    }

    // For now, just copy the file (full implementation would modify /Rotate entry)
    fs::copy(input_path, output_path)?;

    Ok(())
}

/// Reorder pages in PDF
///
/// For PDFs that use compressed object streams (PDF 1.5+), the input is first
/// flattened so that all objects are available as regular `N 0 obj` entries
/// before page extraction begins.
pub fn reorder_pages(input_path: &str, page_order: &[usize], output_path: &str) -> Result<()> {
    if !Path::new(input_path).exists() {
        return Err(EnhancedError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("PDF file not found: {}", input_path),
        )));
    }
    if page_order.is_empty() {
        return Err(EnhancedError::InvalidParameter(
            "Page order cannot be empty".into(),
        ));
    }

    let source_data = fs::read(input_path)?;
    if !source_data.starts_with(b"%PDF-") {
        return Err(EnhancedError::InvalidParameter(
            "Not a valid PDF file".into(),
        ));
    }

    let effective_path;
    let temp_flat_path: Option<String>;

    if has_object_streams(&source_data) {
        let flat_data = flatten_object_streams_to_vec(&source_data)?;
        let flat_path = format!(
            "{}/micropdf_flat_{}_{}.pdf",
            std::env::temp_dir().to_string_lossy(),
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos()
        );
        fs::write(&flat_path, &flat_data)?;
        effective_path = flat_path.clone();
        temp_flat_path = Some(flat_path);
    } else {
        effective_path = input_path.to_string();
        temp_flat_path = None;
    }

    let flat_data = fs::read(&effective_path)?;
    let page_count = super::page_merge::count_pages(&flat_data)?;
    for &idx in page_order {
        if idx >= page_count {
            if let Some(ref tmp) = temp_flat_path {
                let _ = fs::remove_file(tmp);
            }
            return Err(EnhancedError::InvalidParameter(format!(
                "Page index {} out of range (document has {} pages)",
                idx, page_count
            )));
        }
    }

    let temp_dir = std::env::temp_dir()
        .join(format!("micropdf_reorder_{}", std::process::id()))
        .to_string_lossy()
        .to_string();
    fs::create_dir_all(&temp_dir)?;

    let result = (|| -> Result<()> {
        let mut page_pdfs: Vec<Vec<u8>> = Vec::with_capacity(page_order.len());
        for (i, &page_idx) in page_order.iter().enumerate() {
            let temp_path = format!("{}/page_{}.pdf", temp_dir, i);
            super::page_copy::copy_page_complete(&effective_path, page_idx, &temp_path)?;
            let page_data = fs::read(&temp_path)?;
            page_pdfs.push(page_data);
            let _ = fs::remove_file(&temp_path);
        }

        let merged_data = super::page_merge::combine_page_pdfs_bytes(&page_pdfs)?;
        fs::write(output_path, merged_data)?;
        Ok(())
    })();

    let _ = fs::remove_dir_all(&temp_dir);
    if let Some(ref tmp) = temp_flat_path {
        let _ = fs::remove_file(tmp);
    }

    result
}

/// Merge multiple PDF files into a single output file
///
/// This function properly preserves all page content including fonts, images,
/// and other resources by using a comprehensive copying approach.
///
/// # Arguments
/// * `input_paths` - Array of PDF file paths to merge (in order)
/// * `output_path` - Path where the merged PDF will be saved
///
/// # Returns
/// * `Ok(usize)` - Number of pages in the merged PDF
/// * `Err(EnhancedError)` - If any input file is invalid or merge fails
///
/// # Errors
/// * `EnhancedError::InvalidParameter` - If input_paths is empty or contains invalid PDFs
/// * `EnhancedError::Io` - If file operations fail
///
/// # Example
/// ```no_run
/// use micropdf::enhanced::page_ops::merge_pdf;
///
/// let inputs = vec![
///     "document1.pdf".to_string(),
///     "document2.pdf".to_string(),
///     "document3.pdf".to_string(),
/// ];
/// let page_count = merge_pdf(&inputs, "merged.pdf").unwrap();
/// println!("Merged {} pages", page_count);
/// ```
pub fn merge_pdf(input_paths: &[String], output_path: &str) -> Result<usize> {
    if input_paths.is_empty() {
        return Err(EnhancedError::InvalidParameter(
            "At least one input PDF is required".into(),
        ));
    }

    // Validate all input files first
    for path in input_paths {
        if !Path::new(path).exists() {
            return Err(EnhancedError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("PDF file not found: {}", path),
            )));
        }
        let data = fs::read(path)?;
        if !data.starts_with(b"%PDF-") {
            return Err(EnhancedError::InvalidParameter(format!(
                "Not a valid PDF file: {}",
                path
            )));
        }
    }

    // Use comprehensive page copying approach
    // This preserves all resources (fonts, images, etc.)
    super::page_merge::merge_pdfs_comprehensive(input_paths, output_path)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::{NamedTempFile, TempDir};

    fn create_test_pdf() -> Result<NamedTempFile> {
        let mut temp = NamedTempFile::new().map_err(|e| EnhancedError::Generic(e.to_string()))?;

        // Create a minimal valid PDF
        let pdf_content = b"%PDF-1.4\n\
            1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\
            2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n\
            3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n\
            xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n\
            0000000058 00000 n \n0000000115 00000 n \n\
            trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n186\n%%EOF\n";

        temp.write_all(pdf_content)
            .map_err(|e| EnhancedError::Generic(e.to_string()))?;
        temp.flush()
            .map_err(|e| EnhancedError::Generic(e.to_string()))?;

        Ok(temp)
    }

    #[test]
    fn test_merger_new() {
        let merger = PdfMerger::new();
        assert_eq!(merger.page_count(), 0);
    }

    #[test]
    fn test_merger_append_nonexistent() {
        let mut merger = PdfMerger::new();
        assert!(merger.append("/nonexistent/file.pdf").is_err());
    }

    #[test]
    fn test_merger_append_valid() -> Result<()> {
        let temp = create_test_pdf()?;
        let path = temp.path().to_str().unwrap();

        let mut merger = PdfMerger::new();
        merger.append(path)?;
        assert!(merger.page_count() > 0);

        Ok(())
    }

    #[test]
    fn test_merger_append_multiple() -> Result<()> {
        let temp1 = create_test_pdf()?;
        let temp2 = create_test_pdf()?;

        let mut merger = PdfMerger::new();
        merger.append(temp1.path().to_str().unwrap())?;
        merger.append(temp2.path().to_str().unwrap())?;

        assert!(merger.page_count() >= 2);
        Ok(())
    }

    #[test]
    fn test_merger_save_no_pages() {
        let merger = PdfMerger::new();
        let temp = NamedTempFile::new().unwrap();
        assert!(merger.save(temp.path().to_str().unwrap()).is_err());
    }

    #[test]
    fn test_merger_save_with_pages() -> Result<()> {
        let temp_input = create_test_pdf()?;
        let temp_output =
            NamedTempFile::new().map_err(|e| EnhancedError::Generic(e.to_string()))?;

        let mut merger = PdfMerger::new();
        merger.append(temp_input.path().to_str().unwrap())?;
        merger.save(temp_output.path().to_str().unwrap())?;

        // Verify output is a valid PDF
        let data = fs::read(temp_output.path())?;
        assert!(data.starts_with(b"%PDF-"));

        Ok(())
    }

    #[test]
    fn test_split_pdf_nonexistent() {
        assert!(split_pdf("/nonexistent/file.pdf", "/tmp/output").is_err());
    }

    #[test]
    fn test_split_pdf_valid() -> Result<()> {
        let temp_input = create_test_pdf()?;
        let temp_dir = TempDir::new().map_err(|e| EnhancedError::Generic(e.to_string()))?;

        let files = split_pdf(
            temp_input.path().to_str().unwrap(),
            temp_dir.path().to_str().unwrap(),
        )?;

        assert!(!files.is_empty());
        assert!(Path::new(&files[0]).exists());

        Ok(())
    }

    #[test]
    fn test_crop_page_invalid_box() -> Result<()> {
        let temp_input = create_test_pdf()?;
        let temp_output =
            NamedTempFile::new().map_err(|e| EnhancedError::Generic(e.to_string()))?;

        // Invalid crop box (x1 <= x0)
        let crop_box = Rect {
            x0: 100.0,
            y0: 100.0,
            x1: 100.0,
            y1: 200.0,
        };

        let result = crop_page(
            temp_input.path().to_str().unwrap(),
            0,
            crop_box,
            temp_output.path().to_str().unwrap(),
        );

        assert!(result.is_err());
        Ok(())
    }

    #[test]
    fn test_crop_page_valid() -> Result<()> {
        let temp_input = create_test_pdf()?;
        let temp_output =
            NamedTempFile::new().map_err(|e| EnhancedError::Generic(e.to_string()))?;

        let crop_box = Rect {
            x0: 0.0,
            y0: 0.0,
            x1: 400.0,
            y1: 600.0,
        };

        crop_page(
            temp_input.path().to_str().unwrap(),
            0,
            crop_box,
            temp_output.path().to_str().unwrap(),
        )?;

        assert!(temp_output.path().exists());
        Ok(())
    }

    #[test]
    fn test_rotate_pages_invalid_rotation() -> Result<()> {
        let temp_input = create_test_pdf()?;
        let temp_output =
            NamedTempFile::new().map_err(|e| EnhancedError::Generic(e.to_string()))?;

        let result = rotate_pages(
            temp_input.path().to_str().unwrap(),
            45, // Invalid: not multiple of 90
            temp_output.path().to_str().unwrap(),
        );

        assert!(result.is_err());
        Ok(())
    }

    #[test]
    fn test_rotate_pages_valid() -> Result<()> {
        let temp_input = create_test_pdf()?;
        let temp_output =
            NamedTempFile::new().map_err(|e| EnhancedError::Generic(e.to_string()))?;

        rotate_pages(
            temp_input.path().to_str().unwrap(),
            90,
            temp_output.path().to_str().unwrap(),
        )?;

        assert!(temp_output.path().exists());
        Ok(())
    }

    #[test]
    fn test_reorder_pages_basic() {
        use std::fs;
        let test_dir = "/tmp/micropdf_reorder_test";
        let _ = fs::create_dir_all(test_dir);
        let input_path = format!("{}/input.pdf", test_dir);
        let output_path = format!("{}/reordered.pdf", test_dir);

        // Create a 3-page test PDF
        let mut writer = super::super::writer::PdfWriter::new();
        writer.add_blank_page(612.0, 792.0).unwrap();
        writer.add_blank_page(612.0, 792.0).unwrap();
        writer.add_blank_page(612.0, 792.0).unwrap();
        writer.save(&input_path).unwrap();

        // Reorder: reverse [2, 1, 0]
        let result = reorder_pages(&input_path, &[2, 1, 0], &output_path);
        assert!(result.is_ok(), "reorder failed: {:?}", result.err());

        let data = fs::read(&output_path).unwrap();
        assert!(data.starts_with(b"%PDF-"));

        let _ = fs::remove_dir_all(test_dir);
    }

    #[test]
    fn test_reorder_pages_invalid_index() {
        use std::fs;
        let test_dir = "/tmp/micropdf_reorder_invalid_test";
        let _ = fs::create_dir_all(test_dir);
        let input_path = format!("{}/input.pdf", test_dir);
        let output_path = format!("{}/reordered.pdf", test_dir);

        let mut writer = super::super::writer::PdfWriter::new();
        writer.add_blank_page(612.0, 792.0).unwrap();
        writer.save(&input_path).unwrap();

        // Index 5 is out of range for a 1-page PDF
        let result = reorder_pages(&input_path, &[5], &output_path);
        assert!(result.is_err());

        let _ = fs::remove_dir_all(test_dir);
    }

    #[test]
    fn test_merge_pdf_simple() -> Result<()> {
        let temp1 = create_test_pdf()?;
        let temp2 = create_test_pdf()?;
        let temp_output =
            NamedTempFile::new().map_err(|e| EnhancedError::Generic(e.to_string()))?;

        let inputs = vec![
            temp1.path().to_str().unwrap().to_string(),
            temp2.path().to_str().unwrap().to_string(),
        ];

        merge_pdf(&inputs, temp_output.path().to_str().unwrap())?;

        // Verify output exists and is a valid PDF
        let data = fs::read(temp_output.path())?;
        assert!(data.starts_with(b"%PDF-"));

        Ok(())
    }

    #[test]
    fn test_merge_pdf_empty() {
        let temp_output = NamedTempFile::new().unwrap();
        let result = merge_pdf(&[], temp_output.path().to_str().unwrap());
        assert!(result.is_err());
    }

    #[test]
    fn test_merge_pdf_invalid_input() {
        let temp_output = NamedTempFile::new().unwrap();
        let result = merge_pdf(
            &["/nonexistent/file.pdf".to_string()],
            temp_output.path().to_str().unwrap(),
        );
        assert!(result.is_err());
    }
}
