import sys
import os
import zipfile
import xml.etree.ElementTree as ET
from bs4 import BeautifulSoup
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm

def get_opf_path(zip_path):
    """Read META-INF/container.xml to find the root OPF file."""
    with zipfile.ZipFile(zip_path, 'r') as zf:
        try:
            container = zf.read('META-INF/container.xml')
        except KeyError:
            raise Exception("No META-INF/container.xml found – not a valid EPUB.")
        root = ET.fromstring(container)
        # Find the first <rootfile> element
        ns = {'ns': 'urn:oasis:names:tc:opendocument:xmlns:container'}
        rootfile = root.find('.//ns:rootfile', ns)
        if rootfile is None:
            # Try without namespace (some EPUBs omit it)
            rootfile = root.find('.//rootfile')
        if rootfile is None:
            raise Exception("No rootfile found in container.xml")
        opf_path = rootfile.get('full-path')
        if not opf_path:
            raise Exception("No full-path attribute in rootfile")
        return opf_path

def parse_opf(zip_path, opf_path):
    """Parse the OPF file and return manifest dict and spine list."""
    with zipfile.ZipFile(zip_path, 'r') as zf:
        opf_content = zf.read(opf_path)
    root = ET.fromstring(opf_content)

    # Namespace handling – EPUB uses the OPF namespace
    ns = {'opf': 'http://www.idpf.org/2007/opf'}
    # Also handle no namespace
    def find_tag(elem, tag):
        return elem.find(tag, ns) or elem.find(tag)

    # Build manifest: id -> href
    manifest = {}
    for item in root.findall('.//opf:manifest/opf:item', ns) + root.findall('.//manifest/item'):
        item_id = item.get('id')
        href = item.get('href')
        media_type = item.get('media-type')
        if item_id and href:
            # resolve relative path against opf_path
            base_dir = os.path.dirname(opf_path)
            if base_dir:
                full_href = os.path.join(base_dir, href).replace('\\', '/')
            else:
                full_href = href
            manifest[item_id] = full_href

    # Get spine order (itemrefs)
    spine_items = []
    for itemref in root.findall('.//opf:spine/opf:itemref', ns) + root.findall('.//spine/itemref'):
        ref_id = itemref.get('idref')
        if ref_id and ref_id in manifest:
            spine_items.append(manifest[ref_id])
    return spine_items

def convert_epub_to_pdf(epub_path, pdf_path):
    # Ensure output has .pdf extension
    if not pdf_path.lower().endswith('.pdf'):
        pdf_path += '.pdf'

    # 1. Locate OPF
    opf_path = get_opf_path(epub_path)
    print(f"Found OPF: {opf_path}")

    # 2. Get spine order
    spine_files = parse_opf(epub_path, opf_path)
    if not spine_files:
        # Fallback: collect all .html/.xhtml files from the zip
        print("No spine found – falling back to all HTML files.")
        with zipfile.ZipFile(epub_path, 'r') as zf:
            spine_files = [name for name in zf.namelist()
                           if name.endswith(('.html', '.xhtml', '.htm'))]
            spine_files.sort()  # alphabetical order

    print(f"Processing {len(spine_files)} content files.")

    # 3. Prepare PDF
    page_width, page_height = A4
    c = canvas.Canvas(pdf_path, pagesize=A4)

    left_margin = 20 * mm
    right_margin = 20 * mm
    top_margin = 20 * mm
    bottom_margin = 20 * mm
    max_width = page_width - left_margin - right_margin
    y = page_height - top_margin
    font_size = 12
    leading = 14
    c.setFont("Helvetica", font_size)

    # 4. Process each spine file
    with zipfile.ZipFile(epub_path, 'r') as zf:
        for file_path in spine_files:
            try:
                content = zf.read(file_path).decode('utf-8', errors='ignore')
            except KeyError:
                print(f"Warning: {file_path} not found in archive, skipping.")
                continue

            soup = BeautifulSoup(content, 'html.parser')
            for script in soup(["script", "style"]):
                script.decompose()
            text = soup.get_text()
            lines = [line.strip() for line in text.splitlines() if line.strip()]

            for para in lines:
                words = para.split()
                line = ""
                for word in words:
                    test_line = line + " " + word if line else word
                    if c.stringWidth(test_line, "Helvetica", font_size) <= max_width:
                        line = test_line
                    else:
                        if y < bottom_margin + leading:
                            c.showPage()
                            c.setFont("Helvetica", font_size)
                            y = page_height - top_margin
                        c.drawString(left_margin, y, line)
                        y -= leading
                        line = word
                if line:
                    if y < bottom_margin + leading:
                        c.showPage()
                        c.setFont("Helvetica", font_size)
                        y = page_height - top_margin
                    c.drawString(left_margin, y, line)
                    y -= leading
                y -= leading  # blank line between paragraphs

    c.save()
    print(f"✅ PDF saved to: {pdf_path}")

if __name__ == "__main__":
    if len(sys.argv) >= 3:
        epub_input = sys.argv[1]
        pdf_output = sys.argv[2]
    else:
        epub_input = input("Enter EPUB path: ").strip()
        pdf_output = input("Enter PDF output path: ").strip()

    if not os.path.exists(epub_input):
        print(f"❌ Error: File '{epub_input}' not found.")
        sys.exit(1)

    convert_epub_to_pdf(epub_input, pdf_output)
