"""
This class manages connexion to a Printer
"""

import os.path
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass

from enum import Enum
import uuid
import threading
import usb.core

from PIL import Image, ImageEnhance
import numpy as np

# Importing the modules needed for each supported printer Type
import escpos.printer
from brother_ql.models import ModelsManager
from brother_ql.backends import backend_factory
from brother_ql.raster import BrotherQLRaster
from brother_ql.conversion import convert
from brother_ql.backends.helpers import send


class PrinterType(Enum):
    """
    What are the capacities of a Printer ?
    """

    EPSON = "epson"
    BROTHER = "brother"


# For Brother-QL Printers
@dataclass
class PrinterInfo:
    """
    Brother-QL printer information
    """
    identifier: str
    backend: str
    protocol: str
    vendor_id: str
    product_id: str
    serial_number: str
    name: str = "Brother QL Printer"
    model: str = "QL-570"
    status: str = "unknown"
    label_type: str = "unknown"
    label_size: str = "unknown"
    label_width: int = 0
    label_height: int = 0

    def __getitem__(self, item):
        return getattr(self, item)

    def __setitem__(self, key, value):
        setattr(self, key, value)


class Printer(ABC):
    """
    If it outputs printed paper and speaks like a printer, then it must be a printer.
    """

    def __init__(self, app, vendor_id, device_id, printer_type: PrinterType):
        """
        We initialize a Printer via it's USB connexion, and generate a unique ID
        """
        self.id = uuid.uuid4()
        self.app = app
        self.vendor_id = vendor_id
        self.device_id = device_id
        self.ready = False
        self.printer_type = printer_type
        self._lock = threading.Lock()

    @abstractmethod
    def _has_paper(self) -> bool:
        """Check if the printer has papier"""

    @abstractmethod
    def _state(self) -> bool:
        """Reports the state of the Printer"""

    @abstractmethod
    def print_task(self, task_type, data) -> None:
        """Takes a PrintTask and executes it"""


