use std::collections::BTreeSet;
use std::fs::File;
use std::io::Write;
use std::path::Path;

use chrono::Utc;
use quick_xml::escape::escape;
use zip::CompressionMethod;
use zip::write::{SimpleFileOptions, ZipWriter};

use crate::error::{EbookmError, Result};
use crate::pipeline::BuiltEntry;

pub fn write_epub(
    manifest: &crate::manifest::Manifest,
    built: &[BuiltEntry],
    output_path: &Path,
    cover_bytes: Option<(String, Vec<u8>)>,
) -> Result<()> {
    if let Some(parent) = output_path.parent() {
        std::fs::create_dir_all(parent).map_err(|source| EbookmError::Io {
            path: parent.display().to_string(),
            source,
        })?;
    }

    let file = File::create(output_path).map_err(|source| EbookmError::Io {
        path: output_path.display().to_string(),
        source,
    })?;
    let mut zip = ZipWriter::new(file);

    let stored = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
    zip.start_file("mimetype", stored)
        .map_err(|error| EbookmError::Epub {
            message: error.to_string(),
        })?;
    zip.write_all(b"application/epub+zip")
        .map_err(|error| EbookmError::Epub {
            message: error.to_string(),
        })?;

    let deflated = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
    write_file(&mut zip, "META-INF/container.xml", deflated, CONTAINER_XML)?;
    write_file(&mut zip, "OEBPS/styles/book.css", deflated, DEFAULT_STYLES)?;

    let nav = build_nav(manifest, built);
    let ncx = build_ncx(manifest, built);
    let opf = build_opf(
        manifest,
        built,
        cover_bytes.as_ref().map(|(href, _)| href.as_str()),
    );

    write_file(&mut zip, "OEBPS/nav.xhtml", deflated, &nav)?;
    write_file(&mut zip, "OEBPS/toc.ncx", deflated, &ncx)?;
    write_file(&mut zip, "OEBPS/content.opf", deflated, &opf)?;

    if let Some((href, bytes)) = cover_bytes {
        write_bytes(&mut zip, &format!("OEBPS/{href}"), deflated, &bytes)?;
    }

    let mut seen_assets = BTreeSet::new();
    for entry in built {
        write_file(
            &mut zip,
            &format!("OEBPS/text/{}.xhtml", entry.id),
            deflated,
            &entry.chapter.xhtml,
        )?;
        for asset in &entry.assets {
            if seen_assets.insert(asset.href.clone()) {
                write_bytes(
                    &mut zip,
                    &format!("OEBPS/{}", asset.href),
                    deflated,
                    &asset.bytes,
                )?;
            }
        }
    }

    zip.finish().map_err(|error| EbookmError::Epub {
        message: error.to_string(),
    })?;
    Ok(())
}

fn write_file(
    zip: &mut ZipWriter<File>,
    path: &str,
    options: SimpleFileOptions,
    contents: &str,
) -> Result<()> {
    write_bytes(zip, path, options, contents.as_bytes())
}

fn write_bytes(
    zip: &mut ZipWriter<File>,
    path: &str,
    options: SimpleFileOptions,
    contents: &[u8],
) -> Result<()> {
    zip.start_file(path, options)
        .map_err(|error| EbookmError::Epub {
            message: error.to_string(),
        })?;
    zip.write_all(contents).map_err(|error| EbookmError::Epub {
        message: error.to_string(),
    })?;
    Ok(())
}

fn build_nav(manifest: &crate::manifest::Manifest, built: &[BuiltEntry]) -> String {
    let mut nav_points = String::new();
    for section in &manifest.sections {
        let section_target = section
            .entries
            .iter()
            .find_map(|entry_id| built.iter().find(|candidate| &candidate.id == entry_id))
            .map(|entry| format!("text/{}.xhtml", entry.id));
        nav_points.push_str("<li>");
        if let Some(target) = section_target {
            nav_points.push_str(&format!(
                "<a href=\"{}\">{}</a><ol>",
                escape(&target),
                escape(&section.title)
            ));
        } else {
            nav_points.push_str(&format!("<span>{}</span><ol>", escape(&section.title)));
        }
        for entry_id in &section.entries {
            if let Some(entry) = built.iter().find(|candidate| &candidate.id == entry_id) {
                if entry.hidden_from_toc {
                    continue;
                }
                nav_points.push_str(&format!(
                    "<li><a href=\"text/{}.xhtml\">{}</a></li>",
                    escape(&entry.id),
                    escape(&entry.chapter.nav_title)
                ));
            }
        }
        nav_points.push_str("</ol></li>");
    }

    format!(
        r#"<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
  <head>
    <title>{}</title>
    <link rel="stylesheet" type="text/css" href="styles/book.css"/>
  </head>
  <body>
    <nav epub:type="toc" id="toc">
      <h1>{}</h1>
      <ol>{}</ol>
    </nav>
  </body>
</html>"#,
        escape(&manifest.book.title),
        escape(&manifest.book.title),
        nav_points
    )
}

