import logging
import socket
import time
from functools import wraps

from median.models import Compteur
from median.database import mysql_db
from median.views import RawConfig
from common.exception import PrinterException, PrinterTimeoutException, PrinterSocketErrorException
from common.exception import WebPermissionException
from common.status import HTTP_403_FORBIDDEN
from flask import session

from urllib.parse import urlparse
from pathlib import Path
import warnings

logger = logging.getLogger("median.webserver")
data_dir = Path(__file__).resolve().parent

wake_commands = {
    "empty_label": "^XA^XZ",  # ZPL empty label
    "form_feed": "\x1b\x0c",  # ESC + FF (AJA: Might block the printing! 25-03-13)
    "initialize": "\x1b@",  # ESC + @
    "reset": "\x1b\x1d\x0f",  # ESC + GS + SI
}


def Success(message="", id=None, **kwargs):
    """Format a JSON response"""
    res = {
        "id": id,
        "message": message,
    }
    for arg in kwargs:
        res[arg] = kwargs[arg]

    return res


def compute_checksum(number):
    """Calculate the EAN check digit for 13-digit numbers. The number passed
    should not have the check bit included."""
    if len(number) != 12:
        raise Exception("Invalid length")

    checksum_digit = str((10 - sum((3, 1)[i % 2] * int(n) for i, n in enumerate(reversed(number)))) % 10)

    return str(number) + checksum_digit


