"""Application entry point for the warehouse desktop tool.

This module wires together the shared async database client, the global
background event loop and the different Tk/CustomTkinter windows exposed by the
project.
"""

import asyncio
import ctypes
import sys
import tkinter as tk
import time

from runtime_support import ensure_stdio, run_with_fatal_log

ensure_stdio("warehouse_main")

import customtkinter as ctk
from tkinter import messagebox

from async_loop_singleton import get_global_loop, stop_global_loop
from async_msssql_query import AsyncMSSQLClient
from audit_log import log_session_event
from db_config import build_dsn_from_config, ensure_db_config
from gestione_layout import open_layout_window
from gestione_pickinglist import open_pickinglist_window
from login_window import prompt_login
from locale_text import load_locale_catalog, text as loc_text
from reset_corsie import open_reset_corsie_window
from search_pallets import open_search_window
from storico_pickinglist import open_storico_pickinglist_window
from storico_udc import open_storico_udc_window
from tooltips import WidgetToolTip, load_tooltip_catalog, tooltip_text
from udc_non_scaffalate import open_udc_non_scaffalate_window
from ui_theme import theme_font, theme_section, theme_value
from user_session import UserSession, create_user_session
from version_info import module_version, versioned_title
from view_celle_multi_udc import open_celle_multiple_window
from window_placement import (
    cascade_children_below_parent,
    place_window_below_parent_later,
    place_window_fullsize_below_parent_later,
)

# Development shortcut: skip the login dialog and boot directly as MAG1.
# Set to False when you want to restore normal authentication.
BYPASS_LOGIN = True
__version__ = module_version(__name__)
BYPASS_LOGIN_USER = {
    "operator_id": 4,
    "login": "MAG1",
    "nominativo": "MAG1",
    "privilegio": 3,
    "codice_unita": "U1",
}

# Create one global loop for database work. Tk must keep the main thread clean;
# callers schedule async jobs on this loop explicitly.
_loop = get_global_loop()


def _noop(*args, **kwargs):
    """Compatibility no-op used when optional Tk DPI hooks are missing."""
    return None


if not hasattr(tk.Toplevel, "block_update_dimensions_event"):
    tk.Toplevel.block_update_dimensions_event = _noop  # type: ignore[attr-defined]
if not hasattr(tk.Toplevel, "unblock_update_dimensions_event"):
    tk.Toplevel.unblock_update_dimensions_event = _noop  # type: ignore[attr-defined]

db_app: AsyncMSSQLClient | None = None
_APP_MUTEX = None
_APP_MUTEX_NAME = "Local\\WarehousePythonAppSingleton"


def _dispose_db_client() -> None:
    """Best-effort disposal of the shared DB client."""

    global db_app
    if db_app is None:
        return
    try:
        fut = asyncio.run_coroutine_threadsafe(db_app.dispose(), _loop)
        try:
            fut.result(timeout=2)
        except Exception:
            pass
    finally:
        db_app = None


def _destroy_tk_root(root: tk.Misc | None) -> None:
    """Destroy a hidden bootstrap root without leaking a Tk interpreter."""

    if root is None:
        return
    try:
        root.quit()
    except Exception:
        pass
    try:
        root.destroy()
    except Exception:
        pass


def _shutdown_runtime(*, bootstrap: tk.Misc | None = None, dispose_db: bool = True) -> None:
    """Release temporary Tk resources, DB client and background loop."""

    try:
        _destroy_tk_root(bootstrap)
    finally:
        if dispose_db:
            _dispose_db_client()
        try:
            stop_global_loop()
        except Exception:
            pass


def _acquire_single_instance_mutex() -> bool:
    """Return ``True`` only for the first running instance of the application."""

    global _APP_MUTEX
    if not sys.platform.startswith("win"):
        return True

    kernel32 = ctypes.windll.kernel32
    mutex = kernel32.CreateMutexW(None, False, _APP_MUTEX_NAME)
    if not mutex:
        return True

    last_error = kernel32.GetLastError()
    _APP_MUTEX = mutex
    ERROR_ALREADY_EXISTS = 183
    return last_error != ERROR_ALREADY_EXISTS


