"""Blueprint to manage zebra printers, labels and print commands"""

import json
import logging
import re
import socket
from base64 import b64encode
from datetime import datetime

from flask import Blueprint, request, session
from flask_jwt_extended import jwt_required
from common.templating import render_template
from median.models import Printer, PrinterCommand, PrinterCommandLabel, PrinterLabel, PrinterType, PrinterMag
from median.models import Magasin
from median.views import PrinterView, PrinterViewException
from peewee import DatabaseError, DoesNotExist, IntegrityError

from common.util import mustHaveRights
from common.models import WebLogActions
from common.status import (
    HTTP_200_OK,
    HTTP_204_NO_CONTENT,
    HTTP_400_BAD_REQUEST,
    HTTP_404_NOT_FOUND,
    HTTP_500_INTERNAL_SERVER_ERROR,
)

printer_blueprint = Blueprint("printers", __name__)
logger = logging.getLogger("median.log")
RESSOURCE_NAME = "WEB_PRINTERS"


def _printer_to_dict(printer):
    return {
        "name": printer.name,
        "pk": printer.pk,
        "address": printer.address,
        "label_id": printer.current_label_id.pk,
        "label_name": printer.current_label_id.name,
        "label_width": printer.current_label_id.width,
        "label_height": printer.current_label_id.height,
        "type_name": printer.type_id.name,
        "type_lang": printer.type_id.lang,
        "commands": [
            cmd_label.command_id.pk
            for cmd_label in PrinterCommandLabel.select(PrinterCommandLabel)
            .join(PrinterLabel)
            .where((PrinterCommandLabel.label_id == printer.current_label_id.pk) & (PrinterCommandLabel.enabled == 1))
        ],
    }