def send_to_printer(
    printer_address: str = None,
    zpl_content: str = None,
    socket_timeout: float = 60,
    wakeup_printer: bool = True,
    with_status: bool = False,
):
    """
    Send the content to the printer

    :param printer_address: adresse of the printer asan URI (eg: tcp://127.0.0.1:9100/)
    :param printer_content: context as a string value
    :param socket_timeout: timeout before returning an exception (60 seconds by default)
    """
    logger.info(f"Sending print job to {printer_address}")
    logger.debug(f"ZPL content: {zpl_content}")

    # TODO: Change the address to start with tcp://

    try:
        printer_url = urlparse(printer_address)
        # if printer_url.scheme != "zebra":
        #     raise AttributeError({"message": "Invalid printer address format"})

        host_port = printer_url.netloc.split(":")
        if len(host_port) != 2:
            raise AttributeError({"message": "Missing port in printer address"})

        ip = host_port[0]
        port = int(host_port[1])

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(socket_timeout)
        s.connect((ip, port))

        if with_status:
            # First check printer status
            status_cmd = "~HS\r\n"
            s.send(status_cmd.encode())

            try:
                status = s.recv(1024)
                logger.info(f"Printer status: {status}")
            except socket.timeout:
                logger.warning("No status response received")

            # Short pause before sending the print job
            time.sleep(0.2)

        if wakeup_printer:
            wake_command = wake_commands["empty_label"] + "\r\n"
            zpl_content = wake_command + zpl_content

        s.send(zpl_content.encode())
        s.close()

    except socket.timeout as e:
        logger.error(f"Connection timeout to printer at {printer_address}")
        logger.error(str(e))
        raise PrinterTimeoutException("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 PrinterSocketErrorException("Printer connection error" + str(e))
    except Exception as e:
        logger.error(f"Printing error on {printer_address}")
        logger.error(str(e))
        raise PrinterException("Error to send label to printer configuration!" + str(e))
    return True


def get_counter(count_name=None):
    """Retrieve a counter and increment it

    .. deprecated::
        This function is deprecated. Use get_counter from median.utils instead.
    """
    warnings.warn(
        "get_counter is deprecated. Use get_counter from median.utils instead.", DeprecationWarning, stacklevel=2
    )
    with mysql_db.atomic():
        cpt = Compteur.select().where(Compteur.cle == count_name).for_update().get()
        result = cpt.val
        cpt.val = cpt.val + 1
        cpt.save()
    return result


def is_multiple(value: int, divider: int) -> bool:
    """
    Check if value is a multiple of divider

    :param value: Value to check
    :param divider: miltiplication parameters
    :returns: True if multiple of False (0 also return false)
    """
    if divider == 0:
        return False

    if value % divider == 0:
        return True
    else:
        return False


def test_printer_connection(printer_address: str, timeout: float = 5.0) -> dict:
    """
    Test if a printer is connected and can receive commands

    :param printer_address: Address of the printer as a URI (eg: zebra://127.0.0.1:9100/)
    :param timeout: Connection timeout in seconds
    :returns: Dictionary with connection status and optional error message
    """
    logger.info(f"Testing printer connection to {printer_address}")

    try:
        printer_url = urlparse(printer_address)
        allowed_schemes = ["tcp", "zebra"]
        if printer_url.scheme not in allowed_schemes:
            schemes_display = ", ".join(f"{scheme}://" for scheme in allowed_schemes)
            return {
                "connected": False,
                "message": f"Invalid printer address format - must use one of: {schemes_display}",
            }

        host_port = printer_url.netloc.split(":")
        if len(host_port) != 2:
            return {"connected": False, "message": "Missing port in printer address"}

        ip = host_port[0]
        port = int(host_port[1])

        # Try to establish a socket connection
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        s.connect((ip, port))

        # Check printer status by sending a status request command
        status_cmd = "~HS\r\n"
        s.send(status_cmd.encode())

        try:
            status = s.recv(1024)
            logger.debug(f"Printer status response: {status}")
        except socket.timeout:
            logger.warning("No status response received from printer")
            # Even if we don't get a status response, if the connection worked, the printer is likely online

        # Send a wake command to ensure printer can receive data
        wake_command = wake_commands["empty_label"] + "\r\n"
        s.send(wake_command.encode())

        s.close()
        return {"connected": True, "message": "Printer is connected and responding"}

    except socket.timeout:
        logger.error(f"Connection timeout to printer at {printer_address}")
        raise PrinterTimeoutException("Printer connection timeout")
    except socket.error as e:
        logger.error(f"Socket error when connecting to printer: {str(e)}")
        raise PrinterSocketErrorException(f"Printer connection error: {str(e)}")
    except Exception as e:
        logger.error(f"Error testing printer connection to {printer_address}: {str(e)}")
        raise PrinterException(f"Error testing printer connection: {str(e)}")


def zero_padding(value: int, number_of_zero: int) -> str:
    return str(value).zfill(number_of_zero)


def get_config_global_or_local(poste: str, prop: str) -> str:
    """
    Retrieve the configuration parameters for hte poste, if not found we return the
    gloabl paramter, otherwise we return and empty string
    """
    cfg_value = RawConfig(poste).read(param=prop)
    if cfg_value is None:
        cfg_value = RawConfig().read(param=prop)

    return cfg_value and cfg_value.value or ""


def read_printer_template(filename: str = None) -> str:
    """Read the template name path and return the content"""
    logger.info("Read %s template file" % filename)
    template_content = ""
    with open(data_dir / "data" / "riedl" / filename, "r", encoding="utf-8") as f:
        template_content = f.read()
    return template_content


def convert_str_date(source: str, src_pattern: str, dest_pattern: str):
    """
    Convert a date string from one format to another.

    :param source: The date string to convert
    :param src_pattern: The format pattern of the source date string
    :param dest_pattern: The desired format pattern for the output date string
    :returns: The converted date string in the destination format, or empty string if conversion fails
    """
    result = ""
    try:
        result = time.strftime(dest_pattern, time.strptime(source, src_pattern))
    except Exception as e:
        logger.error(f"Error when convert date {source} ({src_pattern} -> {dest_pattern})")
        logger.error(str(e))
    return result


def mustHaveRights(profile, canView=None, canEdit=None, canCreate=None, canDelete=None):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if canView is None and canEdit is None:
                raise AttributeError("mustHaveRights is missing arguments")

            rights = session.get("rights", {})

            has_edit_rights = rights.get(profile, {}).get("edit", 0) == 1 if canEdit else None
            has_view_rights = rights.get(profile, {}).get("visu", 0) == 1 if canView else None

            if (canView and not has_view_rights) or (canEdit and not has_edit_rights):
                username = session.get("username", "Unknown")
                user_id = session.get("user_id", "Unknown")

                if canEdit and not has_edit_rights:
                    verb = "edit"
                elif canView and not has_view_rights:
                    verb = "view"

                logger.warning(
                    f"User {username} (ID: {user_id}) attempted to {verb} without sufficient "
                    f"rights for profile {profile}."
                )
                raise WebPermissionException(
                    "You do not have view or edit rights for this profile.", status_code=HTTP_403_FORBIDDEN
                )

            return func(*args, **kwargs)

        return wrapper

    return decorator