class EscPosPrinter(Printer):
    """
    Create a new ESC/POS based printer.
    """

    def __init__(self, app, vendor_id, device_id):
        """
        Create a connexion to a ESC/POS Printer via USB,
        Making sure the printer is alive,
        Making sure it has paper,
        Define default print settings
        """
        super().__init__(app, vendor_id, device_id, printer_type=PrinterType.EPSON)
        self.printer = None
        self.usb_args = {}
        self.usb_args["idVendor"] = self.vendor_id
        self.usb_args["idProduct"] = self.device_id

        try:
            # This also calls open(), which we need to close()
            # or else the device will appear as busy.
            p = escpos.printer.Usb(self.vendor_id, self.device_id, 0, profile="TM-P80")
        except escpos.exceptions.DeviceNotFoundError as e:
            self.app.logger.error(
                "The USB device is not plugged in : %s",
                str(e),
            )
        except Exception as e:
            self.app.logger.error("Printer could not be connected : %s ", str(e))

        try:
            if p.is_online():
                self.app.logger.debug("Printer online !")
        except Exception as e:
            raise e

        # Setting up the printing options.
        p.set(
            align="center",
            font="a",
            bold=False,
            underline=0,
            width=1,
            height=1,
            density=9,
            invert=False,
            smooth=False,
            flip=False,
            double_width=False,
            double_height=False,
            custom_size=False,
        )
        # Beware : if we print every time the printer becomes ready, it means
        # we are printing before and after every print !
        self.printer = p
        self.printer.close()  # We close the connexion to the Printer

        try:
            self._has_paper()
        except Exception as e:
            raise e

        self.ready = True

    def _has_paper(self):
        """Check if the printer has paper left"""
        self.app.logger.debug("Checking paper status...")
        self.printer.open(self.usb_args)
        status = self.printer.paper_status()
        match status:
            case 0:
                self.app.logger.error("Printer has no more paper, aborting...")
                self.printer.close()
                raise RuntimeError("No more paper in the printer")
            case 1:
                self.app.logger.warning(
                    "Printer needs paper to be changed very soon ! "
                )
                self.printer.close()
                return True
            case 2:
                self.app.logger.debug("Printer has paper, good to go")
                self.printer.close()
                return True

    def _print_txt(self, msg, signature="", bold=False):

        if not isinstance(msg, str):
            self.app.logger.error(
                "It is not possible to print a " + str(type(msg)) + ", only strings."
            )
            raise ValueError

        # We make sure that the signature is not something too goofy
        clean_msg = str(msg) + "\n"
        clean_signature = str(signature)

        # Make checks on the size of the message being printed
        if len(clean_msg) > 4096:
            self.app.logger.warning(
                "Could not print message of this length: " + str(len(clean_msg))
            )
            raise RuntimeError(
                "Could not print message of this length :"
                + str(len(clean_msg))
                + ", needs to be below 4096 caracters long."
            )

        if len(signature) > 256:
            self.app.logger.warning(
                "Could not print signature of this length: " + str(len(clean_signature))
            )
            raise RuntimeError(
                "Could not print signature of this length :"
                + str(len(clean_signature))
                + ", needs to be below 256 caracters long."
            )

        # Do the actual printing
        # We would pop the next element in the queue here, if it's a sms type
        try:
            self.printer.open(self.usb_args)
            self.printer.set(align="center", font="a", bold=bold)
            self.printer.textln(clean_msg)
            if clean_signature:
                self.printer.textln(clean_signature)
            self.printer.textln()
            self.printer.close()
        except Exception as e:
            self.app.logger.error("Unable to print because : " + str(e))
            raise RuntimeError(
                "Unable to print a SMS, the printer couldn't do it."
            ) from e

        self.app.logger.info("Printed text")
        return True

    def _print_img(self, path, signature="", center=True, process=False):
        clean_signature = str(signature)

        if len(signature) > 256:
            self.app.logger.warning(
                "Could not print signature of this length: " + str(len(clean_signature))
            )
            raise ValueError(
                "Could not print signature of this length :"
                + str(len(clean_signature))
                + ", needs to be below 256 caracters long."
            )

        if not os.path.isfile(str(path)):
            self.app.logger.warning("File does not exist : " + str(path))
            raise OSError(
                "The file path for this image :"
                + str(path)
                + " wasn't found. Please try again."
            )

        self.app.logger.debug("Printing file from " + str(path))

        if process:
            try:
                self.app.logger.debug("Proccessing the image")
                path = _process_image(self, path)
            except RuntimeError as e:
                self.app.logger.error(
                    "Error while processing the image, aborting print : %s", str(e)
                )
                raise e
        else:
            self.app.logger.warning("Not proccessing the image")

        try:
            self.printer.open(self.usb_args)
            self.printer.image(path, center=center)
            self.printer.textln(signature)
            self.printer.close()
            self.app.logger.debug("Printed an image : " + str(path))
        except Exception as e:
            self.app.logger.error(str(e))
            raise RuntimeError("Could not print the picture") from e
        finally:
            try:
                os.remove(path)
            except OSError as e:
                raise e

            self.app.logger.debug("Removed image : " + str(path))

            try:
                self.printer.close()
            except Exception as e:
                self.app.logger.error(
                    "Could not close the printer connexion %s", str(e)
                )
                raise RuntimeError("Could not close the printer connexion. ") from e

        self.app.logger.info("Printed a picture")
        return True

    def _qr(self, content):
        try:
            self.printer.open(self.usb_args)
            self.printer.qr(content, center=True)
            self.printer.textln(content)
            self.printer.close()
        except RuntimeError as e:
            self.printer.close()
            self.app.logger.error(str(e))
            raise e

        self.app.logger.info("Printed a QR")

    def _cut(self):
        try:
            self.printer.open(self.usb_args)
            self.printer.cut()
            self.printer.close()
        except Exception as e:
            self.printer.close()
            self.app.logger.error(str(e))
            raise e

        self.app.logger.info("Did a cut")
        return True

    def _state(self):
        return self.printer.is_online() and self.ready and self._has_paper()

    def print_task(self, task_type, data):
        """Execute actual print based on task type"""
        with self._lock:
            if self._state:
                self._state = False
                try:
                    match (task_type.value):
                        case "text":
                            self._print_txt(data["txt"], signature=data["sign"])
                            self._state = True
                        case "image":
                            self._print_img(
                                data["img"], signature=data["sign"], process=data["process"]
                            )
                            self._state = True
                        case "cut":
                            self._cut()
                            self._state = True
                        case "qr":
                            self._qr(data["txt"])
                            self._state = True
                        case _:
                            raise RuntimeError("This task type is not supported")
                except Exception as e:
                    self._state = True
                    raise RuntimeError from e

                raise RuntimeError("The printer is not ready to print yet !")


