use std::path::Path;
use crate::grid::Cell;
use crate::puzzle::Puzzle;

pub fn to_pdf(puzzle: &Puzzle, title: &str, variant: &str) -> Vec<u8> {
    // try to load custom fonts, fall back to built-in
    let data_dir = std::env::var("DATA_DIR").unwrap_or_else(|_| "../data".into());
    let font_dir = Path::new(&data_dir).join("fonts");
    let bold_font = std::fs::read(font_dir.join("PlayfairDisplay-Bold.ttf")).ok();
    let regular_font = std::fs::read(font_dir.join("DMSans-Regular.ttf")).ok();

    let show_solution = variant == "solution";
    let pw = 612.0;
    let ph = 792.0;
    let margin = 48.0;

    let mut cell_numbers = std::collections::HashMap::new();
    for word in &puzzle.words {
        cell_numbers.entry((word.row, word.col)).or_insert(word.clue_number);
    }

    let max_grid_h = ph * 0.45;
    let max_grid_w = pw - margin * 2.0;
    let cell = (max_grid_w / puzzle.width as f64)
        .min(max_grid_h / puzzle.height as f64)
        .min(28.0);
    let grid_w = cell * puzzle.width as f64;
    let grid_h = cell * puzzle.height as f64;
    let grid_x = (pw - grid_w) / 2.0;
    let grid_top = ph - margin - 60.0;

    let mut c = String::new();

    // background
    c.push_str(&format!("0.98 0.965 0.95 rg 0 0 {} {} re f\n", pw, ph));

    // title - centered
    let title_fs = 22.0;
    let title_w = title.len() as f64 * title_fs * 0.48;
    c.push_str(&format!(
        "BT /F2 {} Tf 0.1 0.08 0.06 rg {} {} Td ({}) Tj ET\n",
        title_fs, (pw - title_w) / 2.0, grid_top + 30.0, esc(title)
    ));

    // subtitle - centered
    let sub = if show_solution {
        format!("{}x{} - solution", puzzle.width, puzzle.height)
    } else {
        format!("{}x{} crossword", puzzle.width, puzzle.height)
    };
    let sub_w = sub.len() as f64 * 8.0 * 0.45;
    c.push_str(&format!(
        "BT /F1 8 Tf 0.6 0.55 0.5 rg {} {} Td ({}) Tj ET\n",
        (pw - sub_w) / 2.0, grid_top + 14.0, esc(&sub)
    ));

    // grid border
    c.push_str(&format!(
        "0.1 0.08 0.06 RG 1.5 w {} {} {} {} re S\n",
        grid_x, grid_top - grid_h, grid_w, grid_h
    ));

    // cells
    for r in 0..puzzle.height {
        for co in 0..puzzle.width {
            let x = grid_x + co as f64 * cell;
            let y = grid_top - r as f64 * cell;
            match puzzle.grid.get(r, co) {
                Cell::Black => {
                    c.push_str(&format!("0.1 0.08 0.06 rg {} {} {} {} re f\n", x, y - cell, cell, cell));
                }
                _ => {
                    c.push_str(&format!("0.78 0.75 0.72 RG 0.3 w {} {} {} {} re S\n", x, y - cell, cell, cell));
                    if let Some(&num) = cell_numbers.get(&(r, co)) {
                        c.push_str(&format!(
                            "BT /F1 {} Tf 0.4 0.36 0.32 rg {} {} Td ({}) Tj ET\n",
                            (cell * 0.25).min(7.0), x + 1.5, y - (cell * 0.28).min(7.5), num
                        ));
                    }
                    if show_solution {
                        if let Some(ch) = puzzle.grid.get(r, co).letter() {
                            let fs = (cell * 0.55).min(16.0);
                            c.push_str(&format!(
                                "BT /F1 {} Tf 0.1 0.08 0.06 rg {} {} Td ({}) Tj ET\n",
                                fs, x + cell * 0.3, y - cell * 0.68, ch
                            ));
                        }
                    }
                }
            }
        }
    }

    // clues
    let clue_top = grid_top - grid_h - 24.0;
    let col_w = (pw - margin * 2.0 - 24.0) / 2.0;
    let fs = 7.5;
    let lh = 10.5;

    c.push_str(&format!("BT /F2 10 Tf 0.1 0.08 0.06 rg {} {} Td (ACROSS) Tj ET\n", margin, clue_top));
    c.push_str(&format!("0.78 0.75 0.72 RG 0.5 w {} {} m {} {} l S\n", margin, clue_top - 3.0, margin + col_w, clue_top - 3.0));

    let mut y = clue_top - 16.0;
    for clue in &puzzle.clues_across {
        if y < margin + 20.0 { break; }
        c.push_str(&format!("BT /F2 {} Tf 0.79 0.41 0.29 rg {} {} Td ({}) Tj ET\n", fs, margin, y, clue.number));
        c.push_str(&format!("BT /F1 {} Tf 0.35 0.3 0.28 rg {} {} Td ({}) Tj ET\n", fs, margin + 18.0, y, esc(&truncate(&clue.text, 55))));
        y -= lh;
    }

    let dx = margin + col_w + 24.0;
    c.push_str(&format!("BT /F2 10 Tf 0.1 0.08 0.06 rg {} {} Td (DOWN) Tj ET\n", dx, clue_top));
    c.push_str(&format!("0.78 0.75 0.72 RG 0.5 w {} {} m {} {} l S\n", dx, clue_top - 3.0, dx + col_w, clue_top - 3.0));

    y = clue_top - 16.0;
    for clue in &puzzle.clues_down {
        if y < margin + 20.0 { break; }
        c.push_str(&format!("BT /F2 {} Tf 0.79 0.41 0.29 rg {} {} Td ({}) Tj ET\n", fs, dx, y, clue.number));
        c.push_str(&format!("BT /F1 {} Tf 0.35 0.3 0.28 rg {} {} Td ({}) Tj ET\n", fs, dx + 18.0, y, esc(&truncate(&clue.text, 55))));
        y -= lh;
    }

    c.push_str(&format!("BT /F2 6 Tf 0.6 0.55 0.5 rg {} {} Td (cruciverb) Tj ET\n", margin, margin - 10.0));

    build_pdf_with_fonts(&c, pw, ph, bold_font.as_deref(), regular_font.as_deref())
}

