from datetime import datetime


def applyExclusions(items, datasetTagpath):
	"""
		Sorts a list of tag dictionary objects.
		
		This function assumes that the UDT structure has a '{rootPath}/Manual Exclusions/{Info|Config|Meta}'. We fail gracefully if not.
		
		Args:
			items (List[Dict]): A list of tag dictionaries with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...].
			
			datasetTagpath (String):  A tagpath to an L4 UDT Category, i.e "[PHXA2_IG_COMN]System/Device Datasets/RTU/Info".
			
			Returns:
			List[Dict]: A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...].
	"""
	
	# parse datasetTagpath
	rootPath = datasetTagpath[:datasetTagpath.rindex('/')] # "[PHXA2_IG_COMN]System/Device Datasets/RTU"
	category = datasetTagpath[datasetTagpath.rindex('/')+1:] # "Info"
	exclusionPath = '{rootPath}/Manual Exclusions/{category}'.format(rootPath=rootPath, category=category)
	logger = system.util.getLogger("Device Dataset/itemsExcluded()")
	res = []
	
	# Read sort order, convert from unicode to string, and return unchanged items if empty.
	data = system.tag.readBlocking(exclusionPath)[0]
	excludedTags = list(data.value) if (data and data.value is not None) else []
	excludedTags = [str(tag) for tag in excludedTags]
	if len(excludedTags) == 0: return items
	
	return list(filter(lambda x: str(x.get('tagName')) not in excludedTags, items))


def getTagInstances(deviceDataset, tagPath, folder=""):
    """
        Iterates through a DeviceDataset and returns instances for a Tag Info FlexRepeater.
        This function is called by the 'instances' binding in the L4 /Info views.

        Args:
            deviceDataset (Document): A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...].
                    These are stored in [PHXA2_IG_COMN]System/Device Datasets.
    
            tagPath (String): A tagpath to the root of a device, i.e "[PHXA2_IG_COMN]RM2505/PHXA2_RM2505_FCU_4".
    
            folder (String): The folder the tags live in, i.e "/Config" or "/Meta".
    
        Returns:
            List[List]: instances to be used in a /Tag Info FlexRepeater.
    """

    # Append /Config or /Meta to the tagpath.
    tagPath += folder

    res = []

    for jsonItem in list(deviceDataset):

        tagName = jsonItem.get("tagName", "N/A")
        dataType = jsonItem.get("engUnit", "N/A")
        qualifiedTagpath = "{tagPath}/{tagName}".format(
            tagPath=tagPath, tagName=tagName
        )

        # Create a FlexRepeater instance with the extracted parameters.
        res.append({"name": tagName, "dataType": dataType, "tagPath": qualifiedTagpath})

    return res


def getSortedTagDictionaries(items, datasetTagpath):
    """
        Sorts a list of tag dictionary objects.
        
        This function assumes that the UDT structure has a '{rootPath}/Configuration/{Info|Config|Meta}SortOrder'. We fail gracefully if not.
        
        Args:
            items (List[Dict]): A list of tag dictionaries with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...].
            
            datasetTagpath (String):  A tagpath to an L4 UDT Category, i.e "[PHXA2_IG_COMN]System/Device Datasets/RTU/Info".
            
        Returns:
            List[Dict]: A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...].
    """
    
    # parse datasetTagpath
    rootPath = datasetTagpath[:datasetTagpath.rindex('/')] # "[PHXA2_IG_COMN]System/Device Datasets/RTU"
    category = datasetTagpath[datasetTagpath.rindex('/')+1:] # "Info"
    sortOrderPath = '{rootPath}/Sorting Orders/{category}'.format(rootPath=rootPath, category=category)
    
    # Read sort order, convert from unicode to string, and return unchanged items if empty.
    data = system.tag.readBlocking(sortOrderPath)[0]
    sortingOrder = list(data.value) if (data and data.value is not None) else []
    sortingOrder = [str(item) for item in sortingOrder]
    if len(sortingOrder) == 0: return items
    
    # Sort items based on tag names in sorting order.
    sortedItems = sorted(
        items,
        key=lambda x: (
            sortingOrder.index(x['tagName']) if x['tagName'] in sortingOrder else len(sortingOrder)  # append unsorted tags
        )
    )
    
    return sortedItems
    
    
def getDiff(previousData, currentData, folder=""):
    """
        Get difference between data before generation and after. This is meant to track
        manual additions being lost (functionality which will be improved soon^tm).
    
        Args:
            previousData (List[Dict]): A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...].
        
            currentData (List[Dict]): A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...].
        
        Returns:
            String: A timestamped log message detailing tags that existed in previousData and not in currentData.
        
    """
    logMessage = ""
    
    for item in previousInfo:
        tagName = item.get("tagName", "")
        if tagName is "":
            continue
    
        print(
            "{} in {}? {}.".format(tagName, currentData, tagName in currentData)
        )
        logger.info(
            "{} in {}? {}.".format(tagName, currentData, tagName in currentData)
        )
    
        if tagName not in currentData:
            logMessage += "\n{}: {} was removed in {} update.".format(timestamp, folder, item.tagName)
    
    return logMessage
    
    
