//! QPDFJob equivalent - declarative PDF transformation system
//!
//! Provides a builder-pattern API for common PDF operations: page selection,
//! rotation, encryption, linearization, overlay/underlay, and more.

use super::document::Document;
use super::error::{QpdfError, Result};
use super::writer::{ObjectStreamMode, PdfWriter};

// ============================================================================
// PageRange - parse page specs like "1-5,7,9-end"
// ============================================================================

/// Page range helper for parsing page selection specs
pub struct PageRange;

impl PageRange {
    /// Parse page selection spec into 0-based page indices.
    ///
    /// Supports:
    /// - Single pages: `1`, `7`
    /// - Ranges: `1-5`, `9-end`
    /// - Comma-separated: `1-5,7,9-end`
    ///
    /// Page numbers are 1-based in the spec; returned indices are 0-based.
    pub fn parse(spec: &str, total_pages: usize) -> Result<Vec<usize>> {
        parse_page_range_impl(spec, total_pages)
    }
}

/// Parse page selection spec into 0-based page indices.
///
/// Supports:
/// - Single pages: `1`, `7`
/// - Ranges: `1-5`, `9-end`
/// - Comma-separated: `1-5,7,9-end`
///
/// Page numbers are 1-based in the spec; returned indices are 0-based.
pub fn parse_page_range(spec: &str, total_pages: usize) -> Result<Vec<usize>> {
    parse_page_range_impl(spec, total_pages)
}

fn parse_page_range_impl(spec: &str, total_pages: usize) -> Result<Vec<usize>> {
    if total_pages == 0 {
        return Ok(Vec::new());
    }
    let mut indices = Vec::new();
    for part in spec.split(',') {
        let part = part.trim();
        if part.is_empty() {
            continue;
        }
        if part.eq_ignore_ascii_case("end") {
            indices.push(total_pages - 1);
            continue;
        }
        if let Some((lo_str, hi_str)) = part.split_once('-') {
            let lo_str = lo_str.trim();
            let hi_str = hi_str.trim();
            let lo: usize = lo_str.parse().map_err(|_| {
                QpdfError::InvalidOperation(format!("Invalid page number: {}", lo_str))
            })?;
            let hi = if hi_str.eq_ignore_ascii_case("end") {
                total_pages
            } else {
                hi_str.parse::<usize>().map_err(|_| {
                    QpdfError::InvalidOperation(format!("Invalid page number: {}", hi_str))
                })?
            };
            if lo < 1 || hi < lo {
                return Err(QpdfError::InvalidOperation(format!(
                    "Invalid page range: {} (total pages: {})",
                    part, total_pages
                )));
            }
            let lo_idx = (lo - 1).min(total_pages);
            let hi_idx = hi.min(total_pages);
            for i in lo_idx..hi_idx {
                indices.push(i);
            }
        } else {
            let n: usize = part.parse().map_err(|_| {
                QpdfError::InvalidOperation(format!("Invalid page number: {}", part))
            })?;
            if n < 1 || n > total_pages {
                return Err(QpdfError::InvalidOperation(format!(
                    "Page {} out of range (1-{})",
                    n, total_pages
                )));
            }
            indices.push(n - 1);
        }
    }
    indices.sort_unstable();
    indices.dedup();
    Ok(indices)
}

// ============================================================================
// PageLabelSpec
// ============================================================================

/// Page label specification (PDF 1.3+)
#[derive(Debug, Clone)]
pub struct PageLabelSpec {
    /// 1-based page index where this label range starts
    pub start_page: usize,
    /// Numbering style: "D" decimal, "R" roman upper, "r" roman lower, "A" alpha upper, "a" alpha lower
    pub style: Option<String>,
    /// Optional prefix (e.g. "Appendix ")
    pub prefix: Option<String>,
    /// Starting number for this range
    pub start_num: Option<usize>,
}

// ============================================================================
// EncryptionJobConfig
// ============================================================================

/// Encryption configuration for job
#[derive(Debug, Clone)]
pub struct EncryptionJobConfig {
    pub user_password: String,
    pub owner_password: String,
    pub key_length: i32,
}

impl Default for EncryptionJobConfig {
    fn default() -> Self {
        Self {
            user_password: String::new(),
            owner_password: String::new(),
            key_length: 128,
        }
    }
}

// ============================================================================
// JobResult
// ============================================================================

/// Result of running a PDF job
#[derive(Debug, Clone)]
pub struct JobResult {
    pub output_data: Option<Vec<u8>>,
    pub warnings: Vec<String>,
    pub pages_processed: usize,
}