class BrotherPrinter(Printer):
    """
    Manages connexion and capabilities of a BrotherQL Printer
    """

    def __init__(self, app, vendor_id, device_id):
        super().__init__(
            app, vendor_id="", device_id="", printer_type=PrinterType.BROTHER
        )
        self.printer = None
        self.usb_args = {}
        self.usb_args["idVendor"] = self.device_id
        self.usb_args["idProduct"] = self.vendor_id
        self.model_manager = ModelsManager()

        # Code taken from https://github.com/5shekel/printit/blob/master/printer_utils.py

        backend = backend_factory("pyusb")
        available_devices = backend["list_available_devices"]()

        for printer in available_devices:
            self.app.logger.debug(f"Found device: {printer}")
            identifier = printer["identifier"]
            parts = identifier.split("/")

            if len(parts) < 4:
                self.app.logger.warning(
                    f"Skipping device with invalid identifier format: {identifier}"
                )
                continue

            protocol = parts[0]
            # device_info = parts[2]
            serial_number = parts[3]

        try:
            product_id_int = int(self.device_id, 16)
            for m in self.model_manager.iter_elements():
                if m.product_id == product_id_int:
                    model = m.identifier
                    break
            self.app.logger.debug(f"Matched printer model: {model}")
        except ValueError:
            self.app.logger.warning(f"Invalid product ID format: {m.product_id}")

        self.printer_info = PrinterInfo(
            identifier=identifier,
            backend="pyusb",
            model=model,
            protocol=protocol,
            vendor_id=vendor_id,
            product_id=self.device_id,
            serial_number=serial_number,
        )

        self.ready = True

    def _has_paper(self):
        raise NotImplementedError("This printer model does not support this.")

    def _state(self):
        return self.ready

    def _print_img(self, data):
        """
        Print a raster image via a Brother QL printer
        """
        self.ready = False
        label_type = "102"
        rotate = 0
        dither = False
        try:
            # Prepare the image for printing
            qlr = BrotherQLRaster(self.printer_info["model"])

            instructions = convert(
                qlr=qlr,
                images=[data["img"]],
                label=label_type,
                rotate=rotate,
                threshold=70,
                dither=dither,
                compress=True,
                red=False,
                dpi_600=False,
                hq=False,
                cut=True,
            )

            # Debug logging
            if os.getenv("FLASK_DEBUG"):
                self.app.logger.debug(f"""
                Print parameters:
                - Label type: {label_type}
                - Rotate: {rotate}
                - Dither: {dither}
                - Model: {self.printer_info['model']}
                - Backend: {self.printer_info['backend']}
                - Identifier: {self.printer_info['identifier']}
                """)

            # Try to print using Python API
            # send() =  status = {
            #     'instructions_sent': True, # The instructions were sent to the printer.
            #     'outcome': 'unknown', # String description of the outcome of the sending operation like: 'unknown', 'sent', 'printed', 'error'
            #     'printer_state': None, # If the selected backend supports reading back the printer state, this key will contain it.
            #     'did_print': False, # If True, a print was produced. It defaults to False if the outcome is uncertain (due to a backend without read-back capability).
            #     'ready_for_next_job': False, # If True, the printer is ready to receive the next instructions. It defaults to False if the state is unknown.
            # }
            status = send(
                instructions=instructions,
                printer_identifier=self.printer_info["identifier"],
                backend_identifier="pyusb",
            )

            if (
                not status["did_print"]
                or status["outcome"] == "error"
                or status["outcome"] == "unknown"
            ):
                raise RuntimeError("Failed to print using Python API")

            if status["printer_state"]:
                self.ready = bool(status["printer_state"])
            else:
                self.ready = True

        except usb.core.USBError as e:
            # Treat timeout errors as successful since they often occur after print completion
            if e.errno == 110:  # Operation timed out
                self.app.logger.debug(
                    "USB timeout occurred - this is normal and the print likely completed"
                )
                self.app.logger.debug("Print completed (timeout is normal)")
                self.ready = True

            error_msg = f"USBError encountered: {e}"
            self.app.logger.debug(error_msg)
            raise RuntimeError from e

        except Exception as e:
            error_msg = f"Unexpected error during printing: {str(e)}"
            self.app.logger.debug(error_msg)
            raise RuntimeError from e

    def print_task(self, task_type, data):
        """Execute actual print based on task type"""
        with self._lock:
            if self._state:
                self._state = False
                match (task_type.value):
                    case "image":
                        self._print_img(data["img"])
                        self._state = True
                    case "cut":
                        # The cut happens by default on Brother QL printers.
                        self._state = True
                    case _:
                        raise RuntimeError("This task type is not supported")
            else:
                raise RuntimeError("The printer is not ready to print yet !")

        # These values are by default for now

        # raise NotImplementedError("This printer type is not implemented yet")


