"""Shared Tk/async helpers used by multiple warehouse windows.

The module bundles two concerns used throughout the GUI:

* a modal-like busy overlay shown during long-running tasks;
* an ``AsyncRunner`` that schedules coroutines on the shared loop and
  re-enters Tk safely.

The shared loop itself is defined only in :mod:`async_loop_singleton` and is
reused here instead of being recreated locally.
"""

from __future__ import annotations

import asyncio
import tkinter as tk
from typing import Any, Callable, Optional

import customtkinter as ctk

from async_loop_singleton import get_global_loop
from runtime_support import log_exception
from version_info import module_version

__version__ = module_version(__name__)

try:
    from async_msssql_query import AsyncMSSQLClient  # noqa: F401
except Exception:
    AsyncMSSQLClient = object  # type: ignore


class BusyOverlay:
    """Semi-transparent overlay used to block interaction during async tasks."""

    def __init__(self, parent: tk.Misc):
        """Bind the overlay lifecycle to the given parent widget."""
        self.parent = parent
        self._top: Optional[ctk.CTkToplevel] = None
        self._pb: Optional[ctk.CTkProgressBar] = None
        self._lbl: Optional[ctk.CTkLabel] = None
        self._bind_id = None

    def _reposition(self):
        """Resize the overlay so it keeps covering the parent toplevel."""
        if not self._top:
            return
        root = self.parent.winfo_toplevel()
        root.update_idletasks()
        x, y = root.winfo_rootx(), root.winfo_rooty()
        w, h = root.winfo_width(), root.winfo_height()
        self._top.geometry(f"{w}x{h}+{x}+{y}")

    def show(self, message="Attendere..."):
        """Display the overlay or just update its message if already visible."""
        if self._top:
            if self._lbl:
                self._lbl.configure(text=message)
            return
        root = self.parent.winfo_toplevel()
        top = ctk.CTkToplevel(root)
        self._top = top
        top.overrideredirect(True)
        try:
            top.attributes("-alpha", 0.22)
        except tk.TclError:
            pass
        try:
            top.configure(fg_color="#000000")
        except Exception:
            top.configure(bg="#000000")
        top.attributes("-topmost", True)

        wrap = ctk.CTkFrame(top, corner_radius=8)
        wrap.place(relx=0.5, rely=0.5, anchor="center")
        self._lbl = ctk.CTkLabel(wrap, text=message, font=("Segoe UI", 11, "bold"))
        self._lbl.pack(pady=(0, 10))
        self._pb = ctk.CTkProgressBar(wrap, mode="indeterminate", width=260)
        self._pb.pack(fill="x")
        try:
            self._pb.start()
        except Exception:
            pass

        self._reposition()
        self._bind_id = root.bind("<Configure>", lambda e: self._reposition(), add="+")

    def hide(self):
        """Dismiss the overlay and unregister resize bindings."""
        if self._pb:
            try:
                self._pb.stop()
            except Exception:
                pass
        self._pb = None
        if self._top:
            try:
                self._top.destroy()
            except Exception:
                pass
        self._top = None
        root = self.parent.winfo_toplevel()
        if self._bind_id:
            try:
                root.unbind("<Configure>", self._bind_id)
            except Exception:
                pass
            self._bind_id = None


class AsyncRunner:
    """Run awaitables on the shared loop and callback on Tk's main thread."""

    def __init__(self, widget: tk.Misc):
        """Capture the widget used to marshal callbacks back to Tk."""
        self.widget = widget
        self.loop = get_global_loop()

    def run(
        self,
        awaitable,
        on_success: Callable[[Any], None],
        on_error: Optional[Callable[[BaseException], None]] = None,
        busy: Optional[BusyOverlay] = None,
        message: str = "Operazione in corso...",
    ):
        """Schedule ``awaitable`` and dispatch completion callbacks in Tk."""
        if busy:
            busy.show(message)
        fut = asyncio.run_coroutine_threadsafe(awaitable, self.loop)

        def _widget_alive() -> bool:
            try:
                return bool(self.widget.winfo_exists())
            except tk.TclError:
                return False

        def _dispatch_success(res: Any) -> None:
            try:
                on_success(res)
            except BaseException as ex:
                log_exception(__name__, ex, context="AsyncRunner on_success")

        def _dispatch_error(ex: BaseException) -> None:
            if on_error:
                try:
                    on_error(ex)
                except BaseException as callback_ex:
                    log_exception(__name__, callback_ex, context="AsyncRunner on_error")
            else:
                log_exception(__name__, ex, context="AsyncRunner unhandled error")

        def _poll():
            if not _widget_alive():
                return
            if fut.done():
                if busy:
                    try:
                        busy.hide()
                    except Exception as ex:
                        log_exception(__name__, ex, context="AsyncRunner hide busy")
                try:
                    res = fut.result()
                except BaseException as ex:
                    try:
                        self.widget.after(0, lambda e=ex: _dispatch_error(e))
                    except tk.TclError:
                        log_exception(__name__, ex, context="AsyncRunner error after destroyed")
                else:
                    try:
                        self.widget.after(0, lambda r=res: _dispatch_success(r))
                    except tk.TclError as ex:
                        log_exception(__name__, ex, context="AsyncRunner success after destroyed")
            else:
                try:
                    self.widget.after(60, _poll)
                except tk.TclError:
                    return

        _poll()

    def close(self):
        """No-op kept for API compatibility with older callers."""
        pass
