import requests
import traceback
from pathlib import Path
from requests.exceptions import ConnectionError as RequestsConnectionError, Timeout, RequestException
from urllib3.exceptions import NewConnectionError, MaxRetryError
from packaging import version
import tempfile
import os
import zipfile

from shared.utils import Logger, TokenManager, ConfigManager

# =============== Validation Packages Service ===============
class ValidationPackageService:
    def __init__(self):
        # Logger
        self.logger = Logger.create_logger(logger_name = "Validation Packages Service", log_file_name = "validator_app")
        
        # Packages Folder Paths
        self.developmentPackages: Path = Path(__file__).parent.parent / "packages" / "development"
        self.publicPackages: Path = Path(__file__).parent.parent / "packages" / "public"
        
        # Class Instances
        self.tokenManager = TokenManager()
        self.configManager = ConfigManager()
        
        # Packages
        self._packagesDownloadedDevelopment: list[str] = []
        self._packagesAvailableDevelopment: list[dict[str, object]] = []
        self._packagesDownloadedPublic: list[str] = []
        self._packagesAvailablePublic: list[dict[str, object]] = []
        
    # ========== On Start-Up ==========
    def get_validation_packages(self, production: bool) -> None:
        try:
            # Tokens
            tokens: dict[str, str] = self.tokenManager.read_tokens()
            # Based on the production arg this will set the API to go to the production or the graphit env.
            if production == True:
                self._packagesAvailablePublic.clear()
                
                packagesInstalled: list[str] = [package.name for package in self.publicPackages.iterdir()]
                self._packagesDownloadedPublic = packagesInstalled
                
                templateVersionRequestUrl: str = self.configManager.get_value(
                    section = "PROVIDER", key = self.configManager.get_value(section = "SERVICES", key = "production")) + self.configManager.get_value(section = "METADATA", key = "version")
                mediaURL: str = self.configManager.get_value(section = "MEDIA", key = "production")
                
                templateVersionsResponse = self.request(url = templateVersionRequestUrl, tokens = tokens)
                if type(templateVersionsResponse) == str:
                    return []
                
                templateVersions: list[dict[str, str]] = templateVersionsResponse.json()
            
                for package in templateVersions:
                    if not version.parse(package["versiune"]) >= version.parse("3.0.0"):
                        continue
                    
                    yyyy, mm, dd = package["lansat_data"].split('-')
                    formatted_date = f"{dd}/{mm}/{yyyy}"
                    
                    if package["file_gpgk_pug"] is not None:
                        pug_url = mediaURL + package["file_gpgk_pug"]
                    else:
                        pug_url = None
                    if package["file_gpgk_patj"] is not None:
                        patj_url = mediaURL + package["file_gpgk_patj"]
                    else:
                        patj_url = None
                    if package["file_gpgk_pmud"] is not None:
                        pmud_url = mediaURL + package["file_gpgk_pmud"]
                    else:
                        pmud_url = None
                    
                    validationPackageData = {
                        "Title" : package["titlu"],
                        "ReleaseDate" : formatted_date,
                        "Description" : package["descriere"],
                        "DownloadLink" : mediaURL + package["file"],
                        "PUG" : pug_url,
                        "PUZ" : pug_url,
                        "PUD" : pug_url,
                        "PATJ" : patj_url,
                        "PMUD" : pmud_url,
                        "Downloadable": True if package["versiune"] not in packagesInstalled else False}
                    if validationPackageData not in self._packagesAvailablePublic:
                        self._packagesAvailablePublic.append(validationPackageData)
                
                
            
            elif production == False:
                self._packagesAvailableDevelopment.clear()
                
                packagesInstalled: list[str] = [package.name for package in self.developmentPackages.iterdir()]
                self._packagesDownloadedDevelopment = packagesInstalled
                
                templateVersionRequestUrl: str = self.configManager.get_value(
                    section = "PROVIDER", key = self.configManager.get_value(section = "SERVICES", key = "development")) + self.configManager.get_value(section = "METADATA", key = "version")
                mediaURL: str = self.configManager.get_value(section = "MEDIA", key = "development")
                templateVersionsResponse = self.request(url = templateVersionRequestUrl, tokens = tokens)
                if type(templateVersionsResponse) == str:
                    return []
                
                templateVersions: list[dict[str, str]] = templateVersionsResponse.json()

                for package in templateVersions:
                    if not version.parse(package["versiune"]) >= version.parse("3.0.0"):
                        continue
                    
                    yyyy, mm, dd = package["lansat_data"].split('-')
                    formatted_date = f"{dd}/{mm}/{yyyy}"
                    
                    if package["file_gpgk_pug"] is not None:
                        pug_url = mediaURL + package["file_gpgk_pug"]
                    else:
                        pug_url = None
                    if package["file_gpgk_pud"] is not None:
                        pud_url = mediaURL + package["file_gpgk_pud"]
                    else:
                        pud_url = None
                    if package["file_gpgk_puz"] is not None:
                        puz_url = mediaURL + package["file_gpgk_puz"]
                    else:
                        puz_url = None
                    if package["file_gpgk_patj"] is not None:
                        patj_url = mediaURL + package["file_gpgk_patj"]
                    else:
                        patj_url = None
                    if package["file_gpgk_pmud"] is not None:
                        pmud_url = mediaURL + package["file_gpgk_pmud"]
                    else:
                        pmud_url = None
                    
                    validationPackageData = {
                        "Title" : package["titlu"],
                        "ReleaseDate" : formatted_date,
                        "Description" : package["descriere"],
                        "DownloadLink" : mediaURL + package["file"],
                        "PUG" : pug_url,
                        "PUZ" : puz_url,
                        "PUD" : pud_url,
                        "PATJ" : patj_url,
                        "PMUD" : pmud_url,
                        "Downloadable": True if package["versiune"] not in packagesInstalled else False}
                    if validationPackageData not in self._packagesAvailableDevelopment: 
                        self._packagesAvailableDevelopment.append(validationPackageData)
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.get_validation_packages.__name__}] "
                f"Failed to get all the validation packages. \n{traceBack}")
    # =================================
        
    # ========== Validation Packages Versions ==========
    def get_validation_packages_version(self, production: bool) -> list[dict[str, str]]:
        try:
            # Tokens
            tokens: dict[str, str] = self.tokenManager.read_tokens()
            
            # Based on the production arg this will set the API to go to the production or the graphit env.
            if production == True:
                templateVersionRequestUrl: str = self.configManager.get_value(
                    section = "PROVIDER", key = self.configManager.get_value(section = "SERVICES", key = "production")) + self.configManager.get_value(section = "METADATA", key = "version")
            else:
                templateVersionRequestUrl: str = self.configManager.get_value(
                    section = "PROVIDER", key = self.configManager.get_value(section = "SERVICES", key = "development")) + self.configManager.get_value(section = "METADATA", key = "version")
                
            templateVersionsResponse = self.request(url = templateVersionRequestUrl, tokens = tokens)
            
            if type(templateVersionsResponse) == str:
                return None
            
            templateVersions: list[dict[str, str]] = templateVersionsResponse.json()
            
            return templateVersions
        
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.get_validation_packages_version.__name__}] "
                f"Failed to get all the validation packages versions. \n{traceBack}")
    # ==================================================
    
    # ========== Get Specific Validation Package ==========
    def get_specific_package(self, production: bool, package_version: str) -> str:
        """Gets the package url file based on the package version."""
        try:
            # Tokens
            tokens: dict[str, str] = self.tokenManager.read_tokens()
            if production == True:
                templateVersionRequestUrl: str = self.configManager.get_value(
                    section = "PROVIDER", key = self.configManager.get_value(section = "SERVICES", key = "production")) + self.configManager.get_value(section = "METADATA", key = "version")
                mediaURL: str = self.configManager.get_value(section = "MEDIA", key = "production")
                
                templateVersionsResponse = self.request(url = templateVersionRequestUrl, tokens = tokens)
                if type(templateVersionsResponse) == str:
                    return []
                
                templateVersions: list[dict[str, str]] = templateVersionsResponse.json()
            
                for package in templateVersions:
                    if version.parse(package["versiune"]) ==  version.parse(package_version):
                        packageUrl: str = mediaURL + package["file"]
                        return packageUrl
            else:
                templateVersionRequestUrl: str = self.configManager.get_value(
                    section = "PROVIDER", key = self.configManager.get_value(section = "SERVICES", key = "development")) + self.configManager.get_value(section = "METADATA", key = "version")
                mediaURL: str = self.configManager.get_value(section = "MEDIA", key = "development")
                templateVersionsResponse = self.request(url = templateVersionRequestUrl, tokens = tokens)
                if type(templateVersionsResponse) == str:
                    return []
                
                templateVersions: list[dict[str, str]] = templateVersionsResponse.json()

                for package in templateVersions:
                    if version.parse(package["versiune"]) ==  version.parse(package_version):
                        packageUrl: str = mediaURL + package["file"]
                        return packageUrl
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.get_specific_package.__name__}] "
                f"Failed to get validation package version. \n{traceBack}")
    # =====================================================
    
    # ========== Local Packages ==========
    def get_local_packages(self, production: bool) -> list[str]:
        """Gets all the local validation packages versions."""
        try:
            if production == True:
                packagesInstalled: list[str] = [package.name for package in self.publicPackages.iterdir()]
                return packagesInstalled
            else:
                packagesInstalled: list[str] = [package.name for package in self.developmentPackages.iterdir()]
                return packagesInstalled
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.get_local_packages.__name__}] "
                f"Failed to get all the local validation packages. \n{traceBack}")
    # ====================================
    
    # ========== Download Package ==========
    def download_package(self, production: bool, package_url: str):
        try:
            response = requests.get(package_url, stream=True)
            totalSize = int(response.headers.get('content-length', 0))
            downloaded = 0

            # Temporary file
            with tempfile.NamedTemporaryFile(delete=False) as tmpFile:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        tmpFile.write(chunk)
                        downloaded += len(chunk)
                        yield downloaded / totalSize  # This is the progress (0-1)
                tmpFilePath = tmpFile.name

            # Extract ZIP to target folder
            if production == True:
                extractTo = Path(self.publicPackages)
            else:
                extractTo = Path(self.developmentPackages)
            with zipfile.ZipFile(tmpFilePath, "r") as zip_ref:
                zip_ref.extractall(extractTo)

            os.remove(tmpFilePath)  # Clean up temp file
            yield 1.0  # Final progress complete
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.download_package.__name__}] "
                f"Failed to download the package. \n{traceBack}")
    # ======================================
    
    # ========== Download Template ==========
    def download_template(self, template_url: str) -> None:
        try:
            response = requests.get(template_url, timeout=60)
            response.raise_for_status()
            return response.content
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.download_template.__name__}] "
                f"Failed to download the template. \n{traceBack}")
    # =======================================
    
    # ========== Request ==========
    def request(self, url: str, tokens: dict[str, str]) -> requests.Response | str:
        """Request data based on url and tokens."""
        try:
            response = requests.get(
                url = url,
                headers = {'Authorization': f'JWT {tokens["access"]}'},
                timeout = 60
            )
            
            if response.status_code != 200:
                return "Error"
            
            return response
        
        except NewConnectionError as e:
            # This catches DNS failures, refused connections, etc.
            self.logger.error(f"[{self.__class__.__name__}.{self.request.__name__}] No internet connection or host unreachable: {e}")
            return "ConnectionError"
        
        except MaxRetryError as e:
            # This catches DNS failures, refused connections, etc.
            self.logger.error(f"[{self.__class__.__name__}.{self.request.__name__}] No internet connection or host unreachable: {e}")
            return "ConnectionError"
        
        except RequestsConnectionError as e:
            # This catches DNS failures, refused connections, etc.
            self.logger.error(f"[{self.__class__.__name__}.{self.request.__name__}] No internet connection or host unreachable: {e}")
            return "ConnectionError"
        
        except ConnectionError as e:
            # This catches DNS failures, refused connections, etc.
            self.logger.error(f"[{self.__class__.__name__}.{self.request.__name__}] No internet connection or host unreachable: {e}")
            return "ConnectionError"
            
        except Timeout as e:
            self.logger.error(f"[{self.__class__.__name__}.{self.request.__name__}] Request timed out: {e}")
            return "ConnectionError"
            
        except RequestException as e:
            # Catches all other requests-related errors
            self.logger.error(f"[{self.__class__.__name__}.{self.request.__name__}] Request failed: {e}")
            return "ConnectionError"
            
        except Exception as e:
            self.logger.error(f"[Class '{self.__class__.__name__}' - Function '{self.request.__name__}'] An error has occurred: {e}")
            return "Error"
    # =============================
# ===========================================================