def _build_bypass_session() -> UserSession:
    """Create the development session used when authentication is bypassed."""

    return create_user_session(
        operator_id=int(BYPASS_LOGIN_USER["operator_id"]),
        login=str(BYPASS_LOGIN_USER["login"]),
        nominativo=str(BYPASS_LOGIN_USER["nominativo"]),
        privilegio=int(BYPASS_LOGIN_USER["privilegio"]),
        codice_unita=str(BYPASS_LOGIN_USER["codice_unita"]),
    )


class Launcher(ctk.CTk):
    """Main launcher window that exposes the available warehouse tools."""

    _WINDOW_ORDER = [
        "reset_corsie",
        "layout",
        "multi_udc",
        "non_shelved",
        "search",
        "storico_udc",
        "pickinglist",
        "storico_pickinglist",
    ]

    def __init__(self, session: UserSession, db_client: AsyncMSSQLClient):
        """Create the launcher toolbar and wire every button to a feature window."""
        super().__init__()
        self.session: UserSession = session
        self.db_client = db_client
        self._theme = theme_section("launcher", {})
        self._locale_catalog = load_locale_catalog()
        self._tooltip_catalog = load_tooltip_catalog()
        self._child_windows: list[tk.Misc] = []
        self._child_windows_by_key: dict[str, tk.Misc] = {}
        self._is_cascading = False
        self._focus_restore_pending: set[str] = set()
        self._restore_suppressed_until = 0.0
        self._exit_icon = self._make_exit_icon(
            color=str(theme_value(self._theme, "exit_icon_color", "#ca3d3d"))
        )
        self.title(
            f"{versioned_title(loc_text('launcher.window_title', catalog=self._locale_catalog, default='Warehouse'), __name__)} - {self.session.display_name}"
        )
        self._apply_dynamic_geometry()

        outer_pady = int(theme_value(self._theme, "outer_pady", 10))
        outer_padx = int(theme_value(self._theme, "outer_padx", 0))
        info_padx = int(theme_value(self._theme, "info_padx", 6))
        info_pady = theme_value(self._theme, "info_pady", [4, 2])
        button_padx = int(theme_value(self._theme, "button_padx", 6))
        button_pady = int(theme_value(self._theme, "button_pady", 6))
        max_buttons_per_row = max(1, int(theme_value(self._theme, "max_buttons_per_row", 7)))

        wrap = ctk.CTkFrame(self)
        wrap.pack(padx=outer_padx, pady=outer_pady, fill="x")

        actions = [
            (
                "reset_corsie",
                loc_text("launcher.reset_corsie", catalog=self._locale_catalog, default="Gestione Corsie"),
                "launcher.open_reset_corsie",
                lambda: self._open_or_focus_child_window(
                    "reset_corsie",
                    lambda: open_reset_corsie_window(self, self.db_client, session=self.session),
                ),
            ),
            (
                "layout",
                loc_text("launcher.layout", catalog=self._locale_catalog, default="Gestione Layout"),
                "launcher.open_layout",
                lambda: self._open_or_focus_child_window(
                    "layout",
                    lambda: open_layout_window(self, self.db_client, session=self.session),
                ),
            ),
            (
                "multi_udc",
                loc_text("launcher.multi_udc", catalog=self._locale_catalog, default="UDC Fantasma"),
                "launcher.open_multi_udc",
                lambda: self._open_or_focus_child_window(
                    "multi_udc",
                    lambda: open_celle_multiple_window(self, self.db_client, session=self.session),
                ),
            ),
            (
                "non_shelved",
                loc_text("launcher.non_shelved", catalog=self._locale_catalog, default="UDC non scaffalate"),
                "launcher.open_non_shelved",
                lambda: self._open_or_focus_child_window(
                    "non_shelved",
                    lambda: open_udc_non_scaffalate_window(self, self.db_client, session=self.session),
                ),
            ),
            (
                "search",
                loc_text("launcher.search", catalog=self._locale_catalog, default="Ricerca UDC"),
                "launcher.open_search",
                lambda: self._open_or_focus_child_window(
                    "search",
                    lambda: open_search_window(self, self.db_client, session=self.session),
                ),
            ),
            (
                "storico_udc",
                loc_text("launcher.history_udc", catalog=self._locale_catalog, default="Storico movimenti UDC"),
                "launcher.open_history_udc",
                lambda: self._open_or_focus_child_window(
                    "storico_udc",
                    lambda: open_storico_udc_window(self, self.db_client, session=self.session),
                ),
            ),
            (
                "pickinglist",
                loc_text("launcher.pickinglist", catalog=self._locale_catalog, default="Gestione Picking List"),
                "launcher.open_pickinglist",
                lambda: self._open_or_focus_child_window(
                    "pickinglist",
                    lambda: open_pickinglist_window(self, self.db_client, session=self.session),
                ),
            ),
            (
                "storico_pickinglist",
                loc_text("launcher.history_pickinglist", catalog=self._locale_catalog, default="Storico Picking List"),
                "launcher.open_history_pickinglist",
                lambda: self._open_or_focus_child_window(
                    "storico_pickinglist",
                    lambda: open_storico_pickinglist_window(self, self.db_client, session=self.session),
                ),
            ),
            (
                "arrange",
                loc_text("launcher.arrange", catalog=self._locale_catalog, default="Ridisponi finestre"),
                "launcher.arrange_windows",
                self._cascade_open_windows,
            ),
            (
                "exit",
                loc_text("launcher.exit", catalog=self._locale_catalog, default="Esci"),
                "launcher.exit",
                self._shutdown,
            ),
        ]

        used_columns = max(1, min(len(actions), max_buttons_per_row))

        info = ctk.CTkLabel(
            wrap,
            text=loc_text(
                "launcher.operator",
                catalog=self._locale_catalog,
                default="Operatore: {display_name} ({login})",
            ).format(display_name=self.session.display_name, login=self.session.login),
            anchor="w",
            font=theme_font(self._theme, "info_font", default=("Segoe UI", 12, "bold")),
        )
        info.grid(row=0, column=0, columnspan=used_columns, padx=info_padx, pady=tuple(info_pady), sticky="ew")

        for idx, (_key, label, permission, callback) in enumerate(actions):
            row = 1 + (idx // max_buttons_per_row)
            column = idx % max_buttons_per_row
            text = label
            button_options = {}
            if _key == "exit":
                row = 2
                column = max_buttons_per_row - 1
                text = label
                button_options = {
                    "image": self._exit_icon,
                    "compound": "left",
                }
            button = ctk.CTkButton(
                wrap,
                text=text,
                font=theme_font(self._theme, "button_font", default=("Segoe UI", 11, "bold")),
                state="normal" if self.session.can(permission) else "disabled",
                command=callback,
                **button_options,
            )
            button.grid(row=row, column=column, padx=button_padx, pady=button_pady, sticky="ew")
            tip = tooltip_text(permission, catalog=self._tooltip_catalog)
            if tip:
                WidgetToolTip(button, tip)

        for i in range(max_buttons_per_row):
            wrap.grid_columnconfigure(i, weight=1)

        self.update_idletasks()
        self._apply_dynamic_geometry()

        self.protocol("WM_DELETE_WINDOW", self._shutdown)
        try:
            self.lift()
            self.focus_force()
        except Exception:
            pass

    def _make_exit_icon(self, *, color: str) -> tk.PhotoImage:
        """Create a small red X icon without adding image assets to the project."""

        size = 14
        image = tk.PhotoImage(width=size, height=size)
        for offset in range(3, 11):
            image.put(color, (offset, offset))
            image.put(color, (offset + 1, offset))
            image.put(color, (offset, size - 1 - offset))
            image.put(color, (offset + 1, size - 1 - offset))
        return image

    def _apply_dynamic_geometry(self) -> None:
        """Size the launcher around its current content and keep it docked at the top."""

        top_x = int(theme_value(self._theme, "window_top_x", 0))
        top_y = int(theme_value(self._theme, "window_top_y", 0))
        min_width = int(theme_value(self._theme, "window_min_width", 960))
        target_width = int(theme_value(self._theme, "window_width", 1280))

        requested_width = max(min_width, self.winfo_reqwidth())
        width = max(min_width, target_width, requested_width)
        height = max(80, self.winfo_reqheight())
        self.geometry(f"{width}x{height}+{top_x}+{top_y}")

    def _open_child_window(self, key: str, window: tk.Misc | None) -> None:
        """Track child windows opened from the launcher."""

        if window is None:
            return
        self._child_windows = [w for w in self._child_windows if getattr(w, "winfo_exists", lambda: False)()]
        if window not in self._child_windows:
            self._child_windows.append(window)
        self._child_windows_by_key = {
            child_key: child
            for child_key, child in self._child_windows_by_key.items()
            if getattr(child, "winfo_exists", lambda: False)()
        }
        self._child_windows_by_key[key] = window
        try:
            window.bind(
                "<Destroy>",
                lambda event, win=window, win_key=key: self._forget_child_window(win_key, win, event.widget),
                add="+",
            )
        except Exception:
            pass
        try:
            window.bind(
                "<Activate>",
                lambda event, win=window, win_key=key: self._on_child_activate(win_key, win, event.widget),
                add="+",
            )
        except Exception:
            pass
        try:
            window.bind(
                "<FocusIn>",
                lambda event, win=window, win_key=key: self._on_child_activate(win_key, win, event.widget),
                add="+",
            )
        except Exception:
            pass

    def _open_or_focus_child_window(self, key: str, factory) -> None:
        """Open one child per launcher key, or focus the existing window."""

        self._child_windows_by_key = {
            child_key: child
            for child_key, child in self._child_windows_by_key.items()
            if getattr(child, "winfo_exists", lambda: False)()
        }
        existing = self._child_windows_by_key.get(key)
        if existing is not None and getattr(existing, "winfo_exists", lambda: False)():
            try:
                if hasattr(existing, "state") and existing.state() == "iconic":
                    existing.deiconify()
            except Exception:
                pass
            try:
                existing.lift()
                existing.focus_force()
            except Exception:
                pass
            try:
                place_window_below_parent_later(self, existing)
            except Exception:
                pass
            return

        self._open_child_window(key, factory())

    def _forget_child_window(self, key: str, window: tk.Misc, event_widget: tk.Misc | None = None) -> None:
        """Remove stale references to child windows that have been closed."""

        if event_widget is not None and event_widget is not window:
            return
        try:
            tracked = self._child_windows_by_key.get(key)
            if tracked is window:
                self._child_windows_by_key.pop(key, None)
        except Exception:
            pass
        try:
            self._child_windows = [w for w in self._child_windows if w is not window and getattr(w, "winfo_exists", lambda: False)()]
        except Exception:
            pass

    def _widget_belongs_to_window(self, window: tk.Misc, widget: tk.Misc | None) -> bool:
        """Return True when an event widget is the toplevel itself or one of its descendants."""

        if widget is None:
            return True
        if widget is window:
            return True
        try:
            current = widget
            while current is not None:
                if current is window:
                    return True
                parent_name = current.winfo_parent()
                if not parent_name:
                    break
                current = current.nametowidget(parent_name)
        except Exception:
            pass
        return False

    def _on_child_activate(self, key: str, window: tk.Misc, event_widget: tk.Misc | None = None) -> None:
        """Restore an activated child to the primary slot below the launcher."""

        if self._is_cascading:
            return
        if time.monotonic() < self._restore_suppressed_until:
            return
        if event_widget is not None and event_widget is not window:
            return
        tracked = self._child_windows_by_key.get(key)
        if tracked is not window or not getattr(window, "winfo_exists", lambda: False)():
            return
        if key in self._focus_restore_pending:
            return
        self._focus_restore_pending.add(key)

        def _restore() -> None:
            try:
                tracked_now = self._child_windows_by_key.get(key)
                if tracked_now is window and getattr(window, "winfo_exists", lambda: False)():
                    place_window_below_parent_later(self, window)
                    try:
                        window.lift()
                    except Exception:
                        pass
            finally:
                try:
                    self.after(250, lambda: self._focus_restore_pending.discard(key))
                except Exception:
                    self._focus_restore_pending.discard(key)

        try:
            self.after(0, _restore)
        except Exception:
            self._focus_restore_pending.discard(key)

    def _cascade_open_windows(self) -> None:
        """Cascade all open child windows following launcher button order."""

        self._child_windows = [w for w in self._child_windows if getattr(w, "winfo_exists", lambda: False)()]
        self._child_windows_by_key = {
            key: window
            for key, window in self._child_windows_by_key.items()
            if getattr(window, "winfo_exists", lambda: False)()
        }
        ordered_windows = [
            self._child_windows_by_key[key]
            for key in self._WINDOW_ORDER
            if key in self._child_windows_by_key
        ]
        self._is_cascading = True
        self._focus_restore_pending.clear()
        self._restore_suppressed_until = time.monotonic() + 1.2
        cascade_children_below_parent(
            self,
            ordered_windows,
            x_offset_step=int(theme_value(self._theme, "cascade_x_offset", 28)),
            y_offset_step=int(theme_value(self._theme, "cascade_y_offset", 28)),
            margin_left=int(theme_value(self._theme, "cascade_margin_left", 0)),
            margin_top=int(theme_value(self._theme, "cascade_margin_top", 0)),
        )
        def _finish_cascade() -> None:
            self._is_cascading = False
            self._restore_suppressed_until = 0.0
            try:
                self.lift()
                self.focus_force()
            except Exception:
                pass
        try:
            self.after(250, _finish_cascade)
        except Exception:
            self._is_cascading = False

    def _shutdown(self) -> None:
        """Dispose session and shared DB resources before closing the launcher."""

        try:
            if self.session is not None:
                log_session_event(self.session, action="logout", outcome="ok")
            if self.db_client is not None:
                fut = asyncio.run_coroutine_threadsafe(self.db_client.dispose(), _loop)
                try:
                    fut.result(timeout=2)
                except Exception:
                    pass
        finally:
            self.destroy()


def run_app() -> int:
    """Run the backoffice application entry point."""

    ctk.set_appearance_mode("light")
    ctk.set_default_color_theme("green")
    if not _acquire_single_instance_mutex():
        root = tk.Tk()
        root.withdraw()
        messagebox.showwarning(
            loc_text("launcher.already_running_title", default="Warehouse"),
            loc_text(
                "launcher.already_running_message",
                default="L'applicazione e' gia' in esecuzione.\nChiudi l'istanza aperta prima di avviarne un'altra.",
            ),
            parent=root,
        )
        _destroy_tk_root(root)
        try:
            stop_global_loop()
        except Exception:
            pass
        return 0

    db_cfg = ensure_db_config(_loop)
    if db_cfg is None:
        try:
            stop_global_loop()
        except Exception:
            pass
        return 0

    db_app = AsyncMSSQLClient(build_dsn_from_config(db_cfg))

    if BYPASS_LOGIN:
        session = _build_bypass_session()
        log_session_event(
            session,
            action="login.bypass",
            outcome="ok",
            details={"login": session.login},
        )
        bootstrap = None
    else:
        bootstrap = tk.Tk()
        bootstrap.geometry("1x1+0+0")
        bootstrap.overrideredirect(True)
        bootstrap.attributes("-alpha", 0.0)
        bootstrap.deiconify()
        bootstrap.update_idletasks()
        session = prompt_login(bootstrap, db_app)

    if session is None:
        _shutdown_runtime(bootstrap=bootstrap, dispose_db=True)
        return 0

    _destroy_tk_root(bootstrap)

    try:
        Launcher(session, db_app).mainloop()
    finally:
        _shutdown_runtime(bootstrap=None, dispose_db=True)
    return 0


if __name__ == "__main__":
    raise SystemExit(run_with_fatal_log("Warehouse Backoffice", run_app))
