import json
import os
import subprocess
import sys


class FileOpener:
    """
    A class to handle opening file dialogs using different engines
    (Tkinter, PyQt5, PyQt6, AppleScript).
    This helps prevent infinite loop issues that can occur with certain engines (like PyQt + Bokeh)
    on specific systems.
    """

    def __init__(self, default_engine=None):
        """
        Initialize the FileOpener.

        :param default_engine: "tkinter", "pyqt5", "pyqt6", or "osascript".
                       If None, a dialog box will
                               prompt the user to select an engine on the first run.
        """
        self.engine = default_engine

    def select_engine_dialog(self):
        """Creates a simple Tkinter dialog box to select the file opening engine."""
        import tkinter as tk
        from tkinter import ttk

        root = tk.Tk()
        root.title("Select File Dialog Engine")

        # Keep window on top so it doesn't get hidden behind the browser/terminal
        root.attributes("-topmost", True)

        # Center the window
        window_width = 300
        window_height = 150
        screen_width = root.winfo_screenwidth()
        screen_height = root.winfo_screenheight()
        center_x = int(screen_width / 2 - window_width / 2)
        center_y = int(screen_height / 2 - window_height / 2)
        root.geometry(f"{window_width}x{window_height}+{center_x}+{center_y}")

        label = ttk.Label(root, text="Select File Dialog Engine Framework:")
        label.pack(pady=15)

        selected_engine = tk.StringVar(value="tkinter")

        def on_select():
            self.engine = selected_engine.get()
            root.destroy()

        engine_combobox = ttk.Combobox(
            root,
            textvariable=selected_engine,
            values=["tkinter", "pyqt5", "pyqt6", "osascript"],
            state="readonly",
        )
        engine_combobox.pack(pady=5)

        button = ttk.Button(root, text="Confirm", command=on_select)
        button.pack(pady=10)

        # Run the tkinter loop to wait for selection
        root.mainloop()

        return self.engine

    def get_open_filename(self, title="Select File", filetypes=None):
        """
        Opens a single-file selection dialog using the selected engine.

        :param title: The title of the dialog window.
        :param filetypes: A list of tuples like [("All Files", "*.*"), ("Text Files", "*.txt")]
        :return: Path to the selected file, or an empty string if cancelled.

        .. note::
            If the environment variable ``IECHELLE_TEST_FITS_PATH`` is set, the dialog
            is bypassed entirely and that path is returned immediately. This allows
            automated browser tests (e.g. Playwright) to work without requiring OS-level
            dialog interaction.
        """
        # -- Test-mode bypass -------------------------------------------------
        test_path = os.environ.get("IECHELLE_TEST_FITS_PATH", "")
        if test_path:
            print(
                f"[TEST MODE] FileOpener bypassing dialog, returning: {test_path}"
            )
            return test_path
        # -- Normal interactive flow ------------------------------------------
        if not self.engine:
            self.select_engine_dialog()

        if self.engine == "tkinter":
            return self._open_with_tkinter(title, filetypes, multiple=False)
        elif self.engine == "pyqt5":
            return self._open_with_pyqt5(title, filetypes, multiple=False)
        elif self.engine == "pyqt6":
            return self._open_with_pyqt6(title, filetypes, multiple=False)
        elif self.engine == "osascript":
            return self._open_with_osascript(title, multiple=False)
        else:
            print(f"Unknown engine '{self.engine}', falling back to tkinter.")
            return self._open_with_tkinter(title, filetypes, multiple=False)

    def get_open_filenames(self, title="Select Files", filetypes=None):
        """
        Opens a multi-file selection dialog using the selected engine.

        :param title: The title of the dialog window.
        :param filetypes: A list of tuples like [("All Files", "*.*"), ("Text Files", "*.txt")]
        :return: List of paths to the selected files, or an empty list if cancelled.
        """
        if not self.engine:
            self.select_engine_dialog()

        if self.engine == "tkinter":
            return self._open_with_tkinter(title, filetypes, multiple=True)
        elif self.engine == "pyqt5":
            return self._open_with_pyqt5(title, filetypes, multiple=True)
        elif self.engine == "pyqt6":
            return self._open_with_pyqt6(title, filetypes, multiple=True)
        elif self.engine == "osascript":
            return self._open_with_osascript(title, multiple=True)
        else:
            print(f"Unknown engine '{self.engine}', falling back to tkinter.")
            return self._open_with_tkinter(title, filetypes, multiple=True)

    def get_existing_directory(self, title="Select Directory"):
        """
        Opens a directory selection dialog using the selected engine.

        :param title: The title of the dialog window.
        :return: Path to the selected directory, or an empty string if cancelled.
        """
        if not self.engine:
            self.select_engine_dialog()

        if self.engine == "tkinter":
            return self._open_dir_with_tkinter(title)
        elif self.engine == "pyqt5":
            return self._open_dir_with_pyqt5(title)
        elif self.engine == "pyqt6":
            return self._open_dir_with_pyqt6(title)
        elif self.engine == "osascript":
            return self._open_dir_with_osascript(title)
        else:
            print(f"Unknown engine '{self.engine}', falling back to tkinter.")
            return self._open_dir_with_tkinter(title)

    def get_save_filename(self, title="Save File", filetypes=None):
        """
        Opens a save file selection dialog using the selected engine.

        :param title: The title of the dialog window.
        :param filetypes: A list of tuples like [("All Files", "*.*"), ("Text Files", "*.txt")]
        :return: Path to the selected file to save, or an empty string if cancelled.

        .. note::
            If the environment variable ``IECHELLE_TEST_SAVE_PATH`` is set, the dialog
            is bypassed entirely and that path is returned immediately.
        """
        # -- Test-mode bypass (dynamic or static) -----------------------------
        # First check for an override file (allows dynamic path change during test)
        override_file = os.environ.get("IECHELLE_TEST_SAVE_PATH_OVERRIDE", "")
        if override_file and os.path.exists(override_file):
            try:
                with open(override_file, "r") as f:
                    dynamic_path = f.read().strip()
                if dynamic_path:
                    print(
                        f"[TEST MODE] FileOpener dynamic override: {dynamic_path}"
                    )
                    return dynamic_path
            except Exception as e:
                print(f"[TEST MODE] Failed to read override file: {e}")

        test_path = os.environ.get("IECHELLE_TEST_SAVE_PATH", "")
        if test_path:
            print(
                f"[TEST MODE] FileOpener bypassing save dialog, returning: {test_path}"
            )
            return test_path
        # -- Normal interactive flow ------------------------------------------
        if not self.engine:
            self.select_engine_dialog()

        if self.engine == "tkinter":
            return self._save_with_tkinter(title, filetypes)
        elif self.engine == "pyqt5":
            return self._save_with_pyqt5(title, filetypes)
        elif self.engine == "pyqt6":
            return self._save_with_pyqt6(title, filetypes)
        elif self.engine == "osascript":
            return self._save_with_osascript(title)
        else:
            print(f"Unknown engine '{self.engine}', falling back to tkinter.")
            return self._save_with_tkinter(title, filetypes)

    def _run_osascript(self, script):
        # Run each line as a separate -e argument for better AppleScript parsing.
        args = ["osascript"]
        for line in script.splitlines():
            if line.strip() == "":
                continue
            args.extend(["-e", line])

        proc = subprocess.run(
            args,
            capture_output=True,
            text=True,
            check=False,
        )
        if proc.returncode != 0:
            return ""
        out = proc.stdout.strip()
        if out.startswith('"') and out.endswith('"') and len(out) >= 2:
            out = out[1:-1]
        return out

    def _open_with_osascript(self, title, multiple=False):
        prompt = title.replace('"', '\\"')
        if multiple:
            script = (
                f'set chosen to choose file with prompt "{prompt}" with multiple selections allowed\n'
                'set out to ""\n'
                "repeat with f in chosen\n"
                "set out to out & POSIX path of f & linefeed\n"
                "end repeat\n"
                "return out"
            )
            out = self._run_osascript(script)
            if not out:
                return []
            return [line.strip() for line in out.splitlines() if line.strip()]

        script = (
            f"try\n"
            f'  set f to choose file with prompt "{prompt}"\n'
            f"  return POSIX path of f\n"
            f"on error\n"
            f'  return ""\n'
            f"end try"
        )
        return self._run_osascript(script).strip()

    def _open_dir_with_osascript(self, title):
        prompt = title.replace('"', '\\"')
        script = (
            f"try\n"
            f'  set d to choose folder with prompt "{prompt}"\n'
            f"  return POSIX path of d\n"
            f"on error\n"
            f'  return ""\n'
            f"end try"
        )
        return self._run_osascript(script).strip()

    def _save_with_osascript(self, title):
        prompt = title.replace('"', '\\"')
        script = (
            f"try\n"
            f'  set f to choose file name with prompt "{prompt}"\n'
            f"  return POSIX path of f\n"
            f"on error\n"
            f'  return ""\n'
            f"end try"
        )
        return self._run_osascript(script).strip()

    def _format_filetypes_for_qt(self, filetypes):
        """Helper to format filetypes for PyQt5/6 dialogs."""
        if not filetypes:
            return "All Files (*)"
        # Convert Tkinter style [("Text", "*.txt")] to PyQt style "Text (*.txt)"
        return ";;".join([f"{name} ({ext})" for name, ext in filetypes])

    def _open_with_tkinter(self, title, filetypes, multiple):
        return self._run_tk_dialog_subprocess(
            mode="open_many" if multiple else "open_one",
            title=title,
            filetypes=filetypes,
        )

    def _open_dir_with_tkinter(self, title):
        return self._run_tk_dialog_subprocess(
            mode="directory",
            title=title,
            filetypes=None,
        )

    def _save_with_tkinter(self, title, filetypes):
        return self._run_tk_dialog_subprocess(
            mode="save_one",
            title=title,
            filetypes=filetypes,
        )

    def _run_tk_dialog_subprocess(self, mode, title, filetypes):
        """Run Tk dialogs out-of-process to avoid Bokeh/Tk event-loop conflicts."""
        payload = {
            "mode": mode,
            "title": title,
            "filetypes": filetypes or [],
        }
        script = r"""
import json
import sys
import tkinter as tk
from tkinter import filedialog

cfg = json.loads(sys.argv[1])
root = tk.Tk()
root.withdraw()
root.attributes("-topmost", True)

kwargs = {"title": cfg.get("title", "Select File")}
if cfg.get("filetypes"):
    kwargs["filetypes"] = cfg["filetypes"]

mode = cfg.get("mode")
if mode == "open_many":
    result = list(filedialog.askopenfilenames(**kwargs) or [])
elif mode == "directory":
    result = filedialog.askdirectory(title=cfg.get("title", "Select Directory")) or ""
elif mode == "save_one":
    result = filedialog.asksaveasfilename(**kwargs) or ""
else:
    result = filedialog.askopenfilename(**kwargs) or ""

root.destroy()
print(json.dumps(result))
"""
        proc = subprocess.run(
            [sys.executable, "-c", script, json.dumps(payload)],
            capture_output=True,
            text=True,
            check=False,
        )

        if proc.returncode != 0:
            print("Tk dialog subprocess failed:", proc.stderr.strip())
            return [] if mode == "open_many" else ""

        stdout = proc.stdout.strip()
        if not stdout:
            return [] if mode == "open_many" else ""

        try:
            return json.loads(stdout.splitlines()[-1])
        except json.JSONDecodeError:
            return [] if mode == "open_many" else ""

    def _open_with_pyqt5(self, title, filetypes, multiple):
        try:
            from PyQt5.QtWidgets import QApplication, QFileDialog
            import sys

            app = QApplication.instance()
            if app is None:
                app = QApplication(sys.argv)

            qt_filetypes = self._format_filetypes_for_qt(filetypes)

            if multiple:
                file_paths, _ = QFileDialog.getOpenFileNames(
                    None, title, "", qt_filetypes
                )
                return file_paths
            else:
                file_path, _ = QFileDialog.getOpenFileName(
                    None, title, "", qt_filetypes
                )
                return file_path
        except ImportError:
            print("PyQt5 is not installed. Falling back to tkinter.")
            return self._open_with_tkinter(title, filetypes, multiple)

    def _open_dir_with_pyqt5(self, title):
        try:
            from PyQt5.QtWidgets import QApplication, QFileDialog
            import sys

            app = QApplication.instance()
            if app is None:
                app = QApplication(sys.argv)

            dir_path = QFileDialog.getExistingDirectory(
                None, title, "", QFileDialog.Option.ShowDirsOnly
            )
            return dir_path
        except ImportError:
            return self._open_dir_with_tkinter(title)

    def _save_with_pyqt5(self, title, filetypes):
        try:
            from PyQt5.QtWidgets import QApplication, QFileDialog
            import sys

            app = QApplication.instance()
            if app is None:
                app = QApplication(sys.argv)

            qt_filetypes = self._format_filetypes_for_qt(filetypes)
            file_path, _ = QFileDialog.getSaveFileName(
                None, title, "", qt_filetypes
            )
            return file_path
        except ImportError:
            return self._save_with_tkinter(title, filetypes)

    def _open_with_pyqt6(self, title, filetypes, multiple):
        try:
            from PyQt6.QtWidgets import QApplication, QFileDialog
            import sys

            app = QApplication.instance()
            if app is None:
                app = QApplication(sys.argv)

            qt_filetypes = self._format_filetypes_for_qt(filetypes)

            if multiple:
                file_paths, _ = QFileDialog.getOpenFileNames(
                    None, title, "", qt_filetypes
                )
                return file_paths
            else:
                file_path, _ = QFileDialog.getOpenFileName(
                    None, title, "", qt_filetypes
                )
                return file_path
        except ImportError:
            print("PyQt6 is not installed. Falling back to tkinter.")
            return self._open_with_tkinter(title, filetypes, multiple)

    def _open_dir_with_pyqt6(self, title):
        try:
            from PyQt6.QtWidgets import QApplication, QFileDialog
            import sys

            app = QApplication.instance()
            if app is None:
                app = QApplication(sys.argv)

            dir_path = QFileDialog.getExistingDirectory(
                None, title, "", QFileDialog.Option.ShowDirsOnly
            )
            return dir_path
        except ImportError:
            return self._open_dir_with_tkinter(title)

    def _save_with_pyqt6(self, title, filetypes):
        try:
            from PyQt6.QtWidgets import QApplication, QFileDialog
            import sys

            app = QApplication.instance()
            if app is None:
                app = QApplication(sys.argv)

            qt_filetypes = self._format_filetypes_for_qt(filetypes)
            file_path, _ = QFileDialog.getSaveFileName(
                None, title, "", qt_filetypes
            )
            return file_path
        except ImportError:
            return self._save_with_tkinter(title, filetypes)
