from __future__ import annotations

from io import BytesIO
from pathlib import Path
from types import SimpleNamespace

from app.models.mix_calculator import MixCalculatorSession


class MixCalculatorPdfUnavailableError(RuntimeError):
    pass


def _fmt_number(value: float, digits: int = 2) -> str:
    return f"{value:.{digits}f}"


def _fractional_bag_warning(session_record: MixCalculatorSession) -> str | None:
    rounded_bags = round(session_record.total_bags)
    if abs(session_record.total_bags - rounded_bags) < 1e-9:
        return None
    return (
        f"Batch size {session_record.batch_size_kg:g}kg produces {session_record.total_bags:.2f} bags "
        f"for {session_record.product_unit_of_measure}. This is not a whole-bag quantity."
    )


def _coerce_pdf_source(source):
    if isinstance(source, dict):
        lines = [SimpleNamespace(**line) if isinstance(line, dict) else line for line in source.get("lines", [])]
        return SimpleNamespace(**{**source, "lines": lines})
    return source


def build_mix_calculator_pdf(session_record: MixCalculatorSession | dict) -> bytes:
    session_record = _coerce_pdf_source(session_record)
    try:
        from reportlab.lib import colors
        from reportlab.lib.pagesizes import A4
        from reportlab.lib.utils import ImageReader
        from reportlab.pdfbase.pdfmetrics import stringWidth
        from reportlab.pdfgen import canvas
    except ModuleNotFoundError as exc:
        raise MixCalculatorPdfUnavailableError(
            "PDF generation is unavailable because 'reportlab' is not installed. "
            "Install backend dependencies again to enable PDF export."
        ) from exc

    page_width, page_height = A4
    margin = 26
    gutter = 10
    content_width = page_width - (margin * 2)
    page_top = page_height - 40

    palette = {
        "page": colors.HexColor("#FFFFFF"),
        "line": colors.HexColor("#000000"),
        "muted": colors.HexColor("#000000"),
        "text": colors.HexColor("#000000"),
        "warning_bg": colors.HexColor("#FFFFFF"),
        "warning_text": colors.HexColor("#000000"),
    }
    logo_path = Path(__file__).resolve().parents[3] / "frontend" / "static" / "logo-hsf.png"

    def clamp(value: float, minimum: float, maximum: float) -> float:
        return max(minimum, min(maximum, value))

    def fit_text(value: str, font_name: str, font_size: float, max_width: float) -> str:
        if stringWidth(value, font_name, font_size) <= max_width:
            return value
        ellipsis = "..."
        available = max_width - stringWidth(ellipsis, font_name, font_size)
        trimmed = value
        while trimmed and stringWidth(trimmed, font_name, font_size) > available:
            trimmed = trimmed[:-1]
        return f"{trimmed.rstrip()}{ellipsis}" if trimmed else ellipsis

    def wrap_text(value: str, font_name: str, font_size: float, max_width: float, max_lines: int) -> list[str]:
        words = value.split()
        if not words:
            return []
        lines: list[str] = []
        current = words[0]
        for word in words[1:]:
            candidate = f"{current} {word}"
            if stringWidth(candidate, font_name, font_size) <= max_width:
                current = candidate
            else:
                lines.append(current)
                current = word
                if len(lines) == max_lines - 1:
                    break
        if len(lines) < max_lines:
            lines.append(current)

        remaining_words = words[len(" ".join(lines).split()) :]
        if remaining_words and lines:
            lines[-1] = fit_text(f"{lines[-1]} {' '.join(remaining_words)}", font_name, font_size, max_width)
        return lines[:max_lines]

    def draw_box(pdf: canvas.Canvas, x: float, y_top: float, width: float, height: float):
        pdf.setFillColor(palette["page"])
        pdf.setStrokeColor(palette["line"])
        pdf.setLineWidth(1)
        pdf.rect(x, y_top - height, width, height, fill=1, stroke=1)

    def draw_label_value_card(
        pdf: canvas.Canvas,
        x: float,
        y_top: float,
        width: float,
        height: float,
        label: str,
        value: str,
        subtitle: str | None = None,
        value_font_size: float = 14,
    ):
        draw_box(pdf, x, y_top, width, height)
        inset_x = x + 14
        label_y = y_top - 16
        pdf.setFillColor(palette["muted"])
        pdf.setFont("Helvetica-Bold", 7.5)
        pdf.drawString(inset_x, label_y, label.upper())

        value_y = y_top - 38
        pdf.setFillColor(palette["text"])
        pdf.setFont("Helvetica-Bold", value_font_size)
        pdf.drawString(inset_x, value_y, fit_text(value, "Helvetica-Bold", value_font_size, width - 28))

        if subtitle:
            pdf.setFillColor(palette["muted"])
            pdf.setFont("Helvetica", 8)
            pdf.drawString(inset_x, y_top - height + 14, fit_text(subtitle, "Helvetica", 8, width - 28))

    buffer = BytesIO()
    pdf = canvas.Canvas(buffer, pagesize=A4)
    session_number = getattr(session_record, "session_number", None)
    document_title = (
        f"{session_number} - {session_record.product_name}"
        if session_number
        else f"Mix Calculator - {session_record.product_name}"
    )
    pdf.setTitle(document_title)
    pdf.setAuthor("Lean 101 Clients")

    pdf.setFillColor(palette["page"])
    pdf.rect(0, 0, page_width, page_height, stroke=0, fill=1)

    current_y = page_top
    mix_date_label = f"{session_record.mix_date.day} {session_record.mix_date.strftime('%B %Y')}"

    if logo_path.exists():
        logo_source = str(logo_path)
        try:
            from PIL import Image

            logo_source = Image.open(logo_path).convert("L")
        except ModuleNotFoundError:
            pass

        logo_reader = ImageReader(logo_source)
        logo_width, logo_height = logo_reader.getSize()
        aspect_ratio = logo_height / max(logo_width, 1)
        draw_width = 108
        draw_height = draw_width * aspect_ratio
        pdf.drawImage(
            logo_reader,
            margin,
            current_y - draw_height,
            width=draw_width,
            height=draw_height,
            preserveAspectRatio=True,
            mask="auto",
        )
        current_y -= draw_height + 20

    pdf.setFillColor(palette["text"])
    pdf.setFont("Helvetica-Bold", 15)
    pdf.drawString(margin, current_y, "Calculated Output")
    current_y -= 16

    pdf.setFillColor(palette["muted"])
    pdf.setFont("Helvetica", 10)
    pdf.drawString(margin, current_y, "Snapshot of the scaled raw material requirements.")
    current_y -= 20

    stat_height = 66
    stat_width = (content_width - (gutter * 2)) / 3
    draw_label_value_card(
        pdf,
        margin,
        current_y,
        stat_width,
        stat_height,
        "Total kg",
        _fmt_number(session_record.total_kg),
        "Scaled batch size",
        value_font_size=18,
    )
    draw_label_value_card(
        pdf,
        margin + stat_width + gutter,
        current_y,
        stat_width,
        stat_height,
        "Total bags",
        _fmt_number(session_record.total_bags),
        session_record.product_unit_of_measure,
        value_font_size=18,
    )
    draw_label_value_card(
        pdf,
        margin + ((stat_width + gutter) * 2),
        current_y,
        stat_width,
        stat_height,
        "Prepared by",
        session_record.prepared_by_name,
        mix_date_label,
        value_font_size=10.5,
    )
    current_y -= stat_height + 12

    detail_height = 52
    detail_width = (content_width - gutter) / 2
    draw_label_value_card(
        pdf,
        margin,
        current_y,
        detail_width,
        detail_height,
        "Client",
        session_record.client_name,
        value_font_size=12,
    )
    draw_label_value_card(
        pdf,
        margin + detail_width + gutter,
        current_y,
        detail_width,
        detail_height,
        "Mix",
        session_record.product_name,
        value_font_size=12,
    )
    current_y -= detail_height + 8

    draw_label_value_card(
        pdf,
        margin,
        current_y,
        detail_width,
        detail_height,
        "Formula source",
        session_record.mix_name,
        value_font_size=11,
    )
    draw_label_value_card(
        pdf,
        margin + detail_width + gutter,
        current_y,
        detail_width,
        detail_height,
        "Unit size",
        f"{_fmt_number(session_record.product_unit_size_kg)}kg",
        value_font_size=12,
    )
    current_y -= detail_height + 10

    warning = _fractional_bag_warning(session_record)
    note_lines: list[str] = []
    warning_lines: list[str] = []
    strip_height = 0

    if session_record.notes:
        note_lines = wrap_text(session_record.notes.replace("\n", " "), "Helvetica", 7.5, content_width - 28, 2)
        strip_height += 30
    if warning:
        warning_lines = wrap_text(warning, "Helvetica", 7.5, content_width - 28, 2)
        strip_height += 30
    if strip_height:
        strip_height += 6

    table_header_height = 24
    table_bottom_padding = 12
    table_top = current_y
    available_table_height = table_top - margin - strip_height - table_header_height - table_bottom_padding
    row_count = max(len(session_record.lines), 1)
    row_height = clamp(available_table_height / row_count, 16, 32)
    table_font_size = clamp(row_height * 0.44, 8.5, 12.5)
    table_height = table_header_height + (row_height * row_count)
    table_bottom = table_top - table_height

    pdf.setFillColor(palette["muted"])
    pdf.setFont("Helvetica-Bold", 8.5)
    pdf.drawString(margin + 4, table_top - 7, "RAW MATERIAL")
    pdf.drawString(margin + content_width - 190, table_top - 7, "REQUIRED KG")

    pdf.setStrokeColor(palette["line"])
    pdf.setLineWidth(0.8)
    pdf.line(margin, table_top - table_header_height, margin + content_width, table_top - table_header_height)

    left_col_x = margin + 6
    right_col_x = margin + content_width - 190
    y_cursor = table_top - table_header_height

    for line in session_record.lines:
        y_cursor -= row_height
        pdf.setStrokeColor(palette["line"])
        pdf.setLineWidth(0.6)
        pdf.line(margin, y_cursor, margin + content_width, y_cursor)

        text_y = y_cursor + (row_height / 2) - (table_font_size * 0.35)
        pdf.setFillColor(palette["text"])
        pdf.setFont("Helvetica-Bold", table_font_size)
        pdf.drawString(
            left_col_x,
            text_y,
            fit_text(line.raw_material_name, "Helvetica-Bold", table_font_size, content_width - 210),
        )
        pdf.setFont("Helvetica", table_font_size)
        # Each ingredient carries its own rounding (set in the Ingredients Editor)
        # so the printed sheet matches the on-screen calculated output.
        line_decimals = getattr(line, "rounding_decimals", 2)
        pdf.drawString(right_col_x, text_y, f"{_fmt_number(line.required_kg, line_decimals)}kg")

    strip_y = table_bottom - 6
    if note_lines:
        note_height = 24 if len(note_lines) == 1 else 30
        pdf.setFillColor(palette["page"])
        pdf.setStrokeColor(palette["line"])
        pdf.rect(margin, strip_y - note_height, content_width, note_height, fill=1, stroke=1)
        pdf.setFillColor(palette["muted"])
        pdf.setFont("Helvetica-Bold", 7)
        pdf.drawString(margin + 10, strip_y - 10, "NOTES")
        pdf.setFillColor(palette["text"])
        pdf.setFont("Helvetica", 7.5)
        for idx, text in enumerate(note_lines):
            pdf.drawString(margin + 10, strip_y - 20 - (idx * 8), text)
        strip_y -= note_height + 6

    if warning_lines:
        warning_height = 24 if len(warning_lines) == 1 else 30
        pdf.setFillColor(palette["warning_bg"])
        pdf.setStrokeColor(palette["line"])
        pdf.rect(margin, strip_y - warning_height, content_width, warning_height, fill=1, stroke=1)
        pdf.setFillColor(palette["warning_text"])
        pdf.setFont("Helvetica-Bold", 7)
        pdf.drawString(margin + 10, strip_y - 10, "WARNING")
        pdf.setFont("Helvetica", 7.5)
        for idx, text in enumerate(warning_lines):
            pdf.drawString(margin + 10, strip_y - 20 - (idx * 8), text)

    pdf.showPage()
    pdf.save()
    return buffer.getvalue()