def generateDeviceDataset(deviceTagpath, datasetTagpath, writeToSystem=True, debug=False):
    """
        Create a new device dataset.
    
        Args:
            deviceTagpath (String): A tagpath to the root of a device, i.e "[PHXA2_IG_COMN]Roof/PHXA2_ROOF_RTU_1".
        
            datasetTagpath (String): A tagpath to an L4 UDT category, i.e "[PHXA2_IG_COMN]System/Device Datasets/RTU/{Info|Config|Meta|Alarms}".
        
            writeToSystem (Boolean)[Optional]: If specified as false, returns the result instead of writing it
                to datasetTagpath in the system.
        
        Returns:
            List[Dict]: A device specific dataset with the structure: [{tagName: 'Hello World', 'engUnit': 'String'}, ...].
            
    """
    import json
    
    allTagsOutput = []
    #deviceName = deviceTagpath[deviceTagpath.index('/')+1:]
    if '/' in deviceTagpath:
    	deviceName = deviceTagpath[deviceTagpath.rindex('/')+1:]
    else:
    	deviceName = deviceTagpath
    logger = system.util.getLogger('{} Dataset'.format(deviceName))
    
    
    # Fetch tag configurations and sort by name
    nodes = system.tag.getConfiguration(deviceTagpath, True)
    for item in nodes:
        if "tags" in item:
            sortedTags = sorted(item["tags"], key=lambda x: x["name"])
            sortedTags = sorted(
                sortedTags,
                key=lambda x: (
                    x.get("engUnit", "misc")
                    if x.get("engUnit", "misc") != ""
                    else "misc"
                ),
            )
    
            for tagConfig in sortedTags:
                if str(tagConfig["tagType"]) == "AtomicTag":
                    name = tagConfig.get("name", "")
                    dataType = tagConfig.get("dataType", "")
                    tagPath = deviceTagpath + "/" + name
                    instance = {
                        "instanceStyle": {"classes": ""},
                        "instancePosition": {},
                        "name": name,
                        "dataType": dataType,
                    }
    
                    # Group based on name containing 'Power', 'Current', or 'Voltage'
                    allTagsOutput.append(instance)
    
    if debug:
        logger.info('allTagsOutput: {}'.format(allTagsOutput))
        print('allTagsOutput: {}'.format(allTagsOutput))
    
    ### FlexRepeater Binding
    bindingOutput = []
    for tag in allTagsOutput:
        tagName = tag["name"]
        tag["tagPath"] = tagPath + "/" + tagName
        bindingOutput.append(tag)
    
    ### Logan Script
    res = []
    for item in bindingOutput:
        tagPath = item["tagPath"]
        tagNameIndex = tagPath.rindex("/") + 1
        res.append({"tagName": tagPath[tagNameIndex:], "engUnit": item["dataType"]})
    
    ### Excluding
    excludedTagDictionaries = applyExclusions(items=res, datasetTagpath=datasetTagpath)
    
    ### Sorting
    sortedTagDictionaries = getSortedTagDictionaries(items=excludedTagDictionaries, datasetTagpath=datasetTagpath)
    
    if debug:
        logger.info('sortedTagDictionaries: {}'.format(sortedTagDictionaries))
        logger.info('writing to: {}'.format(datasetTagpath))
        print('sortedTagDictionaries: {}'.format(sortedTagDictionaries))
        print('writing to: {}'.format(datasetTagpath))
    
    if writeToSystem:
        system.tag.writeBlocking(datasetTagpath, [sortedTagDictionaries])
    
    return sortedTagDictionaries


def generateDeviceDatasetTrends(deviceTagpath, datasetTagpath, writeToSystem=True):
    return {}


