"""Brother QL direct print service for case hardware labels."""

from __future__ import annotations

import logging
import socket
from dataclasses import dataclass
from typing import Iterable, List, Optional

from PIL import Image, ImageDraw, ImageFont

# Compatibility shim: brother_ql may still reference Image.ANTIALIAS,
# which was removed in newer Pillow releases.
if not hasattr(Image, "ANTIALIAS") and hasattr(Image, "Resampling"):
    Image.ANTIALIAS = Image.Resampling.LANCZOS

logger = logging.getLogger(__name__)

try:
    from brother_ql.backends.helpers import send
    from brother_ql.conversion import convert
    from brother_ql.raster import BrotherQLRaster
    from brother_ql.labels import ALL_LABELS
except Exception:  # pragma: no cover - handled at runtime
    send = None
    convert = None
    BrotherQLRaster = None
    ALL_LABELS = None


_CODE39_PATTERNS = {
    "0": "nnnwwnwnn", "1": "wnnwnnnnw", "2": "nnwwnnnnw", "3": "wnwwnnnnn",
    "4": "nnnwwnnnw", "5": "wnnwwnnnn", "6": "nnwwwnnnn", "7": "nnnwnnwnw",
    "8": "wnnwnnwnn", "9": "nnwwnnwnn", "A": "wnnnnwnnw", "B": "nnwnnwnnw",
    "C": "wnwnnwnnn", "D": "nnnnwwnnw", "E": "wnnnwwnnn", "F": "nnwnwwnnn",
    "G": "nnnnnwwnw", "H": "wnnnnwwnn", "I": "nnwnnwwnn", "J": "nnnnwwwnn",
    "K": "wnnnnnnww", "L": "nnwnnnnww", "M": "wnwnnnnwn", "N": "nnnnwnnww",
    "O": "wnnnwnnwn", "P": "nnwnwnnwn", "Q": "nnnnnnwww", "R": "wnnnnnwwn",
    "S": "nnwnnnwwn", "T": "nnnnwnwwn", "U": "wwnnnnnnw", "V": "nwwnnnnnw",
    "W": "wwwnnnnnn", "X": "nwnnwnnnw", "Y": "wwnnwnnnn", "Z": "nwwnwnnnn",
    "-": "nwnnnnwnw", ".": "wwnnnnwnn", " ": "nwwnnnwnn", "$": "nwnwnwnnn",
    "/": "nwnwnnnwn", "+": "nwnnnwnwn", "%": "nnnwnwnwn", "*": "nwnnwnwnn",
}


@dataclass
class LabelJob:
    name: str
    meta_line: str
    token: str