fn build_ncx(manifest: &crate::manifest::Manifest, built: &[BuiltEntry]) -> String {
    let mut play_order = 1usize;
    let mut nav_points = String::new();
    for section in &manifest.sections {
        let section_entries: Vec<_> = section
            .entries
            .iter()
            .filter_map(|entry_id| built.iter().find(|candidate| &candidate.id == entry_id))
            .filter(|entry| !entry.hidden_from_toc)
            .collect();

        if section_entries.is_empty() {
            continue;
        }

        let section_play_order = play_order;
        play_order += 1;

        let section_target = section_entries[0]
            .section_anchor
            .as_ref()
            .map(|anchor| format!("text/{}.xhtml#{}", section_entries[0].id, anchor))
            .unwrap_or_else(|| format!("text/{}.xhtml", section_entries[0].id));
        let mut child_points = String::new();
        for entry in &section_entries {
            child_points.push_str(&format!(
                "<navPoint id=\"nav-{}\" playOrder=\"{}\"><navLabel><text>{}</text></navLabel><content src=\"text/{}.xhtml\"/></navPoint>",
                escape(&xml_id("nav", &entry.id)),
                play_order,
                escape(&entry.chapter.nav_title),
                escape(&entry.id)
            ));
            play_order += 1;
        }

        nav_points.push_str(&format!(
            "<navPoint id=\"{}\" playOrder=\"{}\"><navLabel><text>{}</text></navLabel><content src=\"{}\"/>{}</navPoint>",
            escape(&xml_id("section", &section.id)),
            section_play_order,
            escape(&section.title),
            escape(&section_target),
            child_points
        ));
    }

    format!(
        r#"<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
  <head>
    <meta name="dtb:uid" content="{}"/>
  </head>
  <docTitle><text>{}</text></docTitle>
  <navMap>{}</navMap>
</ncx>"#,
        escape(&manifest.book.identifier),
        escape(&manifest.book.title),
        nav_points
    )
}

fn build_opf(
    manifest: &crate::manifest::Manifest,
    built: &[BuiltEntry],
    cover_href: Option<&str>,
) -> String {
    let mut manifest_items = String::from(
        r#"<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
<item id="css" href="styles/book.css" media-type="text/css"/>"#,
    );
    let mut spine_items = String::new();

    for entry in built {
        manifest_items.push_str(&format!(
            "<item id=\"{}\" href=\"text/{}.xhtml\" media-type=\"application/xhtml+xml\"/>",
            escape(&xml_id("entry", &entry.id)),
            escape(&entry.id)
        ));
        spine_items.push_str(&format!(
            "<itemref idref=\"{}\"/>",
            escape(&xml_id("entry", &entry.id))
        ));
        for asset in &entry.assets {
            manifest_items.push_str(&format!(
                "<item id=\"{}\" href=\"{}\" media-type=\"{}\"/>",
                escape(&xml_id("asset", &asset.id)),
                escape(&asset.href),
                escape(&asset.media_type)
            ));
        }
    }

    if let Some(cover_href) = cover_href {
        manifest_items.push_str(&format!(
            "<item id=\"cover\" href=\"{}\" media-type=\"{}\" properties=\"cover-image\"/>",
            escape(cover_href),
            escape(&media_type_from_href(cover_href))
        ));
    }

    let author = manifest
        .book
        .author
        .clone()
        .unwrap_or_else(|| "Unknown".to_string());
    let description = manifest.book.description.clone().unwrap_or_default();
    let modified = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
    format!(
        r#"<?xml version="1.0" encoding="UTF-8"?>
<package version="3.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:identifier id="bookid">{}</dc:identifier>
    <dc:title>{}</dc:title>
    <dc:creator>{}</dc:creator>
    <dc:language>{}</dc:language>
    <dc:description>{}</dc:description>
    <meta property="dcterms:modified">{}</meta>
  </metadata>
  <manifest>{}</manifest>
  <spine toc="ncx">{}</spine>
</package>"#,
        escape(&manifest.book.identifier),
        escape(&manifest.book.title),
        escape(&author),
        escape(&manifest.book.language),
        escape(&description),
        escape(&modified),
        manifest_items,
        spine_items
    )
}

fn xml_id(prefix: &str, value: &str) -> String {
    let mut id = String::with_capacity(prefix.len() + value.len() + 1);
    id.push_str(prefix);
    id.push('-');
    for ch in value.chars() {
        if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.') {
            id.push(ch);
        } else {
            id.push('-');
        }
    }
    id
}

fn media_type_from_href(href: &str) -> String {
    match href.rsplit('.').next().map(|ext| ext.to_ascii_lowercase()) {
        Some(extension) => match extension.as_str() {
            "jpg" | "jpeg" => "image/jpeg",
            "png" => "image/png",
            "gif" => "image/gif",
            "svg" => "image/svg+xml",
            "webp" => "image/webp",
            "avif" => "image/avif",
            _ => "application/octet-stream",
        }
        .to_string(),
        None => "application/octet-stream".to_string(),
    }
}

const CONTAINER_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
  </rootfiles>
</container>"#;

const DEFAULT_STYLES: &str = r#"body { font-family: serif; line-height: 1.5; margin: 5%; }
h1 { margin-bottom: 0.2em; }
.chapter-meta { color: #555; font-size: 0.9em; margin-bottom: 1.5em; }
img { max-width: 100%; height: auto; }
a { color: #0b4f7a; text-decoration: none; }
"#;
