import logging
import traceback
import re
import os
import json
from typing import Literal, Protocol
import time
import tempfile
import filetype
import fiona

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: ...

# =============== Zip Structure Rules ===============
class ZipStructureRules:
    def __init__(self, logger: logging.Logger, loop_limit: float, rules_writer: RulesWriter):
        # Logger
        self.logger: logging = logger
        # Loop limit
        self.loopLimit: float = loop_limit
        # Rules Result Writer
        self.rulesWriter: RulesWriter = rules_writer
    
    # ========== Validation Rules ==========
    def root_dir_number(self, 
            rule: dict[str, str], 
            zip_root_dir: list[str], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the number of directories at the root of the ZIP archive.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            zip_root_dir (dict[str, list[str]]): The ZIP archive root directories list.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                if not int(rule["valoare_regula"]) == int(len(zip_root_dir)):
                    self.rulesWriter.validation_fail(rule = rule)
                    return int(rule['tip_alerta_id']) != 1
                else:
                    self.rulesWriter.validation_pass(rule = rule)
                    return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.root_dir_number.__name__}] "
                f"Failed to verify the number of directories at the root of the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def root_dir_name(self, 
            rule: dict[str, str], zip_root_dir: list[str], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the name of the directory at the root of the ZIP archive.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            zip_root_dir (dict[str, list[str]]): The ZIP archive root directories list.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                # Check the number of dirs
                if len(zip_root_dir) != 1:
                    self.rulesWriter.validation_fail(rule = rule, verify = "Exista mai mult de un fisier principal la radacina arhivei ZIP")
                    return int(rule['tip_alerta_id']) != 1
                
                # Get the directory and regex
                rootDirectory = zip_root_dir[0]
                regex = rule['valoare_regula']
                
                # Verify the root directory name
                if not re.match(regex, rootDirectory):
                    self.rulesWriter.validation_fail(rule=rule)
                    return int(rule['tip_alerta_id']) != 1 
                else:
                    self.rulesWriter.validation_pass(rule = rule)
                    return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.root_dir_name.__name__}] "
                f"Failed to verify the name of directory at the root of the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def gpkg_file_number(self, 
            rule: dict[str, str], zip_gpkg_files: list[str], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the number of gpkg files in the ZIP archive.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            zip_gpkg_files (dict[str, list[str]]): The ZIP archive gpkg files.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                # Checks the number of gpkg files found
                if not int(rule["valoare_regula"]) == int(len(zip_gpkg_files)):
                    self.rulesWriter.validation_fail(rule=rule)
                    return int(rule['tip_alerta_id']) != 1
                else:
                    self.rulesWriter.validation_pass(rule = rule)
                    return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.gpkg_file_number.__name__}] "
                f"Failed to verify the number of gpkg files in the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def gpkg_file_name(self, 
            rule: dict[str, str], zip_gpkg_files: list[str], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the name of gpkg file in the ZIP archive.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            zip_gpkg_files (dict[str, list[str]]): The ZIP archive gpkg files.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                # Checks if there is more than one gpkg file
                if len(zip_gpkg_files) != 1:
                    self.rulesWriter.validation_fail(rule=rule)
                    return int(rule['tip_alerta_id']) != 1
                
                gpkg = zip_gpkg_files[0]
                regex = rule['valoare_regula']
                
                if not re.match(regex, gpkg):
                    self.rulesWriter.validation_fail(rule=rule)
                    return int(rule['tip_alerta_id']) != 1
                else:
                    self.rulesWriter.validation_pass(rule = rule)
                    return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.gpkg_file_name.__name__}] "
                f"Failed to verify the name of gpkg files in the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
        
    def zip_tree_structure(self, 
            rule: dict[str, str], zip_root_dir: list[str], 
            zip_folder_file_relation: list[str], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the tree structure of the ZIP archive

        Args:
            rule (dict[str, str]): The validation rule metadata.
            zip_root_dir (dict[str, list[str]]): The ZIP archive root directories list
            zip_folder_file_relation (dict[str, list[str]]): The sub folder - file relation.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                # Checks if there is more than one directory at the root of the ZIP archive
                if len(zip_root_dir) != 1:
                    self.rulesWriter.validation_fail(rule = rule, verify = "Exista mai mult de un fisier principal la radacina arhivei ZIP")
                    return int(rule['tip_alerta_id']) != 1
                
                # The structure to validate
                ruleValues: dict[str, str] = json.loads(rule["valoare_regula"])
                
                # Verify sub folders
                subFoldersFail: list[str] = []
                subFolders: list[str] = [folder for folder in zip_folder_file_relation if not folder.endswith((".pdf", ".PDF"))]

                for subFolder, _ in ruleValues.items():
                    time.sleep(self.loopLimit)
                    if f"{subFolder}/" not in subFolders:
                        subFoldersFail.append(subFolder)
                
                if len(subFoldersFail) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = f"Verificati folderul '{zip_root_dir[0]}' sa contina urmatoarele foldere: {subFoldersFail}")
                    return int(rule['tip_alerta_id']) != 1
                
                # Pdf files
                folderZero: list[str] = [file for file in zip_folder_file_relation if file.startswith('0_') and file.endswith((".gpkg", ".GPKG"))]
                folderOne: list[str] = [file for file in zip_folder_file_relation if file.startswith('1_') and file.endswith((".pdf", ".PDF"))]
                folderTwo: list[str] = [file for file in zip_folder_file_relation if file.startswith('2_') and file.endswith((".pdf", ".PDF"))]
                folderThree: list[str] = [file for file in zip_folder_file_relation if file.startswith('3_') and file.endswith((".pdf", ".PDF"))]
                folderFour: list[str] = [file for file in zip_folder_file_relation if file.startswith('4_') and file.endswith((".pdf", ".PDF"))]
                # Verify the number of PDF files
                numberFail: list[str] = []
                for subFolder, folderFile in ruleValues.items():
                    if subFolder.startswith("1_"):
                        if not len(folderOne) <= len(folderFile):
                            if subFolder not in numberFail:
                                numberFail.append(subFolder)
                    if subFolder.startswith("2_"):
                        if not len(folderTwo) <= len(folderFile):
                            if subFolder not in numberFail:
                                numberFail.append(subFolder)
                    if subFolder.startswith("3_"):
                        if not len(folderThree) <= len(folderFile):
                            if subFolder not in numberFail:
                                numberFail.append(subFolder)
                            
                if len(numberFail) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = f"Verificati folderele '{numberFail}' sa contina numarul corect de fisiere PDF.")
                    return int(rule['tip_alerta_id']) != 1
                
                # Verify the name of the PDF files
                nameFail: dict[str, list[str]] = {}
                for subFolder, folderFile in ruleValues.items():
                    time.sleep(self.loopLimit)
                    
                    if subFolder.startswith("0_"):
                        if len(folderZero) == 0:
                            nameFail[subFolder] = "Fisierul GPKG nu exista"
                        elif len(folderZero) != 1:
                            nameFail[subFolder] = "Exista mai mult de un fisier GPKG"
                        elif not folderZero[0].endswith("gpkg"):
                            nameFail[subFolder] = "Nu exista fisierul GPKG"
                    
                    if subFolder.startswith("1_"):
                        failedFiles: list[str] = []
                        for file in folderFile:
                            time.sleep(self.loopLimit)
                            if f"{subFolder}/{file}" not in folderOne:
                                failedFiles.append(file)
                        
                        if len(failedFiles) != 0:
                            nameFail[subFolder] = failedFiles
                    
                    if subFolder.startswith("2_"):
                        failedFiles: list[str] = []
                        for file in folderFile:
                            time.sleep(self.loopLimit)
                            if f"{subFolder}/{file}" not in folderTwo:
                                failedFiles.append(file)
                        
                        if len(failedFiles) != 0:
                            nameFail[subFolder] = failedFiles
                            
                    if subFolder.startswith("3_"):
                        failedFiles: list[str] = []
                        for file in folderFile:
                            time.sleep(self.loopLimit)
                            if f"{subFolder}/{file}" not in folderThree:
                                failedFiles.append(file)
                        
                        if len(failedFiles) != 0:
                            nameFail[subFolder] = failedFiles
                    
                    if subFolder.startswith("4_"):
                        failedFiles: list[str] = []
                        for file in folderFour:
                            pdfFile = file.split("/")[1]
                            if not pdfFile.startswith(folderFile[0]) or not pdfFile.endswith(folderFile[1]):
                                failedFiles.append(pdfFile)
                        
                        if len(failedFiles) != 0:
                            nameFail[subFolder] = failedFiles
                            
                if len(nameFail) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = f"Verificati folderele ca foldere sa contina fisierele: {nameFail}")
                    return int(rule['tip_alerta_id']) != 1
                else:
                    self.rulesWriter.validation_pass(rule = rule)
                    return True
                
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.zip_tree_structure.__name__}] "
                f"Failed to verify the tree structure of the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
        
    def avize_files_number(self,
            rule: dict[str, str], avize_files: list[str], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the number of PDF files inside the avize folder.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            avize_files (dict[str, list[str]]): The list of PDF files inside the avize folder.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                if not int(rule["valoare_regula"]) >= int(len(avize_files)):
                    self.rulesWriter.validation_fail(rule=rule)
                    return int(rule['tip_alerta_id']) != 1
                else:
                    self.rulesWriter.validation_pass(rule = rule)
                    return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.avize_files_number.__name__}] "
                f"Failed to verify the number of the PDF files in the avize folder the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def avize_files_name(self,
            rule: dict[str, str], avize_files: list[str], 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify the name of PDF files inside the avize folder.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            avize_files (dict[str, list[str]]): The list of PDF files inside the avize folder.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                regex = rule['valoare_regula']
                avize_failed = []
                
                for aviz in avize_files:
                    time.sleep(self.loopLimit)
                    if not re.match(r''+regex+'', aviz):
                        avize_failed.append(aviz)
                
                if not len(avize_failed) == 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = avize_failed)
                    return int(rule['tip_alerta_id']) != 1
                else:
                    self.rulesWriter.validation_pass(rule = rule)
                    return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.avize_files_name.__name__}] "
                f"Failed to verify the name of the PDF files in the avize folder the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def valid_pdf_files(self,
            rule: dict[str, str], extraction_folder: tempfile.TemporaryDirectory, 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify that the PDF files are valid PDF files.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            pdf_files (dict[str, list[str]]): The list of PDF files inside the avize folder.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                pdfList: list[str] = []
                
                for rootFolder, _, files in os.walk(extraction_folder):
                    time.sleep(self.loopLimit)
                    for file in files:
                        if file.endswith((".pdf", ".PDF")):
                            pdfList.append(f"{rootFolder}\\{file}")
                            
                pdfFail: list[str] = []
                for pdfFile in pdfList:
                    time.sleep(self.loopLimit)
                    
                    kind = filetype.guess(pdfFile)
                    if kind == None:
                        pdfFail.append(pdfFile.split("\\")[-1])
                    elif kind.extension != rule["valoare_regula"]:
                        pdfFail.append(pdfFile.split("\\")[-1])
                    elif kind.extension == rule["valoare_regula"]:
                        continue
                    else:
                        pdfFail.append(pdfFile.split("\\")[-1])
                
                if len(pdfFail) != 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = pdfFail)
                    return int(rule['tip_alerta_id']) != 1
                else:
                    self.rulesWriter.validation_pass(rule = rule)
                    return True
            
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.valid_pdf_files.__name__}] "
                f"Failed to valide the PDF files in the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    
    def valid_gpkg_files(self,
            rule: dict[str, str], extraction_folder: tempfile.TemporaryDirectory, 
            plan_type: Literal["PUG", "PUZ", "PUD", "PATJ", "PMUD"]) -> bool:
        """Verify that the GPKG files are valid GPKG files.

        Args:
            rule (dict[str, str]): The validation rule metadata.
            pdf_files (dict[str, list[str]]): The list of PDF files inside the avize folder.
            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 validation rule passes. False if the rule fails or an error occurs.
        """
        try:
            if plan_type in ["PUG", "PUZ", "PUD", "PATJ", "PMUD"]:
                gpkgList: list[str] = []
                
                for rootFolder, _, files in os.walk(extraction_folder):
                    time.sleep(self.loopLimit)
                    for file in files:
                        if file.endswith((".gpkg", ".GPKG")):
                            gpkgList.append(f"{rootFolder}\\{file}")
                
                if len(gpkgList) == 0:
                    self.rulesWriter.validation_fail(rule = rule, verify = "Nu exista un fisier GPKG")
                    return int(rule['tip_alerta_id']) != 1
                elif len(gpkgList) != 1:
                    self.rulesWriter.validation_fail(rule = rule, verify = "Exista doua sau mai multe fisiere GPKG in arhiva ZIP")
                    return int(rule['tip_alerta_id']) != 1

                with fiona.open(gpkgList[0]) as src:
                    file_format = src.driver.lower()
                    
                    if file_format == rule['valoare_regula']:
                        self.rulesWriter.validation_pass(rule = rule)
                        return True
                    else:
                        self.rulesWriter.validation_fail(rule = rule, verify = "Fisierul GPKG nu este valid")
                        return int(rule['tip_alerta_id']) != 1
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.valid_gpkg_files.__name__}] "
                f"Failed to valide the gpkg files in the ZIP archive. \n{traceBack}")
            
            self.rulesWriter.validation_error(rule = rule)
            return int(rule['tip_alerta_id']) != 1
    # ======================================
# ===================================================