import logging
from pathlib import Path
from typing import Literal
import json
from datetime import datetime
import pandas as pd
import threading
import traceback
import configparser

# =============== Thread-Safe Logger ===============
class Logger:
    _loggers: dict[str, logging.Logger] = {}
    _lock = threading.Lock()  # Ensures thread-safe creation

    @classmethod
    def create_logger(cls, logger_name: str, log_file_name: str = None) -> logging.Logger:
        """
        Creates or returns a singleton logger with console and optional file handlers.
        Thread-safe for GUI apps with multiple threads.
        """
        with cls._lock:
            # Return existing logger if already created
            if logger_name in cls._loggers:
                return cls._loggers[logger_name]

            # Create new logger
            logger = logging.getLogger(logger_name)
            logger.setLevel(logging.INFO)
            logger.propagate = False  # Avoid double logging if root logger exists

            # Remove existing handlers
            for handler in logger.handlers[:]:
                logger.removeHandler(handler)

            # Formatter
            formatter = logging.Formatter(
                "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
            )

            # Console handler
            console_handler = logging.StreamHandler()
            console_handler.setFormatter(formatter)
            logger.addHandler(console_handler)

            # File handler (optional)
            if log_file_name:
                log_path = Path(__file__).parent / "cache" / "logs" / f"{log_file_name}.log"
                log_path.parent.mkdir(parents=True, exist_ok=True)
                file_handler = logging.FileHandler(log_path, encoding="utf-8")
                file_handler.setFormatter(formatter)
                logger.addHandler(file_handler)

            # Store in class dict
            cls._loggers[logger_name] = logger
            return logger
# ================================================

# =============== Template Loader ===============
class AssetLoader:
    def __init__(self, template_type: Literal["Validator", "Updater"]):
        # Logger
        self.logger = Logger.create_logger(logger_name = "Asset Loader", log_file_name = "asset_loader")
        
        # Paths
        self.templatesPath = Path(__file__).parent / "assets" / "templates"
        self.imagesPath = Path(__file__).parent / "assets" / "images"
        self.iconsPath = Path(__file__).parent / "assets" / "icons"
        
        # Templates and images dicts
        self.templates: dict[str, str] = {}
        self.images: dict[str, str] = {}
        
        # Type of assests to load
        if template_type == "Validator":
            self._load_validator_templates()
            self._load_validator_images()
        elif template_type == "Updater":
            self._load_updater_templates()
            self._load_updater_images()
        
    # ========== Validator ==========
    def _load_validator_templates(self):
        """Load the validator templates."""
        try:
            templateFilesList: list[str] = ["validator_window.json", "validator_view.json", "login_view.json"]
            
            for file in templateFilesList:
                templateFilePath = self.templatesPath / file
                if not templateFilePath.exists():
                    raise Exception(f"One of the core templates doesn't exist: '{file}'")
                else:
                    with open(templateFilePath, 'r') as tFile:
                        self.templates[templateFilePath.stem] = json.load(tFile)
            
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self._load_validator_templates.__name__}] "
                f"Failed to load validator templates. \n{traceBack}")
    
    def _load_validator_images(self):
        """Load the validator images."""
        try:
            imagesList: list[str] = ["login.jpg", "validation.png", "down_arrow.png", "bin.png", "save.png"]
            iconName: str = "logo.ico"
            
            for image in imagesList:
                imagePath = self.imagesPath / image
                if not imagePath.exists():
                    raise Exception(f"One of the core images doesn't exist: '{image}'")
                else:
                    self.images[imagePath.stem] = imagePath
            
            iconPath = self.iconsPath / iconName
            if not iconPath.exists():
                raise Exception(f"The icon image doesn't exists: '{iconName}'")
            else:
                self.images[iconPath.stem] = iconPath
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self._load_validator_images.__name__}] "
                f"Failed to load validator images. \n{traceBack}")
    # ===============================
    
    # ========== Updater ==========
    def _load_updater_templates(self):
        """Load the updater templates."""
        try:
            templateFilesList: list[str] = ["updater_window.json", "update_view.json"]
            
            for file in templateFilesList:
                templateFilePath = self.templatesPath / file
                if not templateFilePath.exists():
                    raise Exception(f"One of the core templates doesn't exist: '{file}'")
                else:
                    with open(templateFilePath, 'r') as tFile:
                        self.templates[templateFilePath.stem] = json.load(tFile)
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self._load_updater_templates.__name__}] "
                f"Failed to load updater templates. \n{traceBack}")
            
    def _load_updater_images(self):
        """Load the updater images"""
        try:
            iconName: str = "logo.ico"
            
            iconPath = self.iconsPath / iconName
            if not iconPath.exists():
                raise Exception(f"The icon image doesn't exists: '{iconName}'")
            else:
                self.images[iconPath.stem] = iconPath
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self._load_updater_images.__name__}] "
                f"Failed to load validator images. \n{traceBack}")
    # =============================
