import os
import gc
import time
import shutil
import requests
import zipfile
import tempfile
import logging
import traceback
from typing import Literal
from osgeo import gdal, ogr


# =============== Validation Rules Utils ===============
class RulesUtils:
    def __init__(self, logger: logging.Logger, loop_limit: float):
        # Logger
        self.logger: logging.Logger = logger
        # For limit 
        self.loopLimit = loop_limit
        
        # Saving Data in memory
        self.zipStructure: dict[str, list[str]] = None
        self.zipData: tempfile.TemporaryDirectory = None
        
        self.gpkgDocumentation: gdal.Dataset = None
        self.gpkgTemplate: gdal.Dataset = None
        
        self.gpkgDocumentationDataStructure: dict[str, int | list[str] | dict[str, str | list[str] | dict[str, str]]] = None
        self.gpkgTemplateDataStructure: dict[str, int | list[str] | dict[str, str | list[str] | dict[str, str]]] = None
        self.gpkgTemplateMetadata: dict[str, list[str] | dict[str, dict[str, str]] | dict[str, str]] = None
        
        self.uatLimitGpkg: gdal.Dataset = None
    
    # ========== Read Zip Archive ==========
    def read_zip_archive(self, 
            zip_file_path: str, 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> None:
        """Read the zip archive and return the names of the files and folder.

        Args:
            zip_file_path (str): The ZIP archive path.
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.
        """
        try:
            with zipfile.ZipFile(zip_file_path, mode='r') as archive:
                fileList = archive.namelist()
            
            # Get the root directories names
            rootDirectories: list[str] = [folder.split('/')[0] for folder in fileList]
            rootDirectories = list(filter(None, dict.fromkeys(rootDirectories)))
            
            # Get sub folders names
            subFolders: list[str] = [folder.split('/')[1] for folder in fileList]
            subFolders = list(filter(None, dict.fromkeys(subFolders)))
            
            # Get the sub_folder/file path
            subParentFolderFile: list[str] = [file.split('/', 1)[1] for file in fileList]
            subParentFolderFile = list(filter(None, dict.fromkeys(subParentFolderFile)))
            
            # Get all the files from the sub folders
            subFoldersFiles: list[str] = [folder.split('/', 2)[-1] for folder in fileList]
            subFoldersFiles = list(filter(None, dict.fromkeys(subFoldersFiles)))
            
            # Get all the pdf files names w/o the avize
            pdfFilesNames: list[str] = [item for item in subFoldersFiles if item.endswith((".pdf", ".PDF")) and not item.startswith("4_") and item != ""]
            # Get all the gpkg files names
            gpkgFilesNames: list[str] = [item for item in subFoldersFiles if item.endswith((".gpkg", ".GPKG")) and item != ""]
            
            # Get the avize pdf files
            if plan_type in ["PUG", "PUZ", "PUD"]:
                avizePdfFiles: list[str] = [item for item in subFoldersFiles if item.startswith("4_") and item != ""]
            elif plan_type in ["PATJ"]:
                avizePdfFiles: list[str] = [item for item in subFoldersFiles if item.startswith("7_") and item != ""]
            
            zipStructureData: dict[str, list[str]] = {
                "RootDirs" : rootDirectories,
                "SubFolders" : subFolders,
                "SubFolderFilesRelation": subParentFolderFile,
                "PdfFiles" : pdfFilesNames,
                "GpkgFiles" : gpkgFilesNames,
                "AvizePdfFiles" : avizePdfFiles}
            
            self.zipStructure = zipStructureData
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.read_zip_archive.__name__}] "
                f"Failed to read the zip archive. \n{traceBack}")
    # ======================================
    
    # ========== Extract Zip Archive ==========
    def extract_zip_archive(self, 
            zip_file_path: str, 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> None:
        """Extracts the ZIP archive in a temporary directory.

        Args:
            zip_file_path (str): The ZIP archive path.
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.
        """
        try:
            # Extract Files
            tempDir = tempfile.mkdtemp()
        
            with zipfile.ZipFile(zip_file_path, 'r') as zipArchive:
                zipArchive.extractall(tempDir, members = zipArchive.namelist())
                    
            self.zipData = tempDir
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.extract_zip_archive.__name__}] "
                f"Failed to extract the zip archive. \n{traceBack}")
    # =========================================
    
    # ========== Load GPKG ==========
    def load_gpkg(self, 
            gpkg_type: Literal["Documentation", "Template"], 
            gpkg_file_path: str | None = None, 
            gpkg_template_url: str | None = None) -> None:
        """Load the gpkg.

        Args:
            gpkg_type (Literal[&quot;Documentation&quot;, &quot;Template&quot;]): The type of gpkg
            gpkg_file_path (str | None, optional): The path of the GPKG file from the ZIP archive. Defaults to None.
            gpkg_template_url (str | None, optional): The url of the template gpkg. Defaults to None.
        """
        try:
            # Read the gpkg from the zip extracted data
            if gpkg_type == "Documentation":
                self.gpkgDocumentation: gdal.Dataset = ogr.Open(gpkg_file_path)
            # Request and save the gpkg in a temp dir
            elif gpkg_type == "Template":
                # Request the gpkg template
                response = requests.get(gpkg_template_url)
                response.raise_for_status()
                
                # Create a temp dir to extract the gpkg template
                tempDir = tempfile.mkdtemp()
                gpkgPathTemplate = os.path.join(tempDir, "template.gpkg")
                with open(gpkgPathTemplate, 'wb') as f:
                    f.write(response.content)
                    
                # read the gpkg template file
                self.gpkgTemplate: gdal.Dataset = ogr.Open(gpkgPathTemplate)
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.load_gpkg.__name__}] "
                f"Failed to load the gpkg. \n{traceBack}")
    # ===============================
    
    # ========== Read GPKG Data ==========
    def read_gpkg_data(self, 
            gpkg_dataset: gdal.Dataset, 
            gpkg_type: Literal["Documentation", "Template"],
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> None:
        """Reads the gpkg data

        Args:
            gpkg_dataset (gdal.Dataset): The gpkg gdal dataset.
            gpkg_type (Literal[&quot;Documentation&quot;, &quot;Template&quot;]): The gpkg type.
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.
        """
        try:
            # Data to get
            gpkgLayersNumber: int = 0
            gpkgLayersNames: list[str] = []
            gpkgLayersGeoms: dict[str, str] = {}
            gpkgLayersColumnsNumber: int = {}
            gpkgLayersColumnsNames: dict[str, list[str]] = {}
            gpkgLayersColumnsDataTypes: dict[str, dict[str, str]] = {}
            
            # Getting the data
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                for i in range(gpkg_dataset.GetLayerCount()):
                    time.sleep(self.loopLimit)
                    # Get the layer
                    gpkgLayer: ogr.Layer = gpkg_dataset.GetLayerByIndex(i)
                    gpkgLayerName: str = gpkgLayer.GetName()
                    if not gpkgLayerName.startswith("_"):
                        # Get the layer name and number
                        gpkgLayersNumber += 1
                        gpkgLayersNames.append(gpkgLayerName.lower())
                        # Get the layer geom
                        gpkgLayerGeomName: str = ogr.GeometryTypeToName(gpkgLayer.GetGeomType())
                        gpkgLayersGeoms[gpkgLayerName] = gpkgLayerGeomName
                        # Get layer column number
                        gpkgLayerDefn: ogr.FeatureDefn = gpkgLayer.GetLayerDefn()
                        gpkgLayersColumnsNumber[gpkgLayerName] = gpkgLayerDefn.GetFieldCount()
                        # Get the layer columns names and data type
                        columnList: list[str] = []
                        columnNamesDataType: dict[str, str] = {}
                        for j in range(gpkgLayerDefn.GetFieldCount()):
                            time.sleep(self.loopLimit)
                            # Get the column names
                            gpkgFieldDefn: ogr.FieldDefn = gpkgLayerDefn.GetFieldDefn(j)
                            gpkgColumnName: str = gpkgFieldDefn.GetName()
                            columnList.append(gpkgColumnName.lower())
                            # Get the column data type
                            gpkgColumnDataType: str = gpkgFieldDefn.GetTypeName()
                            columnNamesDataType[gpkgColumnName] = gpkgColumnDataType
                        
                        gpkgLayersColumnsNames[gpkgLayerName] = columnList
                        gpkgLayersColumnsDataTypes[gpkgLayerName] = columnNamesDataType
                        
            if gpkg_type == "Documentation":
                self.gpkgDocumentationDataStructure = {
                    "LayerNumber" : gpkgLayersNumber,
                    "LayerNames" : gpkgLayersNames,
                    "LayerGeoms" : gpkgLayersGeoms,
                    "LayerColumnsNumber" : gpkgLayersColumnsNumber,
                    "LayerColumnsNames" : gpkgLayersColumnsNames,
                    "LayerColumnsDataTypes" : gpkgLayersColumnsDataTypes}
            elif gpkg_type == "Template":
                self.gpkgTemplateDataStructure = {
                    "LayerNumber" : gpkgLayersNumber,
                    "LayerNames" : gpkgLayersNames,
                    "LayerGeoms" : gpkgLayersGeoms,
                    "LayerColumnsNumber" : gpkgLayersColumnsNumber,
                    "LayerColumnsNames" : gpkgLayersColumnsNames,
                    "LayerColumnsDataTypes" : gpkgLayersColumnsDataTypes}
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.read_gpkg_data.__name__}] "
                f"Failed to read the gpkg data. \n{traceBack}")
    # ====================================
    
    # ========== Extract Template Metadata ==========
    def extract_template_metadata(self,
            template_gpkg: gdal.Dataset,
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> None:
        """Extracts the data from the template GPKG to validate the data from the documentation gpkg.

        Args:
            template_gpkg (gdal.Dataset): The template GPKG gdal dataset.
            plan_type (Literal[&quot;PUG&quot;, &quot;PUZ&quot;, &quot;PUD&quot;, &quot;PATJ&quot;, &quot;PMUD&quot;]): The plan type.
        """
        try:
            feature: ogr.Feature
            
            if plan_type in ["PUG", "PUZ", "PUD"]:
                # Data to get
                nivelPlan: list[str] = [] # This is all lowercase
                # Cod_ZF_ZRS Tip_ZF_ZRS Tip_zona
                codZfZrsDefTipZona: dict[str, dict[str, str]] = {}
                codZfZrs: list[str] = []
                # Tip subzona
                tipSubzona: list[str] = []
                # Tip zone
                tipZona: list[str] = []
                # Hilucs
                hilucsDict: dict[str, str] = {} # Hilucs = Hilucs Parent
                # Rte
                rteSubtipTip: dict[str, str] = {} # RTE Subtip - RTE Tip
                rteTip: list[str] = []
                rteSubTip: list[str] = []
                rteStadiu: list[str] = []
                # Ccom
                ccomSubtipTip: dict[str, str] = {} # CCom Subtip - CCom Tip
                ccomTip: list[str] = []
                ccomSubTip: list[str] = []
                ccomStadiu: list[str] = []
                # Tip RU
                ruTip: list[str] = []
                # Date Formats
                dateFormats = [
                    "%Y-%m-%d",      # 2023-12-25
                    "%d-%m-%Y",      # 25-12-2023
                    "%Y/%m/%d",      # 2023/12/25
                    "%d/%m/%Y",      # 25/12/2023
                    "%Y.%m.%d",      # 2023.12.25
                    "%d.%m.%Y",      # 25.12.2023
                ]
                
                # Nivel Plan
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_nivel_plan")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    value = feature.GetField("definitie")
                    if value not in nivelPlan:
                        nivelPlan.append(value)
                
                # Get the Cod_ZF, Tip_ZF and Tip Zona
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_zona")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    valueCod = feature.GetField("cod")
                    valueTipCod = feature.GetField("definitie")
                    valueTipZona = feature.GetField("tip_zona")
                    
                    if valueCod not in codZfZrs:
                        codZfZrs.append(valueCod)
                        
                    if valueCod not in codZfZrsDefTipZona:
                        codZfZrsDefTipZona[valueCod] = {valueTipCod: valueTipZona}
                        
                # Get the Tip subzona
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_tip_subzona")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    valueSubZona = feature.GetField("definitie")
                    
                    if valueSubZona not in tipSubzona:
                        tipSubzona.append(valueSubZona)
                
                # Get the Tip zona
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_tip_zona")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    valueZona = feature.GetField("definitie")
                    
                    if valueZona not in tipZona:
                        tipZona.append(valueZona)
                
                # Get the Hilucs
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_hilucs")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    hilucs = feature.GetField("cod")
                    hilucsParent = feature.GetField("cod_parinte")
                    
                    if hilucs not in hilucsDict:
                        hilucsDict[hilucs] = hilucsParent
                        
                # Get the Tip and Subtip RTE
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_subtip_rte")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    tipRte = feature.GetField("tip_rte")
                    subTipRte = feature.GetField("definitie")
                    
                    if tipRte not in rteTip:
                        rteTip.append(tipRte)
                    
                    if subTipRte not in rteSubTip:
                        rteSubTip.append(subTipRte)
                    
                    if subTipRte not in rteSubtipTip:
                        rteSubtipTip[subTipRte] = tipRte
                
                # Get the RTE stadiu
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_stadiu_rte")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    stadiuRte = feature.GetField("definitie")
                    
                    if stadiuRte not in rteStadiu:
                        rteStadiu.append(stadiuRte)
                        
                # Get the Tip and Subtip CCom
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_subtip_ccom")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    tipCcom = feature.GetField("tip_ccom")
                    subTipCcom = feature.GetField("definitie")
                    
                    if tipCcom not in ccomTip:
                        ccomTip.append(tipCcom)
                    
                    if subTipCcom not in ccomSubTip:
                        ccomSubTip.append(subTipCcom)
                    
                    if subTipCcom not in ccomSubtipTip:
                        ccomSubtipTip[subTipCcom] = tipCcom
                
                # Get the Ccom stadiu
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_stadiu_ccom")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    stadiuCcom = feature.GetField("definitie")
                    
                    if stadiuCcom not in ccomStadiu:
                        ccomStadiu.append(stadiuCcom)

                # Get the Tip RU
                gpkgTemplateLayer: ogr.Layer = template_gpkg.GetLayerByName("_tip_ru")
                gpkgTemplateLayer.ResetReading()
                
                for feature in gpkgTemplateLayer:
                    time.sleep(self.loopLimit)
                    tipRu = feature.GetField("definitie")
                    
                    if tipRu not in ruTip:
                        ruTip.append(tipRu)
                
                gpkgTemplateMetadata = {
                    "NivelPlan" : nivelPlan,
                    "Cod_ZF_ZRS" : codZfZrs,
                    "Cod_Tip_Zona_ZF_ZRS": codZfZrsDefTipZona,
                    "TipZona" : tipZona,
                    "TipSubzona" : tipSubzona,
                    "Hilucs" : hilucsDict,
                    "TipRTE" : rteTip,
                    "SubtipRTE" : rteSubTip,
                    "StadiuRTE" : rteStadiu,
                    "SubtipTipRTE" : rteSubtipTip,
                    "TipCcom" : ccomTip,
                    "SubtipCcom" : ccomSubTip,
                    "StadiuCcom" : ccomStadiu,
                    "SubtipTipCcom" : ccomSubtipTip,
                    "TipRU" : ruTip,
                    "DateFormats" : dateFormats}
                
                self.gpkgTemplateMetadata = gpkgTemplateMetadata
            
            if plan_type in ["PMUD"]:
                pass
            
            if plan_type in ["PATJ"]:
                pass
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.extract_template_metadata.__name__}] "
                f"Failed to extract the metadata from the template. \n{traceBack}")
    # ===============================================
    
    # ========== Memory Management ==========
    def clear_memory(self) -> None:
        try:
            # Clearing data from memory
            self.zipStructure: dict[str, list[str]] = None
            self.zipData: tempfile.TemporaryDirectory = None
            
            self.gpkgDocumentation: gdal.Dataset = None
            self.gpkgTemplate: gdal.Dataset = None
            
            self.gpkgDocumentationDataStructure: dict[str, str] = None
            self.gpkgTemplateDataStructure: dict[str, str] = None
            self.gpkgTemplateMetadata: dict[str, list[str]] = None
            
            self.uatLimitGpkg: gdal.Dataset = None
            # Collect garbage
            gc.collect()
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.clear_memory.__name__}] "
                f"Failed to read the gpkg data. \n{traceBack}")
    # =======================================
# ======================================================