import logging
import socket
import time
from typing import List
from urllib.parse import urlparse, parse_qs
from peewee import DoesNotExist
from median.models import (
    Printer, PrinterMag, PrinterLog, PrinterCommand, PrinterCommandLabel,
    PrinterLabel, Magasin, User)

from ..base import BaseView, BaseViewException
from datetime import datetime

logger = logging.getLogger('median.view.printer')


class PrinterViewException(BaseViewException):
    pass


class PrinterView(BaseView):
    """Manage printer and printing"""
    isTest: bool = False
    numRetry: int = 0
    state_socket: str = ""
    printer: Printer = None
    current_user: User = None
    printer_socket: socket.socket = None
    printer_command: PrinterCommand = None

    def __init__(self, printer: Printer = None, retry: int = 3, current_user: User = None):
        """
        Indicate the number of retry
        """
        logger.debug("PrinterView::init")
        self.printer = printer
        self.numRetry = retry
        self.current_user = current_user

    @staticmethod
    def printers_list():
        """
        Retrieve a list of all printers.

        This method queries the database to fetch all printers and orders them by name.

        Returns:
            list: A list of `Printer` objects ordered by name.

        Example:
            >>> printers = PrinterView.printers_list()
            >>> for printer in printers:
            ...     print(printer.name)
            'Printer1'
            'Printer2'
        """
        return Printer.select(Printer).order_by(Printer.name)

    @staticmethod
    def printers_by_warehouse(magasin: Magasin = None):
        """
        Retrieve a list of printers associated with a specified warehouse.

        This method queries the database to find all printers linked to a given warehouse.
        If no warehouse is provided, it returns an empty list.

        Args:
            magasin (Magasin, optional): The warehouse for which to retrieve the printers.
                                         If not provided, the method returns an empty list.
        Returns:
            list: A list of `Printer` objects associated with the specified warehouse.

        Example:
            >>> warehouse = Magasin.get(id=1)
            >>> printers = PrinterView.printers_by_warehouse(warehouse)
            >>> for printer in printers:
            ...     print(printer.name)
            'Printer1'
            'Printer2'
        """
        if not magasin:
            return []
        return Printer.select(Printer, PrinterMag).join(
            PrinterMag, on=((PrinterMag.printer_id == Printer.pk))
        ).where(
            PrinterMag.mag_id == magasin
        ).order_by(Printer.name)

    @staticmethod
    def printers_by_label(label: PrinterLabel = None) -> List[Printer]:
        """
        Retrieve a list of printers associated with a specified label.

        This method queries the database to find all printers linked to a given label.
        If no label is provided, it returns an empty list.

        Args:
            label (PrinterLabel, optional): The label for which to retrieve the printers.
                                            If not provided, the method returns an empty list.
        Returns:
            List[Printer]: A list of `Printer` objects associated with the specified label.

        Example:
            >>> label = PrinterLabel.get(id=1)
            >>> printers = PrinterView.printers_by_label(label)
            >>> for printer in printers:
            ...     print(printer.name)
            'Printer1'
            'Printer2'
        """
        if not label:
            return []
        return Printer.select(Printer).where(
            Printer.current_label_id == label
        ).order_by(Printer.name)

    @staticmethod
    def printers_by_command(command: PrinterCommand = None) -> List[Printer]:
        """
        Retrieve a list of printers associated with a specified command.

        This method queries the database to find all printers linked to a given command.
        If no command is provided, it returns an empty list.
        Args:
            command (PrinterCommand): The command for which to retrieve the printers.
                                      If not provided, the method returns an empty list.

        Returns:
            List[Printer]: A list of `Printer` objects associated with the specified command.

        Example:
            >>> command = PrinterCommand.get(id=1)
            >>> printers = PrinterView.printers_by_command(command)
            >>> for printer in printers:
            ...     print(printer.name)
            'Printer1'
            'Printer2'
        """
        if not command:
            return []
        return Printer.select(Printer).join(
            PrinterCommandLabel, on=(Printer.current_label_id == PrinterCommandLabel.label_id)
            ).join(
                PrinterCommand, on=(PrinterCommandLabel.command_id == PrinterCommand.pk)
            ).where(PrinterCommand.code == command.code).order_by(Printer.name).distinct()

    @staticmethod
    def default_printer(magasin: Magasin = None) -> Printer:
        """
        Retrieve the default printer associated with a specified warehouse.

        This method queries the database to find the primary printer linked to a given warehouse.
        If no default printer is found, it raises a `PrinterViewException`.
        Args:
            magasin (Magasin, optional): The warehouse for which to retrieve the default printer.
                                         If not provided, the method returns `None`.

        Returns:
            Printer: The default printer associated with the specified warehouse.

        Raises:
            PrinterViewException: If no default printer is found for the specified warehouse.

        Example:
            >>> warehouse = Magasin.get(id=1)
            >>> default_printer = PrinterView.default_printer(warehouse)
            >>> print(default_printer.name)
            'Printer1'
        """
        if not magasin:
            return None
        try:
            return Printer.select(Printer).join(
                PrinterMag, on=((PrinterMag.printer_id == Printer.pk))
            ).where(
                (PrinterMag.mag_id == magasin) & (PrinterMag.primary == 1)
            ).get()
        except DoesNotExist:
            raise PrinterViewException(f"no default printer for {magasin.mag}")

    def printer_template(self, command_name: str) -> list[str, str]:
        """
        Retrieve the print template for a given command name.

        This method fetches the print template associated with the specified command name and
        the current label ID of the printer.
        If the command name does not exist or no template is found for the current label ID,
        it raises a `PrinterViewException`.
        Args:
            command_name (str): The name of the command for which to retrieve the print template.

        Returns:
            tuple: A tuple containing the print code and print dictionary for the specified command name and label ID.

        Raises:
            PrinterViewException: If the command name does not exist or no template is found for the current label ID.

        Example:
            >>> printer_view = PrinterView(printer=printer_instance, current_user=user_instance)
            >>> print_code, print_dict = printer_view.printer_template("example_command")
            >>> print(print_code)
            'example_print_code'
            >>> print(print_dict)
            {'key': 'value'}
        """
        try:
            self.printer_command = PrinterCommand.get(code=command_name)
        except DoesNotExist:
            raise PrinterViewException(f"command name {command_name} not exists")

        try:
            res = PrinterCommandLabel.get(command_id=self.printer_command.pk, label_id=self.printer.current_label_id.pk)
            return (res.print_code, res.print_dict)
        except DoesNotExist:
            raise PrinterViewException(
                f"No command {command_name} label found for label_id {self.printer.current_label_id.pk}")
        return ("", "")

    def send(self, datas: None, details: str = None) -> int:
        """
        Send the content to the socket, if return is 0, the connection is broken
        """
        logger.debug("type of datas %s" % type(datas))
        if self.printer_socket is None and not self.isTest:
            return None
        if datas is None or (isinstance(datas, str) and len(datas) == 0):
            return None

        # Check the state of the connection
        self._check_connection()

        ret_value = 0
        if self.isTest:
            if isinstance(datas, bytes):
                ret_value = len(datas.decode("utf-8"))
                logger.debug(datas.decode("utf-8"))
            else:
                ret_value = len(str(datas))
                logger.debug(str(datas))
        elif isinstance(datas, bytes):
            ret_value = self._send(datas, details)
        else:
            ret_value = self._send(str(datas).encode(), details)

        if ret_value == 0:
            self.state_socket = "broken"
            self.numRetry -= 1

        if self.numRetry <= 0:
            raise PrinterViewException("Number of retry exceeded")
        return ret_value

    def _send(self, datas: bytes, details: str = None) -> int:
        plog = PrinterLog()
        plog.chrono = datetime.now()
        plog.content = datas.decode("utf-8")
        plog.printer_id = self.printer
        plog.label_id = self.printer.current_label_id if isinstance(self.printer.current_label_id, int) \
            else self.printer.current_label_id.pk
        plog.printing_nb = 1
        plog.user_id = self.current_user
        plog.detail = details or ("??? %s" % time.strftime("%Y%m%d-%H%M%S"))
        plog.save()
        try:
            return self.printer_socket.send(datas)
        except socket.timeout as e:
            logger.error(f"Connection timeout to printer at {self.printer.address}")
            logger.error(str(e))
            raise PrinterViewException("Printer connection timeout" + str(e))
        except socket.error as e:
            logger.error(f"Socket error when connecting to printer: {str(e)}")
            logger.error(str(e))
            raise PrinterViewException("Printer connection error" + str(e))
        except Exception as e:
            logger.error(f"Printing error on {self.printer.address}")
            logger.error(str(e))
            raise PrinterViewException("Error to send label to printer configuration!" + str(e))

    def _check_connection(self):
        """
        Check if connection is not broken
        """
        if self.state_socket == "broken":
            self._end_connection()
            self._begin_connection()
            self.state_socket = ""

    def _begin_connection(self):
        url = urlparse(self.printer.address)
        qs = parse_qs(url.query)
        if url.scheme == "tcp":
            host, port = url.netloc.split(":")
            logger.debug("%s" % repr(qs))
            timeout = int(qs.get('timeout', ['30'])[0])
            logger.info("timeout %i" % timeout)
            try:
                self.printer_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.printer_socket.settimeout(timeout)
                self.printer_socket.connect((host, int(port)))
            except socket.timeout as e:
                logger.error(f"Connection timeout to printer at {self.printer.address}")
                logger.error(str(e))
                raise PrinterViewException("Printer connection timeout" + str(e))
            except socket.error as e:
                logger.error(f"Socket error when connecting to printer: {str(e)}")
                logger.error(str(e))
                raise PrinterViewException("Printer connection error" + str(e))
            except Exception as e:
                logger.error(f"Printing error on {self.printer.address}")
                logger.error(str(e))
                raise PrinterViewException("Error to send label to printer configuration!" + str(e))
        elif url.scheme == "mock":
            # Use with pytest, to simulate the Mock connection
            self.isTest = True
        else:
            raise PrinterViewException("Printer must be in TCP")

    def _end_connection(self):
        if self.printer_socket is not None:
            self.printer_socket.close()

    def __enter__(self):
        if self.printer is None:
            raise PrinterViewException("Printer name is required on context mode")
        logger.debug(f"PrinterView::Setting up {self.printer.name}")
        self._begin_connection()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        logger.debug(f"PrinterView::Cleaning up {self.printer.name}")
        self._end_connection()

    def __del__(self):
        try:
            self._end_connection()
        except Exception:
            pass
        finally:
            logger.debug("PrinterView::cleanup")