# ===============================================

# =============== Config Manager ===============
class ConfigManager:
    """Contains the function to read and return the values of the config file."""
    def __init__(self, config_file: str = "config.ini"):
        # Logger
        self.logger: logging.Logger = Logger.create_logger(logger_name = "ConfigManager", log_file_name = "config_manager")
        # Config file path
        self.configFilePath: Path = Path(__file__).parent / "keys" / config_file
        # ConfigParser
        self.parser: configparser.ConfigParser | None = configparser.ConfigParser()
        if self.configFilePath.exists():
            self.parser.read(self.configFilePath)
        else:
            self.parser = None
        
        # Already-read values
        self._values: dict[str, dict[str, str]] = {}
    
    # ========== Return the value ==========
    def get_value(self, section: str, key: str) -> str:
        """Gets the values based on the section and key.

        Args:
            section (str): The section of the config file.
            key (str): The key of the section from the config file

        Raises:
            FileNotFoundError: Config file not found.
            ValueError: Value not found
            ConfigManagerError: Error of the class.

        Returns:
            str: The value of the key
        """
        try:
            # Check if the parser is not none
            if self.parser is None:
                raise FileNotFoundError(f"The config file is missing: {self.configFilePath}")
            
            # Checks the cache
            if section in self._values and key in self._values[section]:
                return self._values[section][key]
            
            # Reads the config file
            value = self.parser.get(section, key)
            
            # Checks if the value exists
            if value is None:
                raise ValueError(f"Value for the Section = '{section}', Key = '{key}' doesn't exists or the section and key are not valid.")
            
            # Cache the value
            if section not in self._values:
                self._values[section] = {}
            self._values[section][key] = value
            
            return value
            
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.get_value.__name__}] "
                f"Failed to read config value. Section = '{section}', Key = '{key}', "
                f"ConfigFile = '{self.configFilePath}'\n{traceBack}")
    # ======================================
# ==============================================

# =============== Token Manager ===============
class TokenManager:
    """Contains the functions to manage the tokens of the user."""
    def __init__(self):
        # Logger
        self.logger: logging.Logger = Logger.create_logger(logger_name = "TokenManager", log_file_name = "token_manager")
        # Tokens file path
        self.tokensFilePath: Path = Path(__file__).parent / "keys" / "tokens.json"
        self.fileExists: bool = self.tokensFilePath.exists()
        # Token file struct and keys
        self.tokensFileKeys: list[str] = ["access", "refresh"]
        
        # Already-read tokens
        self._tokens: dict[str, str] = {}
        
        # Thread lock
        self._lock = threading.Lock()
        
    # ========== Token File Verification ==========
    def verify_file(self) -> None:
        """Ensure the token file exists and has the correct structure."""
        try:
            defaultTokens = {key: "" for key in self.tokensFileKeys}
            
            with self._lock:
                # If file doesn't exist, create it
                if not self.tokensFilePath.exists():
                    with open(self.tokensFilePath, 'w') as tFile:
                        json.dump(defaultTokens, tFile)
                    return  # done

                # File exists, read tokens
                try:
                    with open(self.tokensFilePath, 'r') as tFile:
                        tokensData = json.load(tFile)
                except (json.JSONDecodeError, FileNotFoundError):
                    tokensData = {}

                # Check if all required keys are present
                if not all(key in tokensData for key in self.tokensFileKeys):
                    with open(self.tokensFilePath, 'w') as tFile:
                        json.dump(defaultTokens, tFile)
                    
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.verify_file.__name__}] "
                f"Failed to create the tokens file. "
                f"File path = '{self.tokensFilePath}'\n{traceBack}")
    # =============================================
    
    # ========== Write Token File ==========
    def write_tokens(self, tokens: dict[str, str]) -> None:
        """Write new tokens to the file and keep cache updated."""
        try:
            with self._lock:
                with open(self.tokensFilePath, 'w') as f:
                    json.dump(tokens, f)
                self._tokens = tokens.copy()
                
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.write_tokens.__name__}] "
                f"Failed to write the tokens. "
                f"File path = '{self.tokensFilePath}'\n{traceBack}")
    # ======================================
    
    # ========== Read Token File ==========
    def read_tokens(self) -> dict[str, str]:
        """Read tokens from cache or file."""
        try:
            with self._lock:
                if self._tokens:
                    return self._tokens.copy()

                with open(self.tokensFilePath, 'r') as f:
                    tokensData: dict[str, str] = json.load(f)

                # Ensure all keys exist
                if not all(key in tokensData for key in self.tokensFileKeys):
                    self.verify_file()
                    raise Exception("Wrong token files keys")
                
                self._tokens = tokensData.copy()
                return self._tokens.copy()
            
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.read_tokens.__name__}] "
                f"Failed to read the tokens. "
                f"File path = '{self.tokensFilePath}'\n{traceBack}")
    # =====================================
    
    # ========== Wipe Token File ==========
    def wipe_tokens(self) -> None:
        """Reset the token file and clear the cache."""
        try:
            defaultTokens = {key: "" for key in self.tokensFileKeys}
            with self._lock:
                with open(self.tokensFilePath, 'w') as f:
                    json.dump(defaultTokens, f)
                self._tokens = {}
                
        except Exception as e:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.wipe_tokens.__name__}] "
                f"Failed to wipe the tokens. "
                f"File path = '{self.tokensFilePath}'\n{traceBack}")
    # =====================================
