import os
import re
import shlex
import time
import uuid
from typing import Optional, Pattern

from bottles.backend.dlls.dxvk import DXVKComponent
from bottles.backend.dlls.nvapi import NVAPIComponent
from bottles.backend.dlls.vkd3d import VKD3DComponent
from bottles.backend.logger import Logger
from bottles.backend.models.config import BottleConfig
from bottles.backend.models.process import (
    ProcessFinishedPayload,
    ProcessStartedPayload,
)
from bottles.backend.models.result import Result
from bottles.backend.state import SignalManager, Signals
from bottles.backend.utils.manager import ManagerUtils
from bottles.backend.wine.cmd import CMD
from bottles.backend.wine.explorer import Explorer
from bottles.backend.wine.msiexec import MsiExec
from bottles.backend.wine.start import Start
from bottles.backend.wine.winebridge import WineBridge
from bottles.backend.wine.winecommand import WineCommand
from bottles.backend.wine.winedbg import WineDbg
from bottles.backend.wine.winepath import WinePath

logging = Logger()


class WineExecutor:
    _PLACEHOLDER_PATTERN: Pattern[str] = re.compile(r"%([A-Z_]+)%")
    _KNOWN_PLACEHOLDERS: set[str] = {
        "PROGRAM_NAME",
        "PROGRAM_PATH",
        "PROGRAM_DIR",
        "BOTTLE_NAME",
        "BOTTLE_PATH",
    }

    def __init__(
        self,
        config: BottleConfig,
        exec_path: str,
        args: str = "",
        terminal: bool = False,
        environment: Optional[dict] = None,
        move_file: bool = False,
        move_upd_fn: callable = None,
        pre_script: Optional[str] = None,
        post_script: Optional[str] = None,
        pre_script_args: Optional[str] = None,
        post_script_args: Optional[str] = None,
        cwd: Optional[str] = None,
        monitoring: Optional[list] = None,
        program_dxvk: Optional[bool] = None,
        program_vkd3d: Optional[bool] = None,
        program_nvapi: Optional[bool] = None,
        program_gamescope: Optional[bool] = None,
        program_virt_desktop: Optional[bool] = None,
        program_winebridge: Optional[bool] = None,
        sandbox_override: Optional[str] = None,
    ):
        logging.info("Launching an executable…")
        self.config = config
        # Per-launch dedicated sandbox override (None / "off" / "legacy"); see
        # WineCommand for the meaning of each value.
        self.sandbox_override = sandbox_override
        self.__validate_path(exec_path)

        if monitoring is None:
            monitoring = []

        if environment is None:
            environment = {}

        if move_file:
            exec_path = self.__move_file(exec_path, move_upd_fn)

        self.exec_type = self.__get_exec_type(exec_path)
        self._raw_exec_path = exec_path
        self.exec_path = shlex.quote(exec_path)
        self.args = args
        self.terminal = terminal
        self.environment = environment
        self.pre_script = pre_script
        self.post_script = post_script
        self.pre_script_args = pre_script_args
        self.post_script_args = post_script_args
        self.cwd = self.__get_cwd(cwd)
        self.monitoring = monitoring
        self.use_gamescope = program_gamescope
        self.use_virt_desktop = program_virt_desktop
        self.use_winebridge = (
            program_winebridge
            if program_winebridge is not None
            else getattr(self.config.Parameters, "winebridge", True)
        )
        self._play_session_id = -1

        env_dll_overrides = []

        # None = use global DXVK value
        if program_dxvk is not None:
            # DXVK is globally activated, but disabled for the program
            if not program_dxvk and self.config.Parameters.dxvk:
                # Disable DXVK for the program
                override_dxvk = DXVKComponent.get_override_keys() + "=b"
                env_dll_overrides.append(override_dxvk)

        if program_vkd3d is not None:
            if not program_vkd3d and self.config.Parameters.vkd3d:
                override_vkd3d = VKD3DComponent.get_override_keys() + "=b"
                env_dll_overrides.append(override_vkd3d)

        if program_nvapi is not None:
            if not program_nvapi and self.config.Parameters.dxvk_nvapi:
                override_nvapi = NVAPIComponent.get_override_keys() + "=b"
                env_dll_overrides.append(override_nvapi)

        if (
            program_gamescope is not None
            and program_gamescope != self.config.Parameters.gamescope
        ):
            self.environment["GAMESCOPE"] = "1" if program_gamescope else "0"

        if env_dll_overrides:
            if "WINEDLLOVERRIDES" in self.environment:
                self.environment["WINEDLLOVERRIDES"] += ";" + ";".join(
                    env_dll_overrides
                )
            else:
                self.environment["WINEDLLOVERRIDES"] = ";".join(env_dll_overrides)

    @staticmethod
    def is_unreachable_in_sandbox(path: Optional[str]) -> bool:
        """A document portal path (/run/user/<uid>/doc/<id>/...) lives outside
        the bottle and is not carried into the dedicated sandbox, so a program
        stored there cannot be opened while the sandbox is active. Detect it so
        the frontend can warn before launching."""
        if not path:
            return False
        return "/run/user/" in path and "/doc/" in path

    @classmethod
    def run_program(
        cls,
        config: BottleConfig,
        program: dict,
        terminal: bool = False,
        sandbox_override: Optional[str] = None,
    ):
        if program is None:
            logging.warning("The program entry is not well formatted.")

        placeholders = cls._build_placeholder_map(config, program or {})

        def _resolve(field: str):
            return cls._replace_placeholders((program or {}).get(field), placeholders)

        arguments = _resolve("arguments")
        if not (program or {}).get("arguments_enabled", True):
            arguments = ""

        return cls(
            config=config,
            exec_path=program.get("path"),
            args=arguments,
            pre_script=cls._replace_placeholders(
                program.get("pre_script"), placeholders
            ),
            post_script=cls._replace_placeholders(
                program.get("post_script"), placeholders
            ),
            pre_script_args=_resolve("pre_script_args"),
            post_script_args=_resolve("post_script_args"),
            cwd=_resolve("folder"),
            terminal=terminal,
            program_dxvk=program.get("dxvk"),
            program_vkd3d=program.get("vkd3d"),
            program_nvapi=program.get("dxvk_nvapi"),
            program_gamescope=program.get("gamescope"),
            program_virt_desktop=program.get("virtual_desktop"),
            program_winebridge=program.get("winebridge"),
            sandbox_override=sandbox_override,
        ).run()

    @staticmethod
    def _build_placeholder_map(config: BottleConfig, program: dict) -> dict[str, str]:
        program_path = program.get("path", "") or ""
        program_dir_raw = program.get("folder")
        program_dir = program_dir_raw or ""
        if isinstance(program_dir_raw, str):
            matches = WineExecutor._PLACEHOLDER_PATTERN.findall(program_dir_raw)
            if any(match in WineExecutor._KNOWN_PLACEHOLDERS for match in matches):
                # ignore unresolved placeholders that reference known tokens
                program_dir = ""
        if not program_dir and isinstance(program_path, str) and program_path:
            program_dir = os.path.dirname(program_path)

        bottle_path = ""
        if config:
            try:
                bottle_path = ManagerUtils.get_bottle_path(config)
            except Exception:
                bottle_path = ""

        placeholders = {
            "PROGRAM_NAME": program.get("name", ""),
            "PROGRAM_PATH": program_path,
            "PROGRAM_DIR": program_dir,
            "BOTTLE_NAME": getattr(config, "Name", "") or "",
            "BOTTLE_PATH": bottle_path,
        }
        return {
            key: value for key, value in placeholders.items() if isinstance(value, str)
        }

    @classmethod
    def _replace_placeholders(
        cls, value: Optional[str], placeholders: dict[str, str]
    ) -> Optional[str]:
        if not isinstance(value, str) or not value:
            return value

        def _sub(match: re.Match[str]) -> str:
            key = match.group(1)
            return placeholders.get(key, match.group(0))

        return cls._PLACEHOLDER_PATTERN.sub(_sub, value)

    def __get_cwd(self, cwd: str) -> str | None:
        winepath = WinePath(self.config)
        if cwd in [None, ""]:
            path = self.exec_path
            if winepath.is_windows(self.exec_path):
                path = "\\".join(path.split("\\")[:-1])
                path = winepath.to_unix(path)
            if path.startswith(("'", '"')):
                path = path[1:]
            if path.endswith(("'", '"')):
                path = path[:-1]
            return os.path.dirname(path)
        return cwd  # will be set by WineCommand if None

    @staticmethod
    def __validate_path(exec_path):
        if exec_path in [None, ""]:
            logging.error("No executable file path provided.")
            return False

        if ":\\" in exec_path:
            logging.warning("Windows path detected. Avoiding validation.")
            return True

        if not os.path.isfile(exec_path):
            _msg = f"Executable file path does not exist: {exec_path}"
            if "FLATPAK_ID" in os.environ:
                _msg = f"Executable file path does not exist or is not accessible by the Flatpak: {exec_path}"
            logging.error(
                _msg,
            )
            return False

    def __move_file(self, exec_path, move_upd_fn):
        new_path = ManagerUtils.move_file_to_bottle(
            file_path=exec_path, config=self.config, fn_update=move_upd_fn
        )
        if new_path:
            exec_path = new_path

        self.__validate_path(exec_path)

        return exec_path

    @staticmethod
    def __get_exec_type(exec_path):
        _exec = exec_path.lower()
        if _exec.endswith(".exe"):
            return "exe"
        if _exec.endswith(".msi"):
            return "msi"
        if _exec.endswith(".bat"):
            return "batch"
        if _exec.endswith(".lnk"):
            return "lnk"
        if _exec.endswith(".dll"):
            return "dll"

        logging.warning("Not a common executable type, trying to launch it anyway.")
        return "unsupported"

    def run_cli(self):
        """
        We need to launch the application and then exit,
        so we use Wine Starter, which will exit as soon
        as the program is launched
        """
        winepath = WinePath(self.config)
        start = Start(self.config)

        if winepath.is_unix(self.exec_path):
            return self.__launch_with_bridge()

        res = start.run(
            file=self.exec_path,
            terminal=self.terminal,
            args=self.args,
            environment=self.environment,
            pre_script=self.pre_script,
            post_script=self.post_script,
            pre_script_args=self.pre_script_args,
            post_script_args=self.post_script_args,
            cwd=self.cwd,
        )
        return Result(status=True, data={"output": res})

    def run(self) -> Result:
        # Emit ProgramStarted (best-effort)
        launch_id = f"{self.config.Name}:{int(time.time() * 1000)}:{os.getpid()}"
        bottle_id = self.config.Name
        bottle_name = self.config.Name
        bottle_path = ManagerUtils.get_bottle_path(self.config)
        program_name = (
            os.path.basename(self._raw_exec_path) if self._raw_exec_path else "unknown"
        )
        program_path = self._raw_exec_path or self.exec_path
        
        try:
            fonts_dir = os.path.join(ManagerUtils.get_bottle_path(self.config), "drive_c", "windows", "Fonts")
            stamp_file = os.path.join(ManagerUtils.get_bottle_path(self.config), ".fonts_stamp")
            if os.path.exists(fonts_dir):
                fonts_mtime = os.path.getmtime(fonts_dir)
                stamp_mtime = 0.0
                if os.path.exists(stamp_file):
                    with open(stamp_file, "r") as f:
                        stamp_mtime = float(f.read().strip() or 0)
                
                if fonts_mtime > stamp_mtime:
                    logging.info("Fonts directory modified manually, running wineboot to update fonts registry.")
                    from bottles.backend.wine.wineboot import WineBoot
                    WineBoot(self.config).launch(update=True)
                    with open(stamp_file, "w") as f:
                        f.write(str(fonts_mtime))
        except Exception as e:
            logging.debug(f"Failed to check and update fonts: {e}")

        try:
            SignalManager.send(
                Signals.ProgramStarted,
                Result(
                    True,
                    ProcessStartedPayload(
                        launch_id=launch_id,
                        bottle_id=bottle_id,
                        bottle_name=bottle_name,
                        bottle_path=bottle_path,
                        program_name=program_name,
                        program_path=program_path,
                    ),
                ),
            )
        except Exception:
            pass

        try:
            match self.exec_type:
                case "exe" | "msi":
                    res = self.__launch_with_bridge()
                case "batch":
                    res = self.__launch_batch()
                case "lnk" | "unsupported":
                    res = self.__launch_with_starter()
                case "dll":
                    res = self.__launch_dll()
                case _:
                    res = Result(
                        status=False, data={"message": "Unknown executable type."}
                    )
        except Exception as e:
            try:
                SignalManager.send(
                    Signals.ProgramFinished,
                    Result(
                        True,
                        ProcessFinishedPayload(
                            launch_id=launch_id,
                            status="unknown",
                            ended_at=int(time.time()),
                        ),
                    ),
                )
            except Exception:
                pass
            raise e

        # Finalize session
        try:
            SignalManager.send(
                Signals.ProgramFinished,
                Result(
                    True,
                    ProcessFinishedPayload(
                        launch_id=launch_id,
                        status="success" if res.status else "unknown",
                        ended_at=int(time.time()),
                    ),
                ),
            )
        except Exception:
            pass

        return res

    def __launch_with_bridge(self):
        if self.use_winebridge and self.exec_type == "exe":
            winebridge = WineBridge(self.config)
            if winebridge.is_available():
                winepath = WinePath(self.config)
                exec_path = (
                    winepath.to_windows(self.exec_path, native=True)
                    if winepath.is_unix(self.exec_path)
                    else self.exec_path
                )
                res = winebridge.run_exe(exec_path)
                return Result(status=True, data={"output": res})
        winepath = WinePath(self.config)
        if self.use_virt_desktop:
            if winepath.is_unix(self.exec_path):
                self.exec_path = winepath.to_windows(self.exec_path)
            return self.__launch_with_explorer()
        if winepath.is_windows(self.exec_path):
            return self.__launch_with_starter()

        match self.exec_type:
            case "exe":
                return self.__launch_exe()
            case "msi":
                return self.__launch_msi()
            case "batch":
                return self.__launch_batch()
            case _:
                logging.error(f"exec_type {self.exec_type} is not valid")
                return Result(
                    status=False, data={"message": "Unknown executable type."}
                )

    def __launch_exe(self):
        # winebridge = WineBridge(self.config)
        # if winebridge.is_available():
        #     res = winebridge.run_exe(self.exec_path)
        #     return Result(
        #         status=True,
        #         data={"output": res}
        #     )

        winecmd = WineCommand(
            self.config,
            command=self.exec_path,
            arguments=self.args,
            terminal=self.terminal,
            environment=self.environment,
            communicate=True,
            pre_script=self.pre_script,
            post_script=self.post_script,
            pre_script_args=self.pre_script_args,
            post_script_args=self.post_script_args,
            cwd=self.cwd,
            sandbox_override=self.sandbox_override,
        )
        res = winecmd.run()
        self.__set_monitors()
        return Result(status=True, data={"output": res})

    def __launch_msi(self):
        msiexec = MsiExec(self.config)
        msiexec.install(
            pkg_path=self.exec_path,
            args=self.args,
            terminal=self.terminal,
            cwd=self.cwd,
            environment=self.environment,
        )
        self.__set_monitors()
        return Result(True)

    def __launch_batch(self):
        cmd = CMD(self.config)
        res = cmd.run_batch(
            batch=self.exec_path,
            terminal=self.terminal,
            args=self.args,
            environment=self.environment,
            cwd=self.cwd,
        )
        return Result(status=True, data={"output": res})

    def __launch_with_starter(self):
        start = Start(self.config)
        res = start.run(
            file=self.exec_path,
            terminal=self.terminal,
            args=self.args,
            environment=self.environment,
            pre_script=self.pre_script,
            post_script=self.post_script,
            pre_script_args=self.pre_script_args,
            post_script_args=self.post_script_args,
            cwd=self.cwd,
        )
        self.__set_monitors()
        return Result(status=True, data={"output": res})

    def __launch_with_explorer(self):
        w, h = self.config.Parameters.virtual_desktop_res.split("x")
        start = Explorer(self.config)
        res = start.launch_desktop(
            desktop=str(uuid.uuid4()),
            width=w,
            height=h,
            program=self.exec_path,
            args=self.args,
            environment=self.environment,
            cwd=self.cwd,
        )
        self.__set_monitors()
        return Result(status=res.status, data={"output": res.data})

    @staticmethod
    def __launch_dll():
        logging.warning("DLLs are not supported yet.")
        return Result(status=False, data={"error": "DLLs are not supported yet."})

    def __set_monitors(self):
        if not self.monitoring:
            return

        logging.info("Starting {} monitors".format(len(self.monitoring)))

        winedbg = WineDbg(self.config, silent=True)
        for m in self.monitoring:
            winedbg.wait_for_process(name=m)