fn esc(s: &str) -> String {
    s.replace('\\', "\\\\").replace('(', "\\(").replace(')', "\\)")
}

fn truncate(s: &str, max: usize) -> String {
    if s.len() <= max { s.to_string() } else { format!("{}...", &s[..max - 3]) }
}

fn build_pdf_with_fonts(content: &str, pw: f64, ph: f64, bold_ttf: Option<&[u8]>, regular_ttf: Option<&[u8]>) -> Vec<u8> {
    let mut pdf = Vec::new();
    let mut offsets = Vec::new();
    let mut obj_count = 0;

    pdf.extend_from_slice(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n");

    macro_rules! obj {
        ($data:expr) => {{
            obj_count += 1;
            offsets.push(pdf.len());
            pdf.extend_from_slice(format!("{} 0 obj\n", obj_count).as_bytes());
            pdf.extend_from_slice($data);
            pdf.extend_from_slice(b"\nendobj\n");
            obj_count
        }};
    }

    // obj 1: catalog
    obj!(b"<< /Type /Catalog /Pages 2 0 R >>");

    // obj 2: pages
    obj!(b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>");

    // build font objects
    let (f1_ref, f2_ref);

    if let Some(reg_data) = regular_ttf {
        // embedded TrueType font for F1 (regular/body)
        let stream_id = {
            obj_count += 1;
            offsets.push(pdf.len());
            pdf.extend_from_slice(format!("{} 0 obj\n", obj_count).as_bytes());
            pdf.extend_from_slice(format!("<< /Length {} /Length1 {} >>\nstream\n", reg_data.len(), reg_data.len()).as_bytes());
            pdf.extend_from_slice(reg_data);
            pdf.extend_from_slice(b"\nendstream\nendobj\n");
            obj_count
        };
        let desc_id = obj!(format!("<< /Type /FontDescriptor /FontName /DMSans /Flags 32 /FontFile2 {} 0 R /ItalicAngle 0 /Ascent 1000 /Descent -200 /CapHeight 700 /StemV 80 >>", stream_id).as_bytes());
        f1_ref = obj!(format!("<< /Type /Font /Subtype /TrueType /BaseFont /DMSans /FirstChar 32 /LastChar 255 /Encoding /WinAnsiEncoding /FontDescriptor {} 0 R >>", desc_id).as_bytes());
    } else {
        f1_ref = obj!(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
    }

    if let Some(bold_data) = bold_ttf {
        let stream_id = {
            obj_count += 1;
            offsets.push(pdf.len());
            pdf.extend_from_slice(format!("{} 0 obj\n", obj_count).as_bytes());
            pdf.extend_from_slice(format!("<< /Length {} /Length1 {} >>\nstream\n", bold_data.len(), bold_data.len()).as_bytes());
            pdf.extend_from_slice(bold_data);
            pdf.extend_from_slice(b"\nendstream\nendobj\n");
            obj_count
        };
        let desc_id = obj!(format!("<< /Type /FontDescriptor /FontName /PlayfairDisplay-Bold /Flags 32 /FontFile2 {} 0 R /ItalicAngle 0 /Ascent 1000 /Descent -200 /CapHeight 700 /StemV 120 >>", stream_id).as_bytes());
        f2_ref = obj!(format!("<< /Type /Font /Subtype /TrueType /BaseFont /PlayfairDisplay-Bold /FirstChar 32 /LastChar 255 /Encoding /WinAnsiEncoding /FontDescriptor {} 0 R >>", desc_id).as_bytes());
    } else {
        f2_ref = obj!(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>");
    }

    // obj: content stream
    let content_id = {
        obj_count += 1;
        offsets.push(pdf.len());
        pdf.extend_from_slice(format!("{} 0 obj\n", obj_count).as_bytes());
        pdf.extend_from_slice(format!("<< /Length {} >>\nstream\n", content.len()).as_bytes());
        pdf.extend_from_slice(content.as_bytes());
        pdf.extend_from_slice(b"\nendstream\nendobj\n");
        obj_count
    };

    // obj 3: page (deferred - write now)
    let page_data = format!(
        "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] /Contents {} 0 R /Resources << /Font << /F1 {} 0 R /F2 {} 0 R >> >> >>",
        pw, ph, content_id, f1_ref, f2_ref
    );
    // we need to insert page as obj 3, but we already wrote objects 1,2 then font objects
    // so obj 3 wasn't written yet. let's write it now
    obj_count += 1;
    offsets.push(pdf.len());
    pdf.extend_from_slice(format!("{} 0 obj\n{}\nendobj\n", obj_count, page_data).as_bytes());
    let _page_obj = obj_count;

    // fix pages to reference our page object
    // this is hacky but works - we'll just rebuild the whole thing properly

    // actually let's just rebuild cleanly
    drop(pdf);
    drop(offsets);
    build_pdf_clean(content, pw, ph, regular_ttf, bold_ttf)
}

fn build_pdf_clean(content: &str, pw: f64, ph: f64, regular_ttf: Option<&[u8]>, bold_ttf: Option<&[u8]>) -> Vec<u8> {
    // collect all objects first, then serialize
    struct PdfObj {
        data: Vec<u8>,
    }

    let mut objects: Vec<PdfObj> = Vec::new();

    // helper to add object, returns 1-based index
    let mut add_obj = |data: Vec<u8>| -> usize {
        objects.push(PdfObj { data });
        objects.len() // 1-based
    };

    // obj 1: catalog (placeholder, will reference pages)
    add_obj(b"<< /Type /Catalog /Pages 2 0 R >>".to_vec());

    // obj 2: pages (placeholder, will reference page)
    add_obj(Vec::new()); // placeholder

    // font objects
    let f1_ref;
    let f2_ref;

    if let Some(reg) = regular_ttf {
        let mut stream = format!("<< /Length {} /Length1 {} >>\nstream\n", reg.len(), reg.len()).into_bytes();
        stream.extend_from_slice(reg);
        stream.extend_from_slice(b"\nendstream");
        let stream_id = add_obj(stream);

        let desc = format!("<< /Type /FontDescriptor /FontName /DMSans /Flags 32 /FontFile2 {} 0 R /ItalicAngle 0 /Ascent 1000 /Descent -200 /CapHeight 700 /StemV 80 >>", stream_id);
        let desc_id = add_obj(desc.into_bytes());

        let font = format!("<< /Type /Font /Subtype /TrueType /BaseFont /DMSans /FirstChar 32 /LastChar 255 /Encoding /WinAnsiEncoding /FontDescriptor {} 0 R >>", desc_id);
        f1_ref = add_obj(font.into_bytes());
    } else {
        f1_ref = add_obj(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".to_vec());
    }

    if let Some(bold) = bold_ttf {
        let mut stream = format!("<< /Length {} /Length1 {} >>\nstream\n", bold.len(), bold.len()).into_bytes();
        stream.extend_from_slice(bold);
        stream.extend_from_slice(b"\nendstream");
        let stream_id = add_obj(stream);

        let desc = format!("<< /Type /FontDescriptor /FontName /PlayfairDisplay-Bold /Flags 32 /FontFile2 {} 0 R /ItalicAngle 0 /Ascent 1000 /Descent -200 /CapHeight 700 /StemV 120 >>", stream_id);
        let desc_id = add_obj(desc.into_bytes());

        let font = format!("<< /Type /Font /Subtype /TrueType /BaseFont /PlayfairDisplay-Bold /FirstChar 32 /LastChar 255 /Encoding /WinAnsiEncoding /FontDescriptor {} 0 R >>", desc_id);
        f2_ref = add_obj(font.into_bytes());
    } else {
        f2_ref = add_obj(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>".to_vec());
    }

    // content stream
    let mut cs = format!("<< /Length {} >>\nstream\n", content.len()).into_bytes();
    cs.extend_from_slice(content.as_bytes());
    cs.extend_from_slice(b"\nendstream");
    let content_id = add_obj(cs);

    // page object
    let page = format!(
        "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] /Contents {} 0 R /Resources << /Font << /F1 {} 0 R /F2 {} 0 R >> >> >>",
        pw, ph, content_id, f1_ref, f2_ref
    );
    let page_id = add_obj(page.into_bytes());

    // fix pages object (obj 2) to reference the page
    objects[1].data = format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", page_id).into_bytes();

    // serialize
    let mut pdf = Vec::new();
    pdf.extend_from_slice(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n");

    let mut obj_offsets = Vec::new();
    for (i, obj) in objects.iter().enumerate() {
        obj_offsets.push(pdf.len());
        pdf.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
        pdf.extend_from_slice(&obj.data);
        pdf.extend_from_slice(b"\nendobj\n");
    }

    let xref = pdf.len();
    pdf.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
    pdf.extend_from_slice(b"0000000000 65535 f \n");
    for off in &obj_offsets {
        pdf.extend_from_slice(format!("{:010} 00000 n \n", off).as_bytes());
    }

    pdf.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{}\n%%EOF\n", objects.len() + 1, xref).as_bytes());
    pdf
}