class BrotherLabelPrintService:
    def __init__(
        self,
        model: str,
        host: str,
        port: int,
        label_size: str,
    ) -> None:
        self.model = (model or "QL-710W").strip()
        self.host = (host or "").strip()
        self.port = int(port or 9100)
        self.label_size = self._normalize_label_size((label_size or "62").strip())
        self.label_spec = self._resolve_label_spec(self.label_size)
        self.printable_width = self._resolve_printable_width(self.label_size)
        self.printable_height = self._resolve_printable_height(self.label_size)
        self.is_die_cut = bool(self.label_spec and getattr(self.label_spec, "form_factor", None) and "DIE_CUT" in str(getattr(self.label_spec, "form_factor", "")))

    @property
    def printer_identifier(self) -> str:
        return f"tcp://{self.host}:{self.port}"

    def print_jobs(self, jobs: Iterable[LabelJob]) -> int:
        if not self.host:
            raise ValueError("Printer host is missing")
        if not send or not convert or not BrotherQLRaster:
            raise RuntimeError("brother_ql library is not installed in this environment")

        send_func = send
        convert_func = convert
        raster_cls = BrotherQLRaster

        rendered_images = [self._build_label_image(job) for job in jobs]
        if not rendered_images:
            return 0

        qlr = raster_cls(self.model)
        instructions = convert_func(
            qlr=qlr,
            images=rendered_images,
            label=self.label_size,
            rotate='auto' if self.is_die_cut else 0,
            cut=True,
            dither=False,
            compress=False,
            red=False,
            dpi_600=False,
        )

        self._send_to_printer(instructions, send_func)
        return len(rendered_images)

    def _send_to_printer(self, instructions: List[bytes], send_func) -> None:
        target = self.printer_identifier
        # brother_ql helper changed call signature across versions.
        try:
            send_func(instructions, target, "network", blocking=True)
            return
        except TypeError:
            pass

        try:
            send_func(instructions=instructions, printer_identifier=target, backend_identifier="network", blocking=True)
            return
        except TypeError:
            pass

        # Final fallback to raw socket stream for network printers.
        payload = b"".join(instructions)
        with socket.create_connection((self.host, self.port), timeout=10) as conn:
            conn.sendall(payload)

    def _build_label_image(self, job: LabelJob) -> Image.Image:
        width = self.printable_width
        height = self.printable_height if self.printable_height > 0 else 220
        image = Image.new("RGB", (width, height), "white")
        draw = ImageDraw.Draw(image)
        font_title = ImageFont.load_default()
        font_meta = ImageFont.load_default()
        font_token = ImageFont.load_default()

        title = (job.name or "Ukendt enhed")[:52]
        meta = (job.meta_line or "-")[:88]
        token = (job.token or "")[:64]

        left = 12
        top = 8
        right = max(left + 1, width - 12)

        # Compact layout for die-cut labels to fit exact printable area.
        if self.is_die_cut:
            title_y = top
            meta_y = title_y + 18
            barcode_y = meta_y + 16
            token_y = min(height - 14, barcode_y + max(26, int(height * 0.28)) + 4)
            bar_height = max(24, min(int(height * 0.28), height - barcode_y - 22))
        else:
            title_y = 12
            meta_y = 34
            barcode_y = 64
            token_y = min(height - 16, 170)
            bar_height = max(48, min(92, height - barcode_y - 26))

        draw.text((left, title_y), title, fill="black", font=font_title)
        draw.text((left, meta_y), meta, fill="black", font=font_meta)
        self._draw_code39(draw, token, x=left, y=barcode_y, max_width=max(60, right - left), bar_height=bar_height)
        draw.text((left, token_y), token, fill="black", font=font_token)
        return image

    def _normalize_label_size(self, label_size: str) -> str:
        wanted = str(label_size or "").strip()
        if wanted == "29":
            # Legacy compatibility: old config often used "29" while hardware stock is 62x29 die-cut.
            logger.warning("⚠️ Label size '29' mapped to '62x29' for Brother QL hardware labels")
            return "62x29"
        return wanted or "62"

    @staticmethod
    def _resolve_label_spec(label_size: str):
        if not ALL_LABELS:
            return None
        wanted = str(label_size or "").strip()
        for lbl in ALL_LABELS:
            if getattr(lbl, "identifier", "") == wanted:
                return lbl
        return None

    @staticmethod
    def _resolve_printable_width(label_size: str) -> int:
        default_width = 696  # 62mm endless printable width
        if not ALL_LABELS:
            return default_width
        try:
            wanted = str(label_size or "").strip()
            for lbl in ALL_LABELS:
                if getattr(lbl, "identifier", "") == wanted:
                    dots = getattr(lbl, "dots_printable", None)
                    if isinstance(dots, tuple) and len(dots) > 0 and int(dots[0]) > 0:
                        return int(dots[0])
        except Exception:
            return default_width
        return default_width

    @staticmethod
    def _resolve_printable_height(label_size: str) -> int:
        if not ALL_LABELS:
            return 220
        try:
            wanted = str(label_size or "").strip()
            for lbl in ALL_LABELS:
                if getattr(lbl, "identifier", "") == wanted:
                    dots = getattr(lbl, "dots_printable", None)
                    if isinstance(dots, tuple) and len(dots) > 1 and int(dots[1]) > 0:
                        return int(dots[1])
                    return 220
        except Exception:
            return 220
        return 220

    def _draw_code39(
        self,
        draw: ImageDraw.ImageDraw,
        value: str,
        x: int,
        y: int,
        max_width: int,
        bar_height: int,
    ) -> None:
        safe = "".join(ch for ch in (value or "").upper() if ch in _CODE39_PATTERNS and ch != "*")
        if not safe:
            safe = "EMPTY"
        seq = f"*{safe}*"

        # Prefer physically narrower bars first; scanners struggle when Code39
        # modules become too wide on small die-cut labels.
        variants = [
            (1, 2, 0),
            (1, 3, 1),
            (2, 5, 1),
        ]

        narrow, wide, gap = variants[0]
        for candidate in variants:
            c_narrow, c_wide, c_gap = candidate
            width = self._code39_width(seq, c_narrow, c_wide, c_gap)
            if width <= max_width:
                narrow, wide, gap = c_narrow, c_wide, c_gap
                break

        cursor = x
        for ch in seq:
            pattern = _CODE39_PATTERNS[ch]
            for idx, code in enumerate(pattern):
                stroke = wide if code == "w" else narrow
                if idx % 2 == 0:
                    draw.rectangle([cursor, y, cursor + stroke - 1, y + bar_height], fill="black")
                cursor += stroke
                if idx < len(pattern) - 1:
                    cursor += gap
            cursor += gap

    @staticmethod
    def _code39_width(sequence: str, narrow: int, wide: int, gap: int) -> int:
        total = 0
        for ch in sequence:
            pattern = _CODE39_PATTERNS[ch]
            for idx, code in enumerate(pattern):
                total += wide if code == "w" else narrow
                if idx < len(pattern) - 1:
                    total += gap
            total += gap
        return total