def generateL4(datasetTagpath, writeToSystem=True, debug=False):
    """
        Highest level function. Can be called by users.

        Generate Info, Config, Meta, and Trends in a L4 UDT. Requires a UDT to exist with a reference tagpath specified.

        This function is called by _Manual Refresh in the L4 UDT.
        
        Args:
            datasetTagpath (String): A tagpath to an L4 UDT, i.e "[PHXA2_IG_COMN]System/Device Datasets/RTU".
        
            writeToSystem (Boolean)[Optional]: If specified as false, returns the result instead of writing it
                to datasetTagpath in the system.
               
            debug (Boolean)[Optional]: Enable verbose logging.
        
        Returns:
            Dict{key: val}: A dictionary containing each dataset. Keys are ['info','config','meta','trends'].
        
    """
    timestamp = datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")  # Returns "[2025-08-08 08:28:32]".
    logger = system.util.getLogger("Device Dataset")
    logMessage = ""
    
    if debug:
        logger.info("Generating L4 for {}.".format(datasetTagpath))
        print(datasetTagpath)
    
    # Get all relevant UDT data.
    data = system.tag.readBlocking(
        [
            datasetTagpath + "/Info",
            datasetTagpath + "/Config",
            datasetTagpath + "/Meta",
            datasetTagpath + "/Trends",
            datasetTagpath + "/Alarms",
            datasetTagpath + "/_Reference Tagpath",
        ]
    )
    
    # Catch bad tagPaths and non-existent data.
    previousInfo = data[0].value if (data and data[0].value is not None) else {}
    previousConfig = data[1].value if (data and data[1].value is not None) else {}
    previousMeta = data[2].value if (data and data[2].value is not None) else {}
    previousTrends = data[3].value if (data and data[3].value is not None) else {}
    previousAlarms = data[4].value if (data and data[4].value is not None) else {}
    referenceTagpath = data[5].value if (data and data[5].value is not None) else {}
    
    # If no reference device is given, the script explodes. We catch it here.
    if referenceTagpath is None or len(referenceTagpath) == 0:
        logger.info(
            "\n{}: ERROR: Please verify that a reference tagpath is provided.".format(timestamp)
        )
        logMessage += (
            "\n{}: ERROR: Please verify that a reference tagpath is provided.".format(timestamp)
        )
    
        system.tag.writeAsync([datasetTagpath + "/_Log Message"], [logMessage])
        return # exit early
    
    if debug:
        logger.info(
            "prevInfo: {}\nprevConfig: {}\n prevTrends: {}\n prevMeta: {}\n refTag: {}".format(
                previousInfo,
                previousConfig,
                previousTrends,
                previousMeta,
                referenceTagpath,
            )
        )
        print(
            "prevInfo: {}\nprevConfig: {}\n prevTrends: {}\n prevMeta: {}\n refTag: {}".format(
                previousInfo,
                previousConfig,
                previousTrends,
                previousMeta,
                referenceTagpath,
            )
        )
    
    # Build the dataset for the Info view. Pull each tagName from the dataset to track which manual additions are lost.
    currentInfo = Global.DeviceDatasets.generateDeviceDataset(
        deviceTagpath="{referenceTagpath}".format(referenceTagpath=referenceTagpath), # DON'T APPEND THIS WITH ANYTHING
        datasetTagpath="{datasetTagpath}/Info".format(datasetTagpath=datasetTagpath),
        writeToSystem=writeToSystem,
    )
    
    # Note manual additions that are lost in the update.
    if debug:
        currentInfoNames = list(map(lambda x: str(x.get("tagName", "N/A")), currentInfo))
        logMessage += Global.DeviceDatasets.getDiff(previousData=previousInfo, currentData=currentInfo, folder="Info")
    
    
    currentConfig = Global.DeviceDatasets.generateDeviceDataset(
        deviceTagpath="{referenceTagpath}/Config".format(referenceTagpath=referenceTagpath),
        datasetTagpath="{datasetTagpath}/Config".format(datasetTagpath=datasetTagpath),
        writeToSystem=writeToSystem,
    )
    
    if debug:
        currentConfigNames = list(map(lambda x: str(x.get("tagName", "N/A")), currentConfig))
        logMessage += Global.DeviceDatasets.getDiff(previousData=previousConfig, currentData=currentConfigNames, folder="Config")
    
    
    currentMeta = Global.DeviceDatasets.generateDeviceDataset(
        deviceTagpath="{referenceTagpath}/Meta".format(referenceTagpath=referenceTagpath),
        datasetTagpath="{datasetTagpath}/Meta".format(datasetTagpath=datasetTagpath),
        writeToSystem=True,
    )
    
    if debug:
        currentMetaNames = list(map(lambda x: str(x.get("tagName", "N/A")), currentMeta))
        logMessage += Global.DeviceDatasets.getDiff(previousData=previousMeta, currentData=currentMetaNames, folder="Meta")
    

    currentAlarms = Global.DeviceDatasets.generateDeviceDataset(
        deviceTagpath="{referenceTagpath}/Alarms".format(referenceTagpath=referenceTagpath),
        datasetTagpath="{datasetTagpath}/Alarms".format(datasetTagpath=datasetTagpath),
        writeToSystem=True,
    )
    
    if debug:
        currentAlarmsNames = list(map(lambda x: str(x.get("tagName", "N/A")), currentAlarms))
        logMessage += Global.DeviceDatasets.getDiff(previousData=previousAlarms, currentData=currentAlarmsNames, folder="Alarms")
            
    
    if len(logMessage) == 0:
        logMessage = "{} Successfully generated.".format(timestamp)
    
    # Reset the _Manual Refresh tag back to False.
    system.tag.writeAsync([datasetTagpath + "/_Manual Refresh"], [False])
    
    # Write the aggregate log message to _Log Message.
    system.tag.writeAsync([datasetTagpath + "/_Log Message"], [logMessage])
    
    return logMessage