import asyncio
from datetime import datetime, timezone
import json
import time
import logging
from pathlib import Path
from typing import Callable

from gateway_core.application.ports.NotificationPort import NotificationPort
from gateway_core.domain.FileSystemData import TransferStatus
from gateway_core.domain.Reboot import RebootDto
from gateway_core.domain.SoftwareUpgrade import SoftwareUpgradeDto, SoftwareUpgradeStatus
from gateway_core.domain.SystemInfoData import SystemInfoDto, PlugType, PowerUnitType
from gateway_core.utils.JsonshemaValidater import jsonSchemaValidationPath
from gateway_transport.http.FileTransferClient import TransferStatusDto
from gateway_transport.mqtt.MqttClientProtocol import MqttClientProtocol
from gateway_integration_deltagrid.data.FileNotifierDto import FileNotifierDto

logger = logging.getLogger(__name__)

class DeltaGridMqttOutboundAdapter(NotificationPort):
    TOPIC_PREFIX = "cpsp/events/"

    def __init__(self, mqttClient: MqttClientProtocol) -> None:
        self._mqtt = mqttClient
        self._matchStateUploadDict = {TransferStatus.TRANSFERRING: "Uploading",
                                      TransferStatus.COMPLETED: "Uploaded",
                                      TransferStatus.FAILED: "UploadFailed",
                                      TransferStatus.IDLE: "Idle"}

        self._matchStateDownloadDict = {TransferStatus.TRANSFERRING: "Downloading",
                                      TransferStatus.COMPLETED: "Downloaded",
                                      TransferStatus.FAILED: "DownloadFailed",
                                      TransferStatus.IDLE: "Idle"}


    async def notifySystemInfo(self, systemInfo: SystemInfoDto) -> None:
        """Notifies the backend about the system information."""
        topic = self.TOPIC_PREFIX + "ChargerSystem"

        matchPlugsToDelta =  {PlugType.CCS: "CCS2", PlugType.CHADEMO: "CHAdeMO", PlugType.AC: "AC_plug"}
        matchPowerUnitTypesToDelta = {PowerUnitType.DELTA_GW: "DELTA_GW",
                                      PowerUnitType.DELTA_GW_EVMA253: "DELTA_GW_EVMA253",
                                      PowerUnitType.DELTA_EVMA253: "DELTA_EVMA253",
                                      PowerUnitType.DELTA_EIPM25KTC: "DELTA_EIPM25KTC",
                                      PowerUnitType.AC: "AC0"}

        components = []

        # ad components Boards
        for board in systemInfo.boardInfos:
            componentBoard = {
                "name": board.type,
                "items": [
                    {
                        "id": 0,
                        "data": {
                            "ECP_FW_Version": board.ecpVersion,
                            "ConfigFile_Version": board.configVersion
                        }
                    }
                ]
            }
            components.append(componentBoard)

        # add component Plug
        plugs = []
        for i, plug in enumerate(systemInfo.plugInfos):
            matchDeltaPlug = matchPlugsToDelta.get(plug.type, "")
            if not matchDeltaPlug:
                # ignore other plugs, e.g. DUMMY Plug
                continue

            plugItem = {
                "id": i,
                "data": {
                    "Type": matchDeltaPlug,
                    "OCPPconnectorId": plug.ocppConnectorId
                }
            }
            plugs.append(plugItem)

        componentPlug = {
            "name": "Plug",
            "items": plugs
        }

        components.append(componentPlug)

        # add component CardReader
        cardReader = []
        for i, cardReaderType in enumerate(systemInfo.cardReader):
            cardReaderItem = {
                "id": i,
                "data": {
                    "Type": cardReaderType
                }
            }
            cardReader.append(cardReaderItem)

        componentCardReader = {
            "name": "CardReader",
            "items": cardReader
        }

        components.append(componentCardReader)

        # add component FiscalModule
        fiscalModules = []
        for i, fmVersion in enumerate(systemInfo.fiscalModule):
            fmItem = {
                "id": i,
                "data": {
                    "FM_FW_Version": fmVersion
                }
            }
            fiscalModules.append(fmItem)

        componentFiscalModule = {
            "name": "FiscalModule",
            "items": fiscalModules
        }

        components.append(componentFiscalModule)

        # add component Router
        routers = []
        for i, routerVersion in enumerate(systemInfo.router):
            routerItem = {
                "id": i,
                "data": {
                    "RT_FW_Version": routerVersion
                }
            }
            routers.append(routerItem)

        componentRouter = {
            "name": "Router",
            "items": routers
        }

        components.append(componentRouter)

        # add component GPS
        componentGPS = {
            "name": "GPS",
            "items": [
                {
                    "id": 0,
                    "data": {
                        "Coordinate": systemInfo.GPS
                    }
                }
            ]
        }

        components.append(componentGPS)

        # add component ServiceSim
        serviceSims = []
        for i, serviceSim in enumerate(systemInfo.simCard):
            siItem = {
                "id": i,
                "data": {
                    "IPv4IP": serviceSim.IPv4IP,
                    "ICCID": serviceSim.ICCID
                }
            }
            serviceSims.append(siItem)

        componentServiceSim = {
            "name": "ServiceSim",
            "items": serviceSims
        }

        components.append(componentServiceSim)

        # add component OCPP
        backends = []
        for i, ocpp in enumerate(systemInfo.ocpp):
            ocppItem = {
                "id": i,
                "data": {
                    "BackendUrl": ocpp.backendUrl,
                    "BoxID": ocpp.boxID,
                    "bEnabled": "TRUE" if ocpp.enabled else "FALSE"
                }
            }
            backends.append(ocppItem)

        componentOCPP = {
            "name": "OCPP",
            "items": backends
        }

        components.append(componentOCPP)

        # add component PowerUnit
        powerUnits = []
        for i, powerUnit in enumerate(systemInfo.powerUnits):
            powerUnitItem = {
                "id": i,
                "data": {
                    "Type": matchPowerUnitTypesToDelta.get(powerUnit.type, "OTHERS"),
                }
            }
            if powerUnit.canId:
                powerUnitItem["data"].update({"canId": ", ".join([f"{id}" for id in powerUnit.canId])})
            powerUnits.append(powerUnitItem)

        componentPowerUnit = {
            "name": "PowerUnit",
            "items": powerUnits
        }

        components.append(componentPowerUnit)

        # add component Grid
        componentGrid = {
            "name": "Grid",
            "items": [
                {
                    "id": 0,
                    "data": {
                        "GridCurrent": str(systemInfo.gridCurrent)
                    }
                }
            ]
        }

        components.append(componentGrid)

        payload = {
                "ChargerSystem": {
                    "ModelName": systemInfo.modelName,
                    "SerialNumber": systemInfo.serialNumber,
                    "JSONDataVersion": systemInfo.jsonShemaVersion,
                    "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
                    "components": components
                }
            }

        logger.info(f"sending ChargerSystem info message: {payload}")

        schemaPath = Path(__file__).parent.parent / "schemas/v1_0/A.1.1.ChargerSystem.schema.json"
        if not jsonSchemaValidationPath(payload, schemaPath):
            logger.error("JSON RPC payload could not be validated by schema: %r", payload)
            return

        await self._mqtt.publish(
            topic,
            json.dumps(payload).encode("utf-8"),
            retain=True,
            qos=1,
        )



    async def notifySoftwareUpgradeStatus(self, dto: SoftwareUpgradeDto, status: SoftwareUpgradeStatus) -> None:
        topic = self.TOPIC_PREFIX + "softwareUpgradeStatus"
        payload = {
            "UpgradeFirmwareStatus": {
                "timestamp": "2025-01-23T05:37:38Z",
                "taskId": dto.taskId,
                "data": {
                    "FirmwareStatus": "Installing",
                    "FirmwareDownloadProgress": 63
                }
            }
        }

        logger.info(f"sending answer for upgradeFW request: {payload}")

        schemaPath = Path(__file__).parent.parent / "schemas/v1_0/A.2.UpgradeFirmwareStatus.schema.json"
        if not jsonSchemaValidationPath(payload, schemaPath):
            logger.error("JSON RPC payload could not be validated by schema: %r", payload)
            return

        await self._mqtt.publish(
            topic,
            json.dumps(payload).encode("utf-8"),
            qos=1,
        )

    async def notifyFileList(self, taskId:str, fileList:list[str]) -> None:
        gatheredFilesList = []
        for i, file in enumerate(fileList, 1):
            gatheredFilesList.append({"id": i, "data": file})

        topic = self.TOPIC_PREFIX + "LogFileListRes"
        payload = {
                  "LogFileListRes": {
                    "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
                    "taskId": taskId,
                    "components": [
                      {
                        "name": "LogFileListInfo",
                        "items": gatheredFilesList
                      }
                    ]
                  }
                }

        if not gatheredFilesList:
            logger.warning("notifyFileList: file list is empty for taskId=%s", taskId)

        schemaPath = Path(__file__).parent.parent / "schemas/v1_0/A.3.2.LogFileListRes.schema.json"
        if not jsonSchemaValidationPath(payload, schemaPath):
            logger.error("notifyFileList: schema validation failed, publishing anyway. payload=%s", payload)

        logger.info(f"sending answer for uploadLogFileList request: {payload}")

        await self._mqtt.publish(topic, json.dumps(payload).encode("utf-8"), qos=1)

    async def notifyLogFileUpload(self, taskId: str, getStatus: Callable[[], TransferStatusDto]) -> None:
        fileNotifierDto = FileNotifierDto(taskId=taskId, topic=self.TOPIC_PREFIX + "UploadLog", method="UploadLog",
                                          statusName="logUploadStatus", shemaFileName="A.3.1.UploadLog.schema.json", progressInterval=10, stateMatchDict=self._matchStateUploadDict)

        await self._notifyFileTransfer(getStatus, fileNotifierDto)


    async def notifyConfigFileUpload(self, taskId: str, getStatus: Callable[[], TransferStatusDto]) -> None:

        fileNotifierDto = FileNotifierDto(taskId=taskId, topic=self.TOPIC_PREFIX + "UploadConfig", method="UploadConfig",
                                          statusName="ConfigUploadStatus", shemaFileName="A.5.2.UploadConfig.schema.json", progressInterval=10, stateMatchDict=self._matchStateUploadDict)

        await self._notifyFileTransfer(getStatus, fileNotifierDto)

    async def notifyConfigFileDownload(self, taskId: str, getStatus: Callable[[], TransferStatusDto]) -> None:

        fileNotifierDto = FileNotifierDto(taskId=taskId, topic=self.TOPIC_PREFIX + "UpgradeConfigStatus", method="UpgradeConfigStatus",
                                          statusName="UpgradeStatus", shemaFileName="A.5.1.UpgradeConfigStatus.schema.json", progressInterval=10, stateMatchDict=self._matchStateDownloadDict)

        await self._notifyFileTransfer(getStatus, fileNotifierDto)

    async def _notifyFileTransfer(self, getStatus: Callable[[], TransferStatusDto], notifierInfo: FileNotifierDto) -> None:

        if not isinstance(notifierInfo, FileNotifierDto):
            logger.error(f"Wrong notifierInfo given ({type(notifierInfo)}), FileNotifierDTO expected!")
            return

        topic = notifierInfo.topic

        uploadDone = asyncio.Event()

        statusTask = asyncio.create_task(self._checkStatus(uploadDone, getStatus, 1))

        while True:
            try:
                await asyncio.wait_for(uploadDone.wait(), notifierInfo.progressInterval)
            except asyncio.exceptions.TimeoutError:
                pass

            status: TransferStatusDto = getStatus()

            deltaStatus = notifierInfo.stateMatchDict[status.state]

            payload = {
                notifierInfo.method: {
                    "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
                    "taskId": notifierInfo.taskId,
                    "data": {
                        notifierInfo.statusName: deltaStatus,
                        "Progress": int(status.progress),
                    }
                }
            }

            logger.info(f"sending answer for uploadLog request: {payload}")

            schemaPath = Path(__file__).parent.parent / "schemas/v1_0" / notifierInfo.shemaFileName
            if not jsonSchemaValidationPath(payload, schemaPath):
                logger.error("JSON RPC payload could not be validated by schema: %r", payload)
                return

            await self._mqtt.publish(
                topic,
                json.dumps(payload).encode("utf-8"),
                qos=1,
            )

            if uploadDone.is_set():
                statusTask.cancel()
                return

    async def notifyUpgradeProgress(self, taskId: str, status: str, progress: int) -> None:
        method = "UpgradeConfigStatus"
        statusName = "UpgradeStatus"
        shemaFileName = "A.5.1.UpgradeConfigStatus.schema.json"
        topic = self.TOPIC_PREFIX + "UpgradeConfigStatus"

        payload = {
            method: {
                "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
                "taskId": taskId,
                "data": {
                    statusName: status,
                    "Progress": int(progress),
                }
            }
        }

        logger.info(f"sending answer for uploadLog request: {payload}")

        schemaPath = Path(__file__).parent.parent / "schemas/v1_0" / shemaFileName
        if not jsonSchemaValidationPath(payload, schemaPath):
            logger.error("JSON RPC payload could not be validated by schema: %r", payload)
            return

        await self._mqtt.publish(
            topic,
            json.dumps(payload).encode("utf-8"),
            qos=1,
        )

    async def _checkStatus(self, uploadDone: asyncio.Event, getStatus: Callable, checkinterval: int):
        while True:
            status: TransferStatusDto = getStatus()

            if status.state in [TransferStatus.COMPLETED, TransferStatus.FAILED]:
                uploadDone.set()

            await asyncio.sleep(checkinterval)

    async def notifyRebootStatus(self, dto: RebootDto, status: str) -> None:
        topic = self.TOPIC_PREFIX + "RebootRes"

        payload = {
            "RebootRes": {
                "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
                "taskId": dto.taskId,
                "data": {
                    "status": status,
                    "type": dto.type.value,
                    "entity": dto.entity.value
                }
            }
        }
        # TODO: ask for this missing schema
        # schemaPath = Path(__file__).parent.parent / "schemas/v1_0/A.10.RebootRes.schema.json"
        # if not jsonSchemaValidationPath(payload, schemaPath):
        #     logger.error("RebootRes payload could not be validated by schema: %r", payload)
        #     return

        await self._mqtt.publish(topic, json.dumps(payload).encode("utf-8"), qos=1)