# Copyright (c) 2022 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
# Modified by BigRep GmbH 2024
import json
from json import JSONDecodeError
from time import time
from io import StringIO
from typing import Dict, List, Optional, Set, Callable, Type, TypeVar, Union, Tuple, Any, cast

from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt6.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QLabel

from UM import i18nCatalog
from UM.Logger import Logger  # To log errors talking to the API.
from UM.OutputDevice import OutputDeviceError
from UM.TaskManagement.HttpRequestManager import HttpRequestManager

from cura.API import Account
from cura.CuraApplication import CuraApplication

from .BaseModel import BaseModel
from .BigRepConnectError import BigRepConnectError
from .BigRepConnectOutputDeviceResponse import BigRepConnectOutputDeviceResponse

I18N_CATALOG = i18nCatalog("cura")

BigRepConnectApiClientModel = TypeVar("BigRepConnectApiClientModel", bound=BaseModel)
"""The generic type variable used to document the methods below."""

class BigRepConnectNameDialog(QDialog):
    def __init__(self, default_name: str, parent=None):
        super().__init__(parent)

        self.setWindowTitle("Enter a file name")

        # Widgets
        self.name_input = QLineEdit()
        self.confirm_button = QPushButton("Confirm")
        self.cancel_button = QPushButton("Cancel")

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.name_input)
        layout.addWidget(self.confirm_button)
        layout.addWidget(self.cancel_button)
        self.setLayout(layout)

        self.name_input.setPlaceholderText(default_name)

        # Signals and Slots
        self.confirm_button.clicked.connect(self.accept)  # Triggers the dialog to return the name
        self.cancel_button.clicked.connect(self.reject)  # Triggers the dialog exit without returning

class BigRepConnectMessageDialog(QDialog):
    def __init__(self, message, parent=None):
        super().__init__(parent)

        self.setWindowTitle("Message")

        # Message Label
        self.message_label = QLabel(message)

        # Close Button
        self.close_button = QPushButton("Close")
        self.close_button.clicked.connect(self.close)  # Close the dialog

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.message_label)
        layout.addWidget(self.close_button)
        self.setLayout(layout)

