import traceback
import logging
import time
from typing import Protocol, Literal

class RulesWriter(Protocol):
    def validation_pass(self, rule: dict[str, str]) -> None: ...
    def validation_fail(self, rule: dict[str, str], verify: str = "-") -> None: ...
    def validation_error(self, rule: dict[str, str], verify: str = "-") -> None: ...

# =============== GPKG Structure Rules ===============
class GpkgStructureRules:
    def __init__(self, logger: logging.Logger, loop_limit: float, rules_writer: RulesWriter):
        # Logger
        self.logger: logging.Logger = logger
        # Loop limit
        self.loopLimit: float = loop_limit
        # Rules Result Writer
        self.rulesWriter: RulesWriter = rules_writer
        
    # ========== Validation Rules ==========
    def layer_number(self, rule: dict[str, str], 
            documentation_layers_number: int, template_layers_number: int, 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the number of layers

        Args:
            rule (dict[str, str]): The validation rule metadata.
            documentation_layers_number (int): The number of layers found in the documentation GPKG.
            template_layers_number (int): The number of layers found in the template GPKG.
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.

        Returns:
            bool: True if the rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                if not documentation_layers_number >= template_layers_number:
                    self.rulesWriter.validation_fail(rule = rule, verify = "Numarul de straturi din fisierul GPKG este gresit.")
                    return int(rule['tip_alerta_id']) != 1
                else:
                    return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.layer_number.__name__}] "
                f"Failed to verify the number of layers\n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def layer_names(self, rule: dict[str, str], 
            documentation_layer_names: list[str], template_layers_names: list[str], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the layer names.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            documentation_layer_names (list[str]): The names found in the documentation GPKG.
            template_layers_names (list[str]): The names found in the template GPKG
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.

        Returns:
            bool: True if the rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                documentationLayerNamesLowered = {layer.lower(): layer for layer in documentation_layer_names}
                missingLayers: list[str] = []
                
                for templateLayer in template_layers_names:
                    time.sleep(self.loopLimit)
                    if templateLayer not in documentationLayerNamesLowered:
                        missingLayers.append(templateLayer)
                        
                if len(missingLayers) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = f"Verificati documentatia sa contina urmatoarele straturi: {missingLayers}")
                    return int(rule['tip_alerta_id']) != 1
                else:
                    return True
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.layer_names.__name__}] "
                f"Failed to verify the names of layers\n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def layer_geoms_type(self, rule: dict[str, str], 
            documentation_layer_geoms: dict[str, list[str]], template_layer_geoms: dict[str, list[str]], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the layers geom type.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            documentation_layer_geoms (dict[str, list[str]]): The layers - geoms found in the documentation GPKG.
            template_layer_geoms (dict[str, list[str]]): The layers - geoms found in the template GPKG
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.

        Returns:
            bool: True if the rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                
                documentationLayerGeomsLowered: dict[str, str] = {layer.lower(): geom for layer, geom in documentation_layer_geoms.items()}
                failedLayers: list[str] = []
                
                for templateLayer, templateGeom in template_layer_geoms.items():
                    time.sleep(self.loopLimit)
                    if templateLayer in documentationLayerGeomsLowered:
                        if templateGeom != documentationLayerGeomsLowered[templateLayer]:
                            failedLayers.append(templateLayer)
                
                if len(failedLayers) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = f"Verificati geometria straturilor documentatiei sa fie conform OMDPLA 904: {failedLayers}")
                    return int(rule['tip_alerta_id']) != 1
                else:
                    return True
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.layer_geoms_type.__name__}] "
                f"Failed to verify the geom type of the layers\n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def layer_column_number(self, rule: dict[str, str], 
            documentation_layer_column_numbers: dict[str, int], template_layer_column_numbers: dict[str, int], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the layer column number.

        Args:
            rule (dict[str, str]): The validation rule metadata
            documentation_layer_column_numbers (dict[str, int]): The layer - number of columns found in the documentation GPKG.
            template_layer_column_numbers (dict[str, int]): The layer - number of columns found in the template GPKG.
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.

        Returns:
            bool: True if the rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                documentationLayerColumnsNumberLowered: dict[str, str] = {layer.lower(): geom for layer, geom in documentation_layer_column_numbers.items()}
                failedLayers: list[str] = []
                
                for templateLayer, templateColumnNumber in template_layer_column_numbers.items():
                    time.sleep(self.loopLimit)
                    if templateLayer in documentationLayerColumnsNumberLowered:
                        if not templateColumnNumber <= documentationLayerColumnsNumberLowered[templateLayer]:
                            failedLayers.append(templateLayer)
                            
                if len(failedLayers) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = f"Verificati numarul de coloane din straturile documentatiei sa fie conform OMDPLA 904: {failedLayers}")
                    return int(rule['tip_alerta_id']) != 1
                else:
                    return True
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.layer_column_number.__name__}] "
                f"Failed to verify the number of columns\n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def layer_column_name(self, rule: dict[str, str], 
            documentation_layer_column_names: dict[str, list[str]], template_layer_column_names: dict[str, list[str]], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the names of the columns.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            documentation_layer_column_names (dict[str, list[str]]): The layer - column names from the documentation GPKG.
            template_layer_column_names (dict[str, list[str]]): The layer - column names from the template GPKG.
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.

        Returns:
            bool: True if the rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                documentationLayerColumnsNamesLowered: dict[str, str] = {
                    layer.lower(): [column.lower() for column in columns] 
                    for layer, columns in documentation_layer_column_names.items()}
                failedLayers: dict[str, list[str]] = {}
                
                for templateLayer, templateColumnsNames in template_layer_column_names.items():
                    time.sleep(self.loopLimit)
                    failedColumns: list[str] = []
                    if templateLayer in documentationLayerColumnsNamesLowered:
                        documentationLayerColumnsNames = documentationLayerColumnsNamesLowered[templateLayer]
                        for templateColumnName in templateColumnsNames:
                            time.sleep(self.loopLimit)
                            if templateColumnName not in documentationLayerColumnsNames:
                                failedColumns.append(templateColumnName)
                    
                    if len(failedColumns) != 0:
                        failedLayers[templateLayer] = failedColumns
                
                if len(failedLayers) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = f"Verificati numele coloanelor din straturile documentatiei: {failedLayers}")
                    return int(rule['tip_alerta_id']) != 1
                else:
                    return True
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.layer_column_name.__name__}] "
                f"Failed to verify the name of columns\n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
        
    def layer_column_data_type(self, rule: dict[str, str], 
            documentation_layer_column_data_type: dict[str, dict[str, str]], template_layer_column_data_type: dict[str, dict[str, str]], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                documentationLayerColumnsNamesLowered: dict[str, str] = {
                    layer.lower(): {column.lower() : dataType for column, dataType in columnDataType.items()} 
                    for layer, columnDataType in documentation_layer_column_data_type.items()}
                
                failedLayerColumns: dict[str, list[str]] = {}
                
                for templateLayer, TemplateColumnDataType in template_layer_column_data_type.items():
                    time.sleep(self.loopLimit)
                    if templateLayer in documentationLayerColumnsNamesLowered:
                        documentationColumnDataType: dict[str, str] = documentationLayerColumnsNamesLowered[templateLayer]
                        failedColumns: list[str] = []
                        for templateColumn, templateDataType in TemplateColumnDataType.items():
                            time.sleep(self.loopLimit)
                            if templateColumn in documentationColumnDataType:
                                columnDataType = documentationColumnDataType[templateColumn]
                                if columnDataType != templateDataType:
                                    failedColumns.append(templateColumn)
                        
                        if len(failedColumns) != 0:
                            failedLayerColumns[templateLayer] = failedColumns
                
                if len(failedLayerColumns) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = f"Verificati tipul de date al coloanelor din straturile documentatiei sa fie conform OMDPLA 904: {failedLayerColumns}")
                    return int(rule['tip_alerta_id']) != 1
                else:
                    return True
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.layer_column_data_type.__name__}] "
                f"Failed to verify the data type of columns\n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    # ======================================
# ====================================================