# =============================================

# =============== Raport Manager ===============
class RaportManager:
    def __init__(self):
        # Logger
        self.logger: logging.Logger = Logger.create_logger(logger_name = "RaportManager", log_file_name = "raport_manager")
        # Raport Cache Folder
        self.raportCacheFolder = Path(__file__).parent / "cache" / "reports"
        self.raportCacheFolderDevelopment = Path(__file__).parent / "cache" / "reports" / "development"
        self.raportCacheFolderPublic = Path(__file__).parent / "cache" / "reports" / "public"
        # Pre-Load Raports
        self._raportsDevelopment: list[dict[str, str]] = []
        self._raportsPublic: list[dict[str, str]] = []
        
        if not self.raportCacheFolder.exists():
            self.raportCacheFolder.mkdir()
        if not self.raportCacheFolderPublic.exists():
            self.raportCacheFolderPublic.mkdir()
        if not self.raportCacheFolderDevelopment.exists():
            self.raportCacheFolderDevelopment.mkdir()
                
    # ========== Load the Raports ========== 
    def _load_raports(self, production: bool) -> None:
        """Loads the raports"""
        try:
            threading.Thread(
                target = self._load_raports_worker,
                args = [production],
                daemon = True).start()
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self._load_raports.__name__}] "
                f"Failed to load the raports. \n{traceBack}")
    
    def _load_raports_worker(self, production: bool):
        try:
            if not self.raportCacheFolderPublic.exists():
                self.raportCacheFolderPublic.mkdir()
            if not self.raportCacheFolderDevelopment.exists():
                self.raportCacheFolderDevelopment.mkdir()
                
            if production == True:
                raportList = []
                for raport in self.raportCacheFolderPublic.iterdir():
                    path = raport
                    number, judet, uat, siruta, plan, hours, mins, sec, day, month, year, status = raport.stem.split("_")
                    
                    raportData = {
                        "Number" : int(number),
                        "Judet" : judet,
                        "UAT" : uat,
                        "Siruta" : siruta,
                        "TipPlan" : plan,
                        "TimeDate" : f"{hours}:{mins}:{sec} {day}/{month}/{year}",
                        "ValdationStatus" : status,
                        "Path" : str(path)}
                    if raportData not in raportList:
                        raportList.append(raportData)
                
                raportList.sort(key=lambda x: x["Number"])
                self._raportsPublic = raportList
            else:
                raportList = []
                for raport in self.raportCacheFolderDevelopment.iterdir():
                    path = raport
                    number, judet, uat, siruta, plan, hours, mins, sec, day, month, year, status = raport.stem.split("_")
                    
                    raportData = {
                        "Number" : int(number),
                        "Judet" : judet,
                        "UAT" : uat,
                        "Siruta" : siruta,
                        "TipPlan" : plan,
                        "TimeDate" : f"{hours}:{mins}:{sec} {day}/{month}/{year}",
                        "ValdationStatus" : status,
                        "Path" : str(path)}
                    if raportData not in raportList:
                        raportList.append(raportData)
            
                raportList.sort(key=lambda x: x["Number"])
                self._raportsDevelopment = raportList
                
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self._load_raports_worker.__name__}] "
                f"Failed to load the raports. \n{traceBack}")
    # ======================================
    
    # ========== Save Raport ==========
    def save_raport_local(self, raport: pd.DataFrame, file_path: str, validation_status: str, production: bool, plan_type: str) -> None:
        """Saves the raport in the cache folder.

        Args:
            raport (pd.DataFrame): The raport pandas dataframe.
            file_path (str): The path of the file that was validated.
        """
        try:    
            if production == True:
                if len(self._raportsPublic) != 0:
                    lastRaportNumber = self._raportsPublic[-1]["Number"]
                    raportNumber = int(lastRaportNumber) + 1
                else:
                    raportNumber = len([file for file in self.raportCacheFolderPublic.iterdir()])
                
                filePath = Path(file_path)
                fileName = filePath.stem
                if plan_type == "PATJ":
                    uat, siruta, tipPlan, _ = fileName.split("_")
                    validationStatus = "Pass" if validation_status == True else "Fail"
                    now = datetime.now()
                    timeDate = now.strftime("%H_%M_%S_%d_%m_%Y")
                    
                    raportFileName = f"{raportNumber}__{uat}_{siruta}_{tipPlan}_{timeDate}_{validationStatus}.csv"
                    raportSavePath = self.raportCacheFolderPublic / raportFileName
                    raport.to_csv(raportSavePath, index=False, encoding='utf-8')
                    self._load_raports(production = production)
                else:
                    judet, uat, siruta, tipPlan, _ = fileName.split("_")
                    validationStatus = "Pass" if validation_status == True else "Fail"
                    now = datetime.now()
                    timeDate = now.strftime("%H_%M_%S_%d_%m_%Y")
                    
                    raportFileName = f"{raportNumber}_{judet}_{uat}_{siruta}_{tipPlan}_{timeDate}_{validationStatus}.csv"
                    raportSavePath = self.raportCacheFolderPublic / raportFileName
                    raport.to_csv(raportSavePath, index=False, encoding='utf-8')
                    self._load_raports(production = production)
            else:
                if len(self._raportsDevelopment) != 0:
                    lastRaportNumber = self._raportsDevelopment[-1]["Number"]
                    raportNumber = int(lastRaportNumber) + 1
                else:
                    raportNumber = len([file for file in self.raportCacheFolderDevelopment.iterdir()])
                
                filePath = Path(file_path)
                fileName = filePath.stem
                
                if plan_type == "PATJ":
                    uat, siruta, tipPlan, _ = fileName.split("_")
                    validationStatus = "Pass" if validation_status == True else "Fail"
                    now = datetime.now()
                    timeDate = now.strftime("%H_%M_%S_%d_%m_%Y")
                    
                    raportFileName = f"{raportNumber}__{uat}_{siruta}_{tipPlan}_{timeDate}_{validationStatus}.csv"
                    raportSavePath = self.raportCacheFolderDevelopment / raportFileName
                    raport.to_csv(raportSavePath, index=False, encoding='utf-8')
                    self._load_raports(production = production)
                else:
                    judet, uat, siruta, tipPlan, _ = fileName.split("_")
                    validationStatus = "Pass" if validation_status == True else "Fail"
                    now = datetime.now()
                    timeDate = now.strftime("%H_%M_%S_%d_%m_%Y")
                    
                    raportFileName = f"{raportNumber}_{judet}_{uat}_{siruta}_{tipPlan}_{timeDate}_{validationStatus}.csv"
                    raportSavePath = self.raportCacheFolderDevelopment / raportFileName
                    raport.to_csv(raportSavePath, index=False, encoding='utf-8')
                    self._load_raports(production = production)
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.save_raport_local.__name__}] "
                f"Failed to locally save the raport. \n{traceBack}")
    # =================================
# ==============================================

# =============== Logs Manager ===============
class LogManager:
    def __init__(self):
        # Logger
        self.logger = Logger.create_logger(logger_name = "Log Manager", log_file_name = "log_manager")
        # Path
        self.logsPath = Path(__file__).parent / "cache" / "logs"

    # ========== Functionality ==========
    def get_logs_paths(self) -> list[str]:
        """Gets all the log file paths from the log folder"""
        try:
            logFilePaths: list[str] = []
            
            if not self.logsPath.exists():
                return logFilePaths
            
            for file in self.logsPath.glob("*.log"):
                if file.is_file():
                    logFilePaths.append(file)
            
            return logFilePaths
        except:
            traceBack = traceback.format_exc()
            self.logger.error(
                f"[{self.__class__.__name__} - {self.get_logs_paths.__name__}] "
                f"Failed to get the log paths. \n{traceBack}")
    # ===================================
# ============================================