class BigRepConnectApiClient():
    
    _anti_gc_callbacks = []  # type: List[Callable[[Any], None]]
    
    def __init__(self, app: CuraApplication, on_error: Callable[[List[BigRepConnectError]], None]) -> None:
        super().__init__()
        self._on_error = on_error
        self._app = app
        self._account = app.getCuraAPI().account
        self._http = HttpRequestManager.getInstance()

    @property
    def account(self) -> Account:
        """Gets the account used for the API."""
        return self._account

    def getConnectOutputDevices(self, on_finished: Callable, on_failed: Callable) -> None: # TODO
        """Retrieves all the clusters for the user that is currently logged in.
        """
        if self._account._authorization_service._user_profile is None:
            return

        access_token = self._account._authorization_service.getAccessToken()
        user_id = self._account._authorization_service._user_profile.user_id

        headers = {
            "Authorization": f"Bearer {access_token}"
        }

        service_platform_api_root_url = self._app.bigrepServicePlatformApiRootUrl() 
        printer_url = f'{service_platform_api_root_url}/ups/api/v1/users/{user_id}/printers'

        self._http.get(
            printer_url,
            headers_dict = headers,
            callback = self._parseCallback(on_finished, BigRepConnectOutputDeviceResponse, on_failed),
            error_callback = on_failed
        )

    @staticmethod
    def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]:
        """Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.

        :param reply: The reply from the server.
        :return: A tuple with a status code and a dictionary.
        """

        status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
        try:
            response = bytes(reply.readAll()).decode()
            return status_code, json.loads(response)
        except (UnicodeDecodeError, JSONDecodeError, ValueError) as err:
            error = BigRepConnectError(code=type(err).__name__, title=str(err), http_code=str(status_code),
                               id=str(time()), http_status="500")
            Logger.logException("e", "Could not parse the stardust response: %s", error.toDict())
            return status_code, {"errors": [error.toDict()]}

    def _parseResponse(self, response: Dict[str, Any], on_finished: Union[Callable[[BigRepConnectApiClientModel], Any],
                        Callable[[List[BigRepConnectApiClientModel]], Any]], model_class: Type[BigRepConnectApiClientModel]) -> None:
        """Parses the given response and calls the correct callback depending on the result.

        :param response: The response from the server, after being converted to a dict.
        :param on_finished: The callback in case the response is successful.
        :param model_class: The type of the model to convert the response to. It may either be a single record or a list.
        """

        results = [model_class(c) for c in response]  # type: List[BigRepConnectApiClientModel]
        on_finished_list = cast(Callable[[List[BigRepConnectApiClientModel]], Any], on_finished)
        on_finished_list(results)

    def _parseCallback(self,
                       on_finished: Union[Callable[[BigRepConnectApiClientModel], Any],
                                          Callable[[List[BigRepConnectApiClientModel]], Any]],
                       model: Type[BigRepConnectApiClientModel],
                       on_failed: Optional[Callable] = None) -> Callable[[QNetworkReply], None]:

        """Creates a callback function so that it includes the parsing of the response into the correct model.

        The callback is added to the 'finished' signal of the reply.
        :param reply: The reply that should be listened to.
        :param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either
        a list or a single item.
        :param model: The type of the model to convert the response to.
        """

        def parse(reply: QNetworkReply) -> None:

            self._anti_gc_callbacks.remove(parse)

            # Don't try to parse the reply if we didn't get one
            if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) is None:
                if on_failed is not None:
                    on_failed()
                return

            status_code, response = self._parseReply(reply)
            if status_code >= 300 and on_failed is not None:
                on_failed()
            else:
                self._parseResponse(response, on_finished, model)

        self._anti_gc_callbacks.append(parse)
        return parse
    
    def requestUpload(self, serial_number: str, on_finished_funct):
        access_token = self._account._authorization_service.getAccessToken()

        headers = {
            "Authorization": f"Bearer {access_token}"
        }

        file_name = 'default.gcode'
        dialog = BigRepConnectNameDialog(default_name=file_name)

        if dialog.exec():
            file_name = dialog.name_input.text()
            if file_name == "":
                file_name = dialog.name_input.placeholderText()
        else:
            raise OutputDeviceError.UserCanceledError()
        
        if not serial_number:
            raise OutputDeviceError.UserCanceledError()

        request_url = f"{self._app.bigrepServicePlatformApiRootUrl()}/tms/api/gcode/upload-url?filename={file_name}.gcode&serialNr={serial_number}"

        HttpRequestManager.getInstance().get(
            request_url,
            headers_dict=headers,
            callback=lambda reply: self._fetchGCodeURL(reply, on_finished_funct),
            error_callback=self._errorCallback
        )

    def _fetchGCodeURL(self, reply: QNetworkReply, on_finished_funct):
        gcode_url = HttpRequestManager.getInstance().readJSON(reply)
        upload_url = gcode_url.get("link")

        gcode_ss = StringIO()

        active_build_plate = self._app.getMultiBuildPlateModel().activeBuildPlate
        scene = self._app.getController().getScene()
        if not hasattr(scene, "gcode_dict"):
            self.setInformation(I18N_CATALOG.i18nc("@warning:status", "Please prepare G-code before exporting."))
            return False
        gcode_dict = getattr(scene, "gcode_dict")
        gcode_list = gcode_dict.get(active_build_plate, None)
        if gcode_list is not None:
            for gcode in gcode_list:
                gcode_ss.write(gcode)

        # Option 2: If QByteArray is expected
        gcode_data_string = gcode_ss.getvalue()
        gcode_data = gcode_data_string.encode()

        on_finished_funct()

        headers = {
            'Content-Type': 'application/octet-stream'
        }

        # Upload the backup to storage.
        HttpRequestManager.getInstance().put(
            upload_url,
            data=gcode_data,
            headers_dict=headers,
            callback=self._uploadFinishedCallback
        )
    
    def _errorCallback(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError"):
        message_dialog = BigRepConnectMessageDialog("Access token outdated. Redo upload.")
        message_dialog.exec()

    def _uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None):
        if not HttpRequestManager.replyIndicatesSuccess(reply, error):
            message_dialog = BigRepConnectMessageDialog("CONNECT upload failed.")
        else:
            message_dialog = BigRepConnectMessageDialog("CONNECT upload successful.")
        message_dialog.exec()