// ============================================================================
// PdfJob
// ============================================================================

/// Declarative PDF transformation job (QPDFJob equivalent)
#[derive(Debug)]
pub struct PdfJob {
    input_file: Option<String>,
    input_data: Option<Vec<u8>>,
    output_file: Option<String>,
    pages_spec: Option<String>,
    rotate_angle: Option<i32>,
    rotate_pages: Option<String>,
    encryption: Option<EncryptionJobConfig>,
    decrypt: bool,
    linearize: bool,
    object_streams: Option<ObjectStreamMode>,
    compress_streams: bool,
    decompress_streams: bool,
    flatten_annotations: bool,
    remove_unreferenced: bool,
    page_labels: Vec<PageLabelSpec>,
    split_pages: Option<Option<usize>>,
    overlay_path: Option<String>,
    underlay_path: Option<String>,
}

impl PdfJob {
    pub fn new() -> Self {
        Self {
            input_file: None,
            input_data: None,
            output_file: None,
            pages_spec: None,
            rotate_angle: None,
            rotate_pages: None,
            encryption: None,
            decrypt: false,
            linearize: false,
            object_streams: None,
            compress_streams: false,
            decompress_streams: false,
            flatten_annotations: false,
            remove_unreferenced: false,
            page_labels: Vec::new(),
            split_pages: None,
            overlay_path: None,
            underlay_path: None,
        }
    }

    pub fn input_file(mut self, path: &str) -> Self {
        self.input_file = Some(path.to_string());
        self
    }

    pub fn input_data(mut self, data: Vec<u8>) -> Self {
        self.input_data = Some(data);
        self
    }

    pub fn output_file(mut self, path: &str) -> Self {
        self.output_file = Some(path.to_string());
        self
    }

    pub fn pages(mut self, spec: &str) -> Self {
        self.pages_spec = Some(spec.to_string());
        self
    }

    pub fn rotate(mut self, angle: i32, pages: Option<&str>) -> Self {
        self.rotate_angle = Some(angle);
        self.rotate_pages = pages.map(String::from);
        self
    }

    pub fn encrypt(mut self, config: EncryptionJobConfig) -> Self {
        self.encryption = Some(config);
        self
    }

    pub fn decrypt(mut self) -> Self {
        self.decrypt = true;
        self
    }

    pub fn linearize(mut self) -> Self {
        self.linearize = true;
        self
    }

    pub fn object_streams(mut self, mode: ObjectStreamMode) -> Self {
        self.object_streams = Some(mode);
        self
    }

    pub fn compress_streams(mut self) -> Self {
        self.compress_streams = true;
        self
    }

    pub fn decompress_streams(mut self) -> Self {
        self.decompress_streams = true;
        self
    }

    pub fn flatten_annotations(mut self) -> Self {
        self.flatten_annotations = true;
        self
    }

    pub fn remove_unreferenced(mut self) -> Self {
        self.remove_unreferenced = true;
        self
    }

    pub fn set_page_labels(mut self, labels: Vec<PageLabelSpec>) -> Self {
        self.page_labels = labels;
        self
    }

    pub fn split_pages(mut self, group_size: Option<usize>) -> Self {
        self.split_pages = Some(group_size);
        self
    }

    pub fn overlay(mut self, path: &str) -> Self {
        self.overlay_path = Some(path.to_string());
        self
    }

    pub fn underlay(mut self, path: &str) -> Self {
        self.underlay_path = Some(path.to_string());
        self
    }

    /// Execute the job
    pub fn run(self) -> Result<JobResult> {
        let data = self.load_input()?;
        let mut doc = Document::from_bytes(data)?;

        let mut warnings = Vec::new();

        if self.decrypt && doc.is_encrypted() {
            doc.remove_security_restrictions();
        }

        let pages_processed = self.apply_page_selection(&mut doc, &mut warnings)?;

        if self.remove_unreferenced {
            doc.fix_dangling_references();
        }

        let output_data = self.write_output(&doc)?;

        if let Some(path) = &self.output_file {
            if let Some(ref data) = output_data {
                std::fs::write(path, data)?;
            }
        }

        warnings.extend(doc.get_warnings().iter().cloned());

        Ok(JobResult {
            output_data,
            warnings,
            pages_processed,
        })
    }

    fn load_input(&self) -> Result<Vec<u8>> {
        if let Some(ref data) = self.input_data {
            return Ok(data.clone());
        }
        if let Some(ref path) = self.input_file {
            return Ok(std::fs::read(path)?);
        }
        Err(QpdfError::InvalidOperation(
            "No input specified: use input_file() or input_data()".to_string(),
        ))
    }

