import atexit
import configparser
import os
import sys
from importlib.resources import as_file, files

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from ws_package.log.writer import LogWriter

logger = LogWriter.file(__name__)

# True when we must suppress background threads that would keep the process alive.
#
# PyInstaller has TWO phases where our code runs:
#   1. Import-scanning: PyInstaller's own analysis loop imports our modules.
#      Detectable by: "PyInstaller" in sys.modules.
#   2. DLL-collection: PyInstaller spawns the *frozen EXE* as a short-lived
#      subprocess to discover which DLLs are loaded at import time. At that
#      point sys.frozen is True but the process must exit quickly. A non-daemon
#      observer thread will block exit and trigger PyInstaller's 30 s timeout,
#      producing an incomplete DLL list and a broken bundle.
#
# We suppress the observer in BOTH phases.  For phase 2 we also make the
# observer thread a daemon as a belt-and-suspenders measure so that even if
# the guard somehow misses, the thread cannot prevent the process from exiting.
_PYINSTALLER_BUILDING = (
    not getattr(sys, "frozen", False)   # phase 1: import-scanning (not frozen)
    and "PyInstaller" in sys.modules    # PyInstaller's analysis loop is active
)


class ConfigHandler(FileSystemEventHandler):
    def __init__(self, config: "Config"):
        self._config = config

    def on_modified(self, event):
        if event.src_path.endswith("config.ini"):
            logger.info("Config file changed: %s — reloading", event.src_path)
            self._config.read()
            try:
                from ws_package.utils.build_mode import clear_build_mode_cache
                clear_build_mode_cache()
            except Exception:
                pass
        return super().on_modified(event)


class Config:
    def __init__(self):
        self.config = configparser.ConfigParser()
        config_resource = files("ws_package").joinpath("config.ini")
        with as_file(config_resource) as config_path:
            self.CONFIG_FILE = str(config_path)

        self.read()

        if _PYINSTALLER_BUILDING:
            self.observer = None
            logger.debug("PyInstaller build mode (import-scan): skipping watchdog observer")
        else:
            config_handler = ConfigHandler(self)
            self.observer = Observer()
            # Mark the observer thread as a daemon so that PyInstaller's DLL-
            # collection subprocess (which runs our frozen app briefly) can exit
            # without waiting for this thread to stop.  In normal app usage the
            # main thread never exits until the app is shut down, so the observer
            # stays alive for the full app lifetime regardless of daemon status.
            self.observer.daemon = True
            self.observer.schedule(config_handler, path=os.path.dirname(self.CONFIG_FILE), recursive=False)
            self.observer.start()
            logger.info("Watching config file: %s", self.CONFIG_FILE)
            atexit.register(self.observer.stop)
            atexit.register(self.observer.join)

    def read(self):
        try:
            logger.debug("Reading config file: %s", self.CONFIG_FILE)
            self.config.read(self.CONFIG_FILE)
        except configparser.Error as e:
            logger.error("Error reading config file: %s", e)
            raise RuntimeError(e)

    def __call__(self, value=None, section="Application", dtype="str"):
        try:
            if dtype == "int":
                return self.config.getint(section, value)
            elif dtype == "bool":
                return self.config.getboolean(section, value)
            elif dtype == "float":
                return self.config.getfloat(section, value)
            else:
                return self.config.get(section, value)
        except (configparser.NoOptionError, configparser.NoSectionError, configparser.Error):
            if value and (value.startswith("log_level_") or value == "session_gap_threshold_minutes"):
                logger.debug("Optional config '%s' not found in [%s], using default", value, section)
            else:
                logger.error("Cannot read [%s] %s as %s from config", section, value, dtype)
            return None


config = Config()
