//! Attachments — list, add, extract, remove file attachments.
//!
//! Zero `unsafe` in this file.

use crate::cpdf::bookmarks::rebuild_xref_data;
use crate::cpdf::document::CpdfDocument;
use crate::cpdf::error::{CpdfError, Result};
use serde::Serialize;

#[derive(Debug, Clone, Serialize)]
pub struct AttachmentInfo {
    pub name: String,
    pub page: Option<usize>,
    pub size: usize,
}

/// List all attachments in the document.
pub fn list_attachments(doc: &CpdfDocument) -> Result<Vec<AttachmentInfo>> {
    let bytes = doc.to_bytes();
    let text = String::from_utf8_lossy(&bytes);
    let mut attachments = Vec::new();

    if let Some(names_idx) = text.find("/EmbeddedFiles") {
        let after = &text[names_idx..];
        if let Some(arr_start) = after.find('[') {
            let arr_content = &after[arr_start + 1..];
            if let Some(arr_end) = arr_content.find(']') {
                let arr = &arr_content[..arr_end];
                let mut pos = 0;
                while pos < arr.len() {
                    let remaining = arr[pos..].trim_start();
                    if remaining.is_empty() {
                        break;
                    }

                    if remaining.starts_with('(') {
                        if let Some(close) = remaining[1..].find(')') {
                            let name = remaining[1..1 + close].to_string();
                            attachments.push(AttachmentInfo {
                                name,
                                page: None,
                                size: 0,
                            });
                            pos += (arr.len() - pos) - remaining.len() + close + 2;
                            continue;
                        }
                    }
                    pos += 1;
                }
            }
        }
    }

    Ok(attachments)
}

/// List attachments as JSON.
pub fn list_attachments_json(doc: &CpdfDocument) -> Result<String> {
    let attachments = list_attachments(doc)?;
    serde_json::to_string_pretty(&attachments).map_err(|e| CpdfError::Parse(e.to_string()))
}

/// Attach a file to the document.
pub fn attach_file(doc: CpdfDocument, path: &str, _page: Option<usize>) -> Result<CpdfDocument> {
    let file_data = std::fs::read(path)?;

    let file_name = std::path::Path::new(path)
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("attachment");

    let bytes = doc.to_bytes();
    let mut text = String::from_utf8_lossy(&bytes).to_string();

    let next_num = {
        let mut max = 0usize;
        let mut pos = 0;
        while let Some(idx) = text[pos..].find(" 0 obj") {
            let abs = pos + idx;
            let start = text[..abs].rfind(|c: char| !c.is_ascii_digit() && c != ' ');
            let num_str = match start {
                Some(s) => text[s + 1..abs].trim(),
                None => text[..abs].trim(),
            };
            if let Ok(n) = num_str.parse::<usize>() {
                if n > max {
                    max = n;
                }
            }
            pos = abs + 6;
        }
        max + 1
    };

    let stream_obj = format!(
        "{next_num} 0 obj\n<< /Type /EmbeddedFile /Length {} >>\nstream\n",
        file_data.len()
    );
    let filespec_num = next_num + 1;
    let filespec_obj = format!(
        "{filespec_num} 0 obj\n<< /Type /Filespec /F ({file_name}) /EF << /F {next_num} 0 R >> >>\nendobj\n\n"
    );

    let insert_pos = text
        .rfind("xref")
        .or_else(|| text.rfind("startxref"))
        .unwrap_or(text.len());
    let mut insertion = stream_obj;
    insertion.push_str(&String::from_utf8_lossy(&file_data));
    insertion.push_str("\nendstream\nendobj\n\n");
    insertion.push_str(&filespec_obj);
    text.insert_str(insert_pos, &insertion);

    if text.contains("/EmbeddedFiles") {
        if let Some(arr_idx) = text.find("/EmbeddedFiles") {
            let after = &text[arr_idx..];
            if let Some(bracket) = after.find('[') {
                let insert = arr_idx + bracket + 1;
                text.insert_str(insert, &format!(" ({file_name}) {filespec_num} 0 R"));
            }
        }
    } else if let Some(cat_idx) = text
        .find("/Type /Catalog")
        .or_else(|| text.find("/Type/Catalog"))
    {
        let dict_end = text[cat_idx..].find(">>").unwrap_or(0) + cat_idx;
        text.insert_str(
            dict_end,
            &format!(
                " /Names << /EmbeddedFiles << /Names [ ({file_name}) {filespec_num} 0 R ] >> >>"
            ),
        );
    }

    let final_bytes = rebuild_xref_data(text.as_bytes());
    CpdfDocument::from_bytes(final_bytes)
}

/// Remove all attachments.
pub fn remove_attachments(doc: CpdfDocument) -> Result<CpdfDocument> {
    let bytes = doc.to_bytes();
    let mut text = String::from_utf8_lossy(&bytes).to_string();

    if let Some(idx) = text.find("/EmbeddedFiles") {
        let after = &text[idx..];
        let mut depth = 0i32;
        let mut end = 0;
        let chars: Vec<char> = after.chars().collect();
        let mut i = 0;
        while i < chars.len().saturating_sub(1) {
            if chars[i] == '<' && chars[i + 1] == '<' {
                depth += 1;
                i += 2;
            } else if chars[i] == '>' && chars[i + 1] == '>' {
                depth -= 1;
                if depth == 0 {
                    end = i + 2;
                    break;
                }
                i += 2;
            } else {
                i += 1;
            }
        }
        if end > 0 {
            text = format!("{}{}", &text[..idx], &text[idx + end..]);
        }
    }

    let final_bytes = rebuild_xref_data(text.as_bytes());
    CpdfDocument::from_bytes(final_bytes)
}

/// Extract attachments to a directory.
pub fn extract_attachments(doc: &CpdfDocument, _output_dir: &str) -> Result<()> {
    Err(CpdfError::Unsupported(
        "extract_attachments not yet supported".into(),
    ))
}
