import os
import sys
from pathlib import Path
from typing import Type
import filetype
import traceback
import tempfile
import zipfile
from osgeo import ogr, gdal
import shutil
import gc
import pandas as pd
import importlib.util

from shared.utils import Logger
from shared.contracts import (
    GetVerificationResultsCallback, GetValidationResultsCallback, 
    ValidationSignature, ZipStructRulesGet, GpkgDataRulesGet, GpkgStructRulesGet, UatLimitGet,
    GetProgressbarSteps, GetProgressbarUpdate, GetProgressbarEnd)

# =============== Document Verification ===============
class DocumentVerification:
    def __init__(self):
        # Logger
        self.logger = Logger.create_logger(logger_name = "Document Verification", log_file_name = "document_verification")
        
        # Version Layer
        self.versionLayer: str = "__versiune"
        
        # Results
        self.fileType: str | None = None
        self.planType: str | None = None
        self.templateVersion: str | None = None
        self.localPackages: list[str] | None = None
        self.updateStatus: bool | None = None
        self.validateStatus: bool | None = None
        self.templateGpkgExists: bool | None = None
        
        # Verification Rules
        self.verificationRules: dict[int, str] = {
            1 : {
                "rule" : "Se verifica fisierul incarcat..."
            },
            2 : {
                "rule" : "Se verifica daca fisierul exista...",
                "pass" : "Fisierul incarcat exista...",
                "fail" : "Fisierul incarcat nu exista."
            },
            3 : {
                "rule" : "Se verifica tipul fisierul incarcat...",
                "pass" : {
                    "zip" : "Fisierul incarcat este o arhiva zip...",
                    "gpkg" : "Fisierul incarcat este un gpkg..."
                },
                "fail" : "Fisierul incarcat nu este o arhiva zip sau un fisier gpkg."
            },
            4 : {
                "rule" : {
                    "zip" : "Se extrage si se verifica fisierul gpkg din arhiva zip...",
                    "gpkg" : "Se verifica fisierul gpkg..."
                },
                "fail" : {
                    "GpkgNumber" : "Exista mai mult de un fisier gpkg in arhiva zip.",
                    "GpkgExist" : "Nu exista fisierul gpkg in arhiva zip.",
                    "LayerExist" : "Stratul cu versiunea template-ului nu exista.",
                    "DataMissing" : "Stratul cu versiunea template-ului nu contine date.",
                    "RowsNumber" : "Stratul cu versiunea template-ului contine prea multe inregistrari."
                },
                "pass" : "Versiunea de template a fost gasita..."
            },
            5 : {
                "rule" : "Se verifica daca versiunea template-ului este valida...",
                "pass" : "Versiune template-ului este valida...",
                "fail" : { 
                    "VersionNotFound" : "Versiunea template-ului nu este valida.",
                    "TemplateNotFound" : "Nu exista un template GPKG pentru tipul de plan planType"}
            },
            6 : {
                "rule" : "Se verifica daca pachetul de reguli este instalat local...",
                "pass" : {
                    "zip" : "Pachetul de reguli este instalat local. Arhiva zip se poate valida.",
                    "gpkg" : "Pachetul de reguli este instalat local. Fisierul gpkg se poate valida."
                },
                "fail" : "Pachetul de reguli nu este instalat local. Apasati pe butonul 'Update' pentru al instala."
            }
        }
    
    # ========== Verify Documentation ==========
    def verify_documentation(self, file_path: str, valid_templates: list[str], local_validation_packages: list[str], get_verification_results_callback: GetVerificationResultsCallback) -> None:
        try:
            for ruleNum, rule in self.verificationRules.items():
                # Shows that the verification started
                if ruleNum == 1:
                    get_verification_results_callback(complete = False, message = rule["rule"], verification_result = None)
                # Verifies if the file exists.
                elif ruleNum == 2:
                    get_verification_results_callback(complete = False, message = rule["rule"], verification_result = None)
                    fileExistsStatus = self.verify_file_exists(file_path = file_path)
                    if fileExistsStatus == False:
                        get_verification_results_callback(complete = True, message = rule["fail"], verification_result = None)
                        return
                    else:
                        get_verification_results_callback(complete = False, message = rule["pass"], verification_result = None)
                # Verifies the file type
                elif ruleNum == 3:
                    get_verification_results_callback(complete = False, message = rule["rule"], verification_result = None)
                    fileTypeStatus = self.verify_file_type(file_path = file_path)
                    
                    if fileTypeStatus == False:
                        get_verification_results_callback(complete = True, message = rule["fail"], verification_result = None)
                        return
                    else:
                        get_verification_results_callback(complete = False, message = rule["pass"][self.fileType], verification_result = None)
                # Verifies if the template version layer exists
                elif ruleNum == 4:
                    get_verification_results_callback(complete = False, message = rule["rule"][self.fileType], verification_result = None)
                    
                    templateExistStatus, ErrorMsg = self.verify_template_version(file_path = file_path)
                    
                    if templateExistStatus == False and ErrorMsg is not None:
                        get_verification_results_callback(complete = True, message = rule["fail"][ErrorMsg], verification_result = None)
                        return
                    else:
                        get_verification_results_callback(complete = False, message = rule["pass"], verification_result = None)
                
                # Verifies if the template is valid
                elif ruleNum == 5:
                    get_verification_results_callback(complete = False, message = rule["rule"], verification_result = None)
                    templateValidStatus, templateExists = self.verify_template_valid(valid_template_versions = valid_templates)
                    
                    if templateValidStatus == False:
                        get_verification_results_callback(complete = True, message = rule["fail"]["VersionNotFound"], verification_result = None)
                        return
                    elif templateExists == False:
                        get_verification_results_callback(complete = True, message = rule["fail"]["TemplateNotFound"].replace("planType", self.planType), verification_result = None)
                        return
                    else:
                        get_verification_results_callback(complete = False, message = rule["pass"], verification_result = None)
                
                # Verifies if the template version matches a validation package version
                elif ruleNum == 6:
                    get_verification_results_callback(complete = False, message = rule["rule"], verification_result = None)
                    
                    packageVerificationStatus = self.verify_packages(local_validation_packages = local_validation_packages)
                    
                    if packageVerificationStatus == False:
                        verification_results = {
                            "FileType" : None,
                            "PlanType" : None,
                            "VersionTemplate" : self.templateVersion,
                            "LocalPackages" : None,
                            "UpdateStatus" : True,
                            "ValidateStatus" : None,
                            "Error" : None}
                        get_verification_results_callback(complete = True, message = rule["fail"], verification_result = verification_results)
                    else:
                        verification_results = {
                            "FileType" : self.fileType,
                            "PlanType" : self.planType,
                            "VersionTemplate" : self.templateVersion,
                            "LocalPackages" : self.localPackages,
                            "UpdateStatus" : None,
                            "ValidateStatus" : True,
                            "Error" : None}
                        get_verification_results_callback(complete = True, message = rule["pass"][self.fileType], verification_result = verification_results)
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.verify_documentation.__name__}] "
                f"Failed to verify the documentation. \n{traceBack}")

            verification_results = {
                "FileType" : None,
                "PlanType" : None,
                "VersionTemplate" : None,
                "LocalPackages" : None,
                "UpdateStatus" : None,
                "ValidateStatus" : None,
                "Error" : True}
            
            get_verification_results_callback(complete = True, message = "Eroare interna in procesul de verificare.", verification_result = verification_results)
    # ==========================================
    
    # ========== Verify File Exists ==========
    def verify_file_exists(self, file_path: str) -> bool:
        """Verifies if the file exists."""
        try:
            if not os.path.exists(file_path):
                return False
            
            return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.verify_file_exists.__name__}] "
                f"Failed to verify if the file exists. \n{traceBack}")
            raise 
    # ========================================
    
    # ========== Verify File Type ==========
    def verify_file_type(self, file_path: str) -> bool:
        """Verifies the file type"""
        try:
            kind = filetype.guess(file_path)
            
            if not kind:
                return False
            
            if kind.mime == "application/zip":
                self.fileType = "zip"
                return True
            elif kind.mime == "application/x-sqlite3":
                self.fileType = "gpkg"
                return True
            else:
                return False
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.verify_file_type.__name__}] "
                f"Failed to verify the file type. \n{traceBack}")
            raise
    # ======================================
    
    # ========== Verify Template Version ==========
    def verify_template_version(self, file_path: str) -> tuple[bool, str | None]:
        """Verifies the template."""
        try:
            if self.fileType == "zip":
                temp_dir = tempfile.mkdtemp()
                
                with zipfile.ZipFile(file_path, 'r') as zipArchive:
                    gpkg_files = [gpkg for gpkg in zipArchive.namelist() if gpkg.lower().endswith('.gpkg')]
                    
                    if len(gpkg_files) == 0:
                        zipArchive.close()
                        shutil.rmtree(temp_dir, ignore_errors=True)
                        return False, "GpkgExist"
                    
                    if len(gpkg_files) != 1:
                        zipArchive.close()
                        shutil.rmtree(temp_dir, ignore_errors=True)
                        return False, "GpkgNumber"
                    
                    zipArchive.extract(gpkg_files[0], temp_dir)
                    gpkgPath = os.path.join(temp_dir, gpkg_files[0])
                    gpkgPath = os.path.normpath(gpkgPath)
                    
                    gpkgDataset: gdal.Dataset = ogr.Open(gpkgPath)
                    versionLayer: ogr.Layer = gpkgDataset.GetLayerByName(self.versionLayer)
                    
                    if versionLayer is None:
                        gpkgDataset.Close()
                        zipArchive.close()
                        shutil.rmtree(temp_dir, ignore_errors=True)
                        return False, "LayerExist"
                    
                    versionLayer.ResetReading()
                    feature: ogr.Feature = versionLayer.GetNextFeature()
                    
                    if not feature:
                        gpkgDataset.Close()
                        zipArchive.close()
                        shutil.rmtree(temp_dir, ignore_errors=True)
                        return False, "DataMissing"
                    
                    feature_defn: ogr.FeatureDefn = versionLayer.GetLayerDefn()
                    row_dict = {}
                    
                    for i in range(feature_defn.GetFieldCount()):
                        field_defn: ogr.FieldDefn = feature_defn.GetFieldDefn(i)
                        field_name = field_defn.GetName()
                        field_value = feature.GetField(i)
                        row_dict[field_name] = field_value
                    
                    if not len(row_dict) == 3:
                        gpkgDataset.Close()
                        zipArchive.close()
                        shutil.rmtree(temp_dir, ignore_errors=True)
                        return False, "RowsNumber"
                    
                    self.planType = row_dict["tip"]
                    self.templateVersion = row_dict["versiune"]
                    
                    gpkgDataset.Close()
                    zipArchive.close()
                    shutil.rmtree(temp_dir, ignore_errors=True)
                    gc.collect()
                    return True, None
                
            elif self.fileType == "gpkg":
                gpkgDataset: gdal.Dataset = ogr.Open(file_path)
                versionLayer: ogr.Layer = gpkgDataset.GetLayerByName(self.versionLayer)
                
                if versionLayer is None:
                    gpkgDataset.Close()
                    return False, "LayerExist"
                
                versionLayer.ResetReading()
                feature: ogr.Feature = versionLayer.GetNextFeature()
                
                if not feature:
                    gpkgDataset.Close()
                    return False, "DataMissing"
                
                feature_defn: ogr.FeatureDefn = versionLayer.GetLayerDefn()
                row_dict = {}
                
                for i in range(feature_defn.GetFieldCount()):
                    field_defn: ogr.FieldDefn = feature_defn.GetFieldDefn(i)
                    field_name = field_defn.GetName()
                    field_value = feature.GetField(i)
                    row_dict[field_name] = field_value
                
                if not len(row_dict) == 3:
                    gpkgDataset.Close()
                    return False, "RowsNumber"
                
                self.planType = row_dict["tip"]
                self.templateVersion = row_dict["versiune"]
                
                gpkgDataset.Close()
                gc.collect()
                return True, None
            else:
                False, None
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.verify_template_version.__name__}] "
                f"Failed to verify the file template version. \n{traceBack}")
            raise
    # =============================================
    
    # ========== Verify Template Valid ==========
    def verify_template_valid(self, valid_template_versions: list[dict[str, str]]) -> bool | bool:
        """Verifies if the template version is valid."""
        try:
            validationPackagesExists: bool = None

            for validationPackage in valid_template_versions:
                if self.templateVersion == validationPackage["versiune"]:
                    validationPackagesExists = True
                    
            if validationPackagesExists == True:
                package = next((packageDict for packageDict in valid_template_versions if packageDict["versiune"] == self.templateVersion))
                if self.planType == "PUG":
                    if "file_gpgk_pug" in package:
                        self.templateGpkgExists = False if package["file_gpgk_pug"] is None or "" else True
                    else:
                        self.templateGpkgExists = False
                if self.planType == "PUD":
                    if "file_gpgk_pud" in package:
                        self.templateGpkgExists = False if package["file_gpgk_pud"] is None or "" else True
                    else:
                        self.templateGpkgExists = False
                if self.planType == "PUZ":
                    if "file_gpgk_puz" in package:
                        self.templateGpkgExists = False if package["file_gpgk_puz"] is None or "" else True 
                    else:
                        self.templateGpkgExists = False
                if self.planType == "PATJ":
                    if "file_gpgk_patj" in package:
                        self.templateGpkgExists = False if package["file_gpgk_patj"] is None or "" else True
                    else:
                        self.templateGpkgExists = False
                if self.planType == "PMUD":
                    if "file_gpgk_pmud" in package:
                        self.templateGpkgExists = False if package["file_gpgk_pmud"] is None or "" else True
                    else:
                        self.templateGpkgExists = False
            
            if validationPackagesExists == None:
                return False, False
            
            if self.templateGpkgExists == False:
                return True, False
            
            return True, True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.verify_template_valid.__name__}] "
                f"Failed to verify the file if the template is valid. \n{traceBack}")
            raise
    # ===========================================
    
    # ========== Verify Packages ==========
    def verify_packages(self, local_validation_packages: list[str]) -> bool:
        """Verify if the template version has a matching validation package in the local folders."""
        try:
            self.localPackages = local_validation_packages
            if self.templateVersion not in local_validation_packages:
                return False
            
            return True
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.verify_packages.__name__}] "
                f"Failed to verify the packages. \n{traceBack}")
            raise
    # =====================================