    fn apply_page_selection(
        &self,
        doc: &mut Document,
        _warnings: &mut Vec<String>,
    ) -> Result<usize> {
        let total = doc.page_count()?;
        if total == 0 {
            return Ok(0);
        }

        let indices = if let Some(ref spec) = self.pages_spec {
            PageRange::parse(spec, total)?
        } else {
            (0..total).collect::<Vec<_>>()
        };

        let page_refs = doc.get_all_pages()?;
        let to_remove: Vec<_> = (0..page_refs.len())
            .filter(|i| !indices.contains(i))
            .map(|i| page_refs[i])
            .collect();

        for page_ref in to_remove.into_iter().rev() {
            doc.remove_page(page_ref)?;
        }

        Ok(indices.len())
    }

    fn write_output(&self, doc: &Document) -> Result<Option<Vec<u8>>> {
        let root_ref = doc
            .get_root_ref()
            .ok_or_else(|| QpdfError::Structure("No root catalog".to_string()))?;

        let mut writer = PdfWriter::new();

        for (obj_ref, obj) in doc.get_all_objects() {
            writer.add_object_with_id(obj_ref, obj.clone());
        }
        writer.set_root(root_ref);

        if let Some(mode) = self.object_streams {
            writer.set_object_stream_mode(mode);
        }

        let data = writer.write_to_memory()?;
        Ok(Some(data))
    }
}

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

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

    #[test]
    fn test_page_range_single() {
        let r = PageRange::parse("1", 10).unwrap();
        assert_eq!(r, vec![0]);
        let r = PageRange::parse("5", 10).unwrap();
        assert_eq!(r, vec![4]);
    }

    #[test]
    fn test_page_range_range() {
        let r = PageRange::parse("1-5", 10).unwrap();
        assert_eq!(r, vec![0, 1, 2, 3, 4]);
        let r = PageRange::parse("3-3", 10).unwrap();
        assert_eq!(r, vec![2]);
    }

    #[test]
    fn test_page_range_end() {
        let r = PageRange::parse("end", 10).unwrap();
        assert_eq!(r, vec![9]);
        let r = PageRange::parse("9-end", 10).unwrap();
        assert_eq!(r, vec![8, 9]);
    }

    #[test]
    fn test_page_range_comma() {
        let r = PageRange::parse("1,3,5", 10).unwrap();
        assert_eq!(r, vec![0, 2, 4]);
        let r = PageRange::parse("1-3,7,9-end", 10).unwrap();
        assert_eq!(r, vec![0, 1, 2, 6, 8, 9]);
    }

    #[test]
    fn test_page_range_dedup() {
        let r = PageRange::parse("1,1,2,2", 10).unwrap();
        assert_eq!(r, vec![0, 1]);
    }

    #[test]
    fn test_page_range_empty_total() {
        let r = PageRange::parse("1-5", 0).unwrap();
        assert!(r.is_empty());
    }

    #[test]
    fn test_page_range_invalid() {
        assert!(PageRange::parse("0", 10).is_err());
        assert!(PageRange::parse("11", 10).is_err());
        assert!(PageRange::parse("5-3", 10).is_err());
        assert!(PageRange::parse("abc", 10).is_err());
    }

    #[test]
    fn test_job_builder() {
        let job = PdfJob::new()
            .input_file("/tmp/test.pdf")
            .output_file("/tmp/out.pdf")
            .pages("1-5")
            .linearize();
        assert!(job.input_file.is_some());
        assert!(job.output_file.is_some());
        assert_eq!(job.pages_spec.as_deref(), Some("1-5"));
        assert!(job.linearize);
    }

    #[test]
    fn test_job_run_minimal() {
        let minimal_pdf = concat!(
            "%PDF-1.4\n",
            "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n",
            "2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n",
            "xref\n0 3\n",
            "0000000000 65535 f \n",
            "0000000009 00000 n \n",
            "0000000058 00000 n \n",
            "trailer\n<< /Size 3 /Root 1 0 R >>\n",
            "startxref\n110\n%%EOF"
        );
        let job = PdfJob::new()
            .input_data(minimal_pdf.as_bytes().to_vec())
            .run();
        assert!(job.is_ok(), "Job failed: {:?}", job.err());
        let result = job.unwrap();
        assert!(result.output_data.is_some());
        assert_eq!(result.pages_processed, 0);
    }

    #[test]
    fn test_job_no_input() {
        let job = PdfJob::new().output_file("/tmp/out.pdf").run();
        assert!(job.is_err());
    }
}