def _process_image(self, path):
    brightness_factor = 1.5  # Used only if image is too dark
    brightness_threshold = 100  # Brightness threshold (0–255)
    # contrast_factor = 0.6  # Less than 1.0 = lower contrast
    max_width = 575
    max_height = 1000

    with Image.open(path) as original_img:
        # Convert to RGB if needed (JPEG doesn't support alpha)
        if original_img.mode in ("RGBA", "P"):
            self.app.logger.debug("Converting the image to RGB from RGBA")
            original_img = original_img.convert("RGB")

        # Resize while maintaining aspect ratio
        original_img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
        self.app.logger.debug("Resized the image")

        # # Convert to grayscale for dithering
        # dithered_img = original_img.convert("L").convert("1")
        # Dithering using default method (Floyd–Steinberg)
        # self.app.logger.debug("Dithered the image")

        # Compute brightness of original image (grayscale average)
        grayscale = original_img.convert("L")
        avg_brightness = np.array(grayscale).mean()
        self.app.logger.debug(
            "Average brightness of the image : " + str(avg_brightness)
        )

        # Dynamically compute brightness factor if too dark
        if avg_brightness < brightness_threshold:
            brightness_factor = (
                1 + (brightness_threshold - avg_brightness) / brightness_threshold
            )
            brightness_factor = min(
                max(brightness_factor, 1.1), 2.5
            )  # Clamp between 1.1 and 2.5
            self.app.logger.debug(
                f"Image too dark, increasing brightness by a factor of {brightness_factor:.2f}"
            )
            enhancer = ImageEnhance.Brightness(original_img)
            original_img = enhancer.enhance(brightness_factor)

        # # Reduce contrast
        # contrast_enhancer = ImageEnhance.Contrast(original_img)
        # original_img = contrast_enhancer.enhance(contrast_factor)

        # Convert to JPEG and save
        jpeg_path = os.path.splitext(path)[0] + "_processed.jpg"
        original_img.save(jpeg_path, format="JPEG", quality=95, optimize=True)
        self.app.logger.debug("Processed and saved image.")

    return jpeg_path