# =====================================================

# =============== Document Validation ===============
class DocumentValidation:
    def __init__(self):
        # Logger
        self.logger = Logger.create_logger(logger_name = "Document Validation", log_file_name = "document_validation")

        # Base path
        self.basePath = Path(__file__).parent
        
    # ========== Validate Documentation ==========
    def validate_documentation(self, file_path: str, verification_results: dict[str, str], production: bool,
            validation_package_version: str, get_validation_results_callback: GetValidationResultsCallback, 
            template_url: str, zip_struct_rules_get: ZipStructRulesGet, gpkg_struct_rules_get: GpkgStructRulesGet, gpkg_data_rules_get: GpkgDataRulesGet,
            uat_limit_get: UatLimitGet, progressbar_steps: GetProgressbarSteps, progressbar_update: GetProgressbarUpdate, progressbar_end: GetProgressbarEnd):
        """Validate the documentation."""
        try:
            # ===== Load Validation Package =====
            Validation = self.load_validation_package(package_version = validation_package_version, production = production)
            self.validationPackageInstance = Validation(logger = self.logger)
            # ===================================
            
            # ===== Validate Documentation =====
            self.validationPackageInstance.validate(file_path = file_path, validation_package_version = validation_package_version, verification_results = verification_results, 
                template_url = template_url, get_validation_results_callback = get_validation_results_callback, 
                zip_struct_rules_get = zip_struct_rules_get, gpkg_struct_rules_get = gpkg_struct_rules_get,
                gpkg_data_rules_get = gpkg_data_rules_get, uat_limit_get = uat_limit_get, progressbar_steps = progressbar_steps,
                progressbar_update = progressbar_update, progressbar_end = progressbar_end)
            # ==================================
            
            # ===== Clear Validation Package =====
            self.validationPackageInstance = None
            Validation = None
            gc.collect()
            # ====================================
            
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.validate_documentation.__name__}] "
                f"Failed to validate the documentation. \n{traceBack}")
            get_validation_results_callback(complete = False, status_validation = False, message = "Eroare interna a validatorului", raport = None)
    # ============================================
    
    # ========== Load Validation Package ==========
    def load_validation_package(self, package_version: str, production: bool) -> Type[ValidationSignature]:
        """Loads the selected validation package."""
        try:
            if production == True:
                packageFolder = "public"
            else:
                packageFolder = "development"
                
            sys.path.insert(0, self.basePath / packageFolder / package_version)
            currentDir = os.path.dirname(os.path.abspath(__file__))
            folderPath = os.path.join(currentDir, packageFolder, package_version)
            
            validationPath = os.path.join(folderPath, f"validation.py")
            validNameValidation = f"{package_version}_validation.py"
            
            spec = importlib.util.spec_from_file_location(
                name = validNameValidation, 
                location = validationPath,
                submodule_search_locations = [folderPath])
            moduleValidation = importlib.util.module_from_spec(spec)
            sys.modules[validNameValidation] = moduleValidation
            spec.loader.exec_module(moduleValidation)
            
            Validation: Type[ValidationSignature] = getattr(moduleValidation, "Validation", None)
            
            return Validation
        
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.load_validation_package.__name__}] "
                f"Failed to load validation package. \n{traceBack}")
            raise
    # =============================================
# ===================================================