@printer_blueprint.route("all", methods=["GET"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canView=True)
def get_data():
    printers = Printer.select(Printer, PrinterLabel, PrinterType).join(PrinterLabel).switch(Printer).join(PrinterType)

    res_printers = []
    for printer in printers:
        res_printers.append(_printer_to_dict(printer))

    commands = PrinterCommand.select().order_by(+PrinterCommand.name)

    res_commands = []
    for command in commands:
        res_commands.append(
            {
                "pk": command.pk,
                "print_code": command.pk,
                "name": command.name,
                "code": command.code,
                "labels": [
                    {
                        "cmd_lbl_pk": cmd_label.pk,
                        "label_pk": cmd_label.label_id.pk,
                        "enabled": cmd_label.enabled,
                        "print_code": cmd_label.print_code,
                        "print_dict": cmd_label.print_dict,
                        "print_density": cmd_label.density,
                        "print_unit": cmd_label.unit,
                    }
                    for cmd_label in PrinterCommandLabel.select(PrinterCommandLabel, PrinterLabel)
                    .join(PrinterLabel)
                    .where(PrinterCommandLabel.command_id == command.pk)
                ],
            }
        )

    labels = PrinterLabel.select()

    res_labels = []
    for label in labels:
        res_labels.append(
            {
                "pk": label.pk,
                "name": label.name,
                "height": label.height,
                "width": label.width,
            }
        )

    types = PrinterType.select()

    res_types = []
    for printer_type in types:
        res_types.append(
            {
                "pk": printer_type.pk,
                "name": printer_type.name,
                "lang": printer_type.lang,
                "density": printer_type.density,
                "quality": printer_type.quality,
                "unit": printer_type.unit,
            }
        )

    res = {
        "printers": res_printers,
        "commands": res_commands,
        "labels": res_labels,
        "types": res_types,
    }

    return res, HTTP_200_OK


@printer_blueprint.route("command/<string:code>", methods=["GET"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canView=True)
def get_printers_by_command(code):
    try:
        printerCommand: PrinterCommand = PrinterCommand.get(PrinterCommand.code == code)
        printers: Printer = PrinterView.printers_by_command(printerCommand)

        result = [
            {
                "pk": printer.pk,
                "name": printer.name,
                "label_name": printer.current_label_id.name,
                "label_width": printer.current_label_id.width,
                "label_height": printer.current_label_id.height,
                "primary": printer.printermag.primary if hasattr(printer, "printermag") else None,
            }
            for printer in printers
        ]

        return {"printers": result}, HTTP_200_OK

    except Exception as e:
        logger.error(f"Error fetching printers: {str(e)}")
        return {"message": "Error retrieving printers"}, HTTP_500_INTERNAL_SERVER_ERROR


@printer_blueprint.route("", methods=["GET"])
@printer_blueprint.route("<string:mag_code>", methods=["GET"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canView=True)
def get_printers(mag_code=None):
    try:
        if mag_code:
            # Get printers for a specific magasin
            mag: Magasin = Magasin.get(Magasin.mag == mag_code)
            printers: Printer = PrinterView.printers_by_warehouse(mag)

        else:
            # Get all printers from all magasins
            printers: Printer = PrinterView.printers_list()

        result = [
            {
                "pk": printer.pk,
                "name": printer.name,
                "label_name": printer.current_label_id.name,
                "label_width": printer.current_label_id.width,
                "label_height": printer.current_label_id.height,
                "primary": printer.printermag.primary if hasattr(printer, "printermag") else None,
            }
            for printer in printers
        ]

        return {"printers": result}, HTTP_200_OK

    except Exception as e:
        logger.error(f"Error fetching printers: {str(e)}")
        return {"message": "Error retrieving printers"}, HTTP_500_INTERNAL_SERVER_ERROR


@printer_blueprint.route("", methods=["PUT"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def add_printer():
    data = request.get_json()

    new_printer_name = data.get("name", None)
    new_printer_type = data.get("typeId", None)

    if not new_printer_name or not new_printer_type:
        return {"message": "printers.printer.create.missing_fields"}, HTTP_400_BAD_REQUEST

    # Check for duplicate name
    if Printer.get_or_none(Printer.name == new_printer_name):
        return {"message": "printers.printer.create.duplicate_name"}, HTTP_400_BAD_REQUEST

    # Check if printer type exists
    if not PrinterType.get_or_none(PrinterType.pk == new_printer_type):
        return {"message": "printers.printer.create.invalid_type"}, HTTP_400_BAD_REQUEST

    default_printer_label_id = PrinterLabel.select()[0].pk or None

    new_printer = Printer.create(
        name=new_printer_name,
        type_id=new_printer_type,
        current_label_id=default_printer_label_id,
        address="tcp://0.0.0.0:0000",
    )

    log_printers(
        session["username"],
        "create_printer",
        f"Added printer (pk:{new_printer.pk}, name:{new_printer.name})",
    )

    return {}, HTTP_200_OK


@printer_blueprint.route("<string:printer_id>", methods=["DELETE"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def delete_printer(printer_id):
    try:
        printer_id = int(printer_id)
    except ValueError:
        return {"message": "printers.printer.delete.invalid_id"}, HTTP_400_BAD_REQUEST

    try:
        printer = Printer.get_or_none(Printer.pk == printer_id)
        if printer is None:
            return {"message": "printers.printer.delete.not_found"}, HTTP_400_BAD_REQUEST

        printer.delete_instance()
        log_printers(session["username"], "delete_printer", f"Deleted printer (pk:{printer.pk})")
        return {"printer": printer_id}, HTTP_200_OK

    except Exception as e:
        logger.error(f"Error deleting printer: {str(e)}")
        return {"message": "printers.printer.delete.error"}, HTTP_500_INTERNAL_SERVER_ERROR


@printer_blueprint.route("/test_print/<string:printer_id>", methods=["GET"])
def test_print(printer_id):
    printer: Printer = Printer.get_or_none(Printer.pk == int(printer_id))

    # Get commands compatible with the printer's current label
    compatible_commands = []
    if printer:
        compatible_commands: PrinterCommandLabel = list(
            PrinterCommandLabel.select(PrinterCommandLabel, PrinterCommand)
            .join(PrinterCommand)
            .where(
                (PrinterCommandLabel.label_id == printer.current_label_id)
                & (PrinterCommandLabel.enabled == 1)
                & (PrinterCommand.code.startswith("calibrate"))
            )
        )

    if not printer:
        return {"message": "no printer found"}, HTTP_404_NOT_FOUND

    if len(compatible_commands) == 0:
        return {"message": "no calibration labels found"}, HTTP_404_NOT_FOUND

    try:
        first_command: PrinterCommandLabel = compatible_commands[0]

        zpl_code = first_command.print_code
        zpl_dict: dict = json.loads(first_command.print_dict)

        # zpl_dict.update({"version": "0.0.0.0.0"})

        rendered_zpl = render_template(zpl_code, zpl_dict)

        try:
            with PrinterView(printer, 3, session["user_id"]) as p:
                print_result = p.send(rendered_zpl, "Calibration")

                logger.info(f"Test print successfully sent to {printer.address}")
                return {
                    "message": "Test print sent successfully",
                    "ip": printer.address,
                    "print_status": print_result,
                }, HTTP_200_OK
        except PrinterViewException as pve:
            logger.error(f"Printer view error: {str(pve)}")
            return {"message": f"Printer error: {str(pve)}"}, HTTP_500_INTERNAL_SERVER_ERROR

    except socket.timeout:
        logger.error(f"Connection timeout to printer at {printer.address}")
        return {"message": "Printer connection timeout"}, HTTP_500_INTERNAL_SERVER_ERROR
    except socket.error as e:
        logger.error(f"Socket error when connecting to printer: {str(e)}")
        return {"message": f"Printer connection error: {str(e)}"}, HTTP_500_INTERNAL_SERVER_ERROR
    except Exception as e:
        logger.error(f"Error sending test print: {str(e)}")
        return {"message": f"Error sending test print: {str(e)}"}, HTTP_500_INTERNAL_SERVER_ERROR


@printer_blueprint.route("<string:printer_id>", methods=["PATCH"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def update_printer(printer_id):
    data = request.get_json()

    printers = (
        Printer.select(Printer, PrinterLabel, PrinterType)
        .where(Printer.pk == printer_id)
        .join(PrinterLabel)
        .switch(Printer)
        .join(PrinterType)
    )

    if printers.count() == 1:
        printer = printers[0]
    else:
        return {"message": "printers.printer.update.error"}, HTTP_400_BAD_REQUEST

    if printer:
        label_pk = data.get("label", None)
        ip = data.get("ip", None)

        # CURRENT LABEL UPDATE
        if label_pk:
            if isinstance(label_pk, int) and label_pk >= 0:
                try:
                    printer.current_label_id = PrinterLabel.get_by_id(label_pk)
                    printer.save()

                except (ValueError, AttributeError, DoesNotExist) as e:
                    return {"message": str(e)}, HTTP_500_INTERNAL_SERVER_ERROR
            else:
                return {"message": "printers.printer.label.update.error"}, HTTP_400_BAD_REQUEST

        # PRINT IP ADDRESS UPDATE
        if ip:
            if not re.match(r"^tcp://(?:[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]{1,5}$", ip):
                return {"message": "printers.printer.ip.invalid"}, HTTP_400_BAD_REQUEST
            try:
                printer.address = ip
                printer.save()
            except (ValueError, AttributeError, DoesNotExist) as e:
                return {"message": str(e)}, HTTP_500_INTERNAL_SERVER_ERROR

    log_printers(session["username"], "update_printer", f"Updated printer (pk:{printer.pk})")

    return _printer_to_dict(printer), HTTP_200_OK


@printer_blueprint.route("command/<string:command_id>", methods=["PATCH"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def update_command(command_id):
    data = request.get_json()
    new_code: str = data.get("command_code", None)
    new_dict = data.get("command_dict", None)

    if new_code is None or new_dict is None:
        return {}, HTTP_400_BAD_REQUEST

    cmd_label = PrinterCommandLabel.get_or_none(PrinterCommandLabel.pk == command_id)

    if cmd_label:
        # ZPL/EPL Validation?
        cmd_label.print_code = new_code

        # JSON Validation
        try:
            json.loads(new_dict)
            cmd_label.print_dict = new_dict
        except json.JSONDecodeError as e:
            return {
                "message": "printers.command.update.dict.error",
                "args": str(e),
            }, HTTP_400_BAD_REQUEST

        cmd_label.save()

        log_printers(session["username"], "update_command", f"Updated Cmd_Label (pk:{cmd_label.pk})")
        return {}, HTTP_200_OK

    return {}, HTTP_500_INTERNAL_SERVER_ERROR


@printer_blueprint.route("label", methods=["PUT"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def create_cmd_label():
    i18n_root = "printers.command.cmd_lbl.create"
    data = request.get_json()
    d_command_pk = data.get("command_pk", None)
    d_label_pk = data.get("label_pk", None)

    if d_command_pk is None or d_label_pk is None:
        return {"message": i18n_root + ".error"}, HTTP_400_BAD_REQUEST

    cmd_lbl = PrinterCommandLabel.get_or_none(
        PrinterCommandLabel.command_id == d_command_pk, PrinterCommandLabel.label_id == d_label_pk
    )

    try:
        if cmd_lbl is not None:
            cmd_lbl.enabled = 1
            cmd_lbl.save()
        else:
            cmd_lbl = PrinterCommandLabel.create(
                command_id=d_command_pk, label_id=d_label_pk, print_code="", print_dict="{}"
            )
    except (IntegrityError, DatabaseError) as e:
        return {"message": i18n_root + ".error", "args": str(e)}, HTTP_500_INTERNAL_SERVER_ERROR

    return {}, HTTP_204_NO_CONTENT


@printer_blueprint.route("label/<string:cmd_label_pk>", methods=["DELETE"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def delete_cmd_label(cmd_label_pk):
    i18n_root = "printers.command.cmd_lbl.delete"

    if cmd_label_pk == "":
        return {"message": i18n_root + ".error"}, HTTP_400_BAD_REQUEST

    try:
        cmd_lbl = PrinterCommandLabel.get_by_id(cmd_label_pk)
    except DoesNotExist:
        return {"message": i18n_root + ".notfound"}, HTTP_400_BAD_REQUEST

    try:
        # Soft delete only, we want to keep the ZPL&Json
        cmd_lbl.enabled = 0
        cmd_lbl.save()
    except (IntegrityError, DatabaseError) as e:
        return {"message": i18n_root + ".error", "args": str(e)}, HTTP_500_INTERNAL_SERVER_ERROR

    return {}, HTTP_204_NO_CONTENT


@printer_blueprint.route("<int:mag_pk>/<int:printer_pk>", methods=["POST"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def add_printer_to_magasin(mag_pk, printer_pk):
    data = request.get_json()
    printerData = data.get("printer", None)
    is_primary = printerData.get("primary", 0) if printerData else 0

    if printerData is None:
        logger.info("Printer : Update printer/mag without data -> primary set to 0")

    if not mag_pk or printer_pk is None:
        return {"message": "printers.magasin.add.missing_fields"}, HTTP_400_BAD_REQUEST

    try:
        magasin = Magasin.get_or_none(Magasin.pk == mag_pk)
        if not magasin:
            return {"message": "printers.magasin.add.invalid_magasin"}, HTTP_400_BAD_REQUEST

        printer = Printer.get_or_none(Printer.pk == printer_pk)
        if not printer:
            return {"message": "printers.magasin.add.invalid_printer"}, HTTP_400_BAD_REQUEST

        existing_link = PrinterMag.get_or_none(
            (PrinterMag.mag_id == magasin.pk) & (PrinterMag.printer_id == printer.pk)
        )

        if existing_link:
            if existing_link.primary != is_primary:
                existing_link.primary = is_primary
                existing_link.save()

            return {"message": "printers.magasin.add.link_exists"}, HTTP_200_OK

        else:
            printer_mag = PrinterMag.create(printer_id=printer.pk, mag_id=magasin.pk, primary=is_primary)

            log_printers(
                session["username"],
                "add_printer_to_magasin",
                f"Added printer (pk:{printer.pk}) to magasin {mag_pk}",
            )

        return {"printer_mag_id": printer_mag.pk}, HTTP_200_OK

    except Exception as e:
        logger.error(f"Error adding printer to magasin: {str(e)}")
        return {"message": "printers.magasin.add.error"}, HTTP_500_INTERNAL_SERVER_ERROR


@printer_blueprint.route("<int:mag_pk>/<int:printer_pk>", methods=["DELETE"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canEdit=True)
def remove_printer_from_magasin(mag_pk, printer_pk):
    if not mag_pk or printer_pk is None:
        return {"message": "printers.magasin.remove.missing_fields"}, HTTP_400_BAD_REQUEST

    try:
        magasin = Magasin.get_or_none(Magasin.pk == mag_pk)
        if not magasin:
            return {"message": "printers.magasin.remove.invalid_magasin"}, HTTP_400_BAD_REQUEST

        printer = Printer.get_or_none(Printer.pk == printer_pk)
        if not printer:
            return {"message": "printers.magasin.remove.invalid_printer"}, HTTP_400_BAD_REQUEST

        link = PrinterMag.get_or_none((PrinterMag.mag_id == magasin.pk) & (PrinterMag.printer_id == printer.pk))

        if not link:
            return {"message": "printers.magasin.remove.link_not_found"}, HTTP_404_NOT_FOUND

        link.delete_instance()

        log_printers(
            session["username"],
            "remove_printer_from_magasin",
            f"Removed printer (pk:{printer.pk}) from magasin {mag_pk}",
        )

        return {}, HTTP_200_OK

    except Exception as e:
        logger.error(f"Error removing printer from magasin: {str(e)}")
        return {"message": "printers.magasin.remove.error"}, HTTP_500_INTERNAL_SERVER_ERROR


@printer_blueprint.route("preview", methods=["POST"])
@jwt_required()
@mustHaveRights(RESSOURCE_NAME, canView=True)
def preview_label():
    data = request.get_json()
    try:
        front_zpl = data.get("front_zpl")
        front_dict = data.get("front_dict")

        rendered_zpl = render_template(front_zpl, {}, json.loads(front_dict))

        zpl_base64 = b64encode(rendered_zpl.encode("utf-8"))
    except Exception as e:
        logger.error(f"Error fetching label or command: {str(e)}")
        return {"message": "printers.label.preview.error"}, HTTP_500_INTERNAL_SERVER_ERROR

    return {"zpl": str(zpl_base64, "utf-8")}, HTTP_200_OK


def log_printers(username: str, action: str, message: str):
    """
    Add new log for Printers

    :param username: User made the action to log
    :param action:
    :param message: message to log
    """
    logger.info("Printers[%s](%s)): %s", action, username, message)
    wlog = WebLogActions()
    wlog.chrono = datetime.now()
    wlog.username = username
    wlog.equipement_type = ""
    wlog.action = action
    wlog.message = message
    wlog.save()
