"""The @entrypoint decorator for declarative app entry points."""

from __future__ import annotations

import asyncio
import tempfile
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, cast, overload

from qtpy.QtCore import QFile, QIODevice, QTextStream
from qtpy.QtWidgets import QApplication, QWidget

from qtpie.styles.watcher import QssWatcher, ScssWatcher

# Import App and run_app lazily to avoid circular imports
_App: type | None = None
_run_app_fn: Callable[..., int] | None = None


def _get_app_class() -> type:
    """Lazily import App class to avoid circular imports."""
    global _App
    if _App is None:
        from qtpie.app import App

        _App = App
    return _App


def _get_run_app_fn() -> Callable[..., int]:
    """Lazily import run_app function to avoid circular imports."""
    global _run_app_fn
    if _run_app_fn is None:
        from qtpie.app import run_app

        _run_app_fn = run_app
    return _run_app_fn


@dataclass(frozen=True)
class EntryConfig:
    """Configuration stored by @entrypoint decorator."""

    dark_mode: bool = False
    light_mode: bool = False
    title: str | None = None
    size: tuple[int, int] | None = None
    stylesheet: str | None = None
    watch_stylesheet: bool = False
    scss_search_paths: tuple[str, ...] = field(default_factory=tuple)
    window: type[QWidget] | None = None


# Attribute name for storing entry config
ENTRY_CONFIG_ATTR = "_qtpie_entry_config"


def _is_main_module(target: Any) -> bool:
    """Check if target's module is __main__."""
    return getattr(target, "__module__", None) == "__main__"


def _should_auto_run(target: Any) -> bool:
    """Check if we should auto-run the entry point."""
    return _is_main_module(target) and QApplication.instance() is None


def _load_qrc_stylesheet(qrc_path: str) -> str:
    """Load stylesheet content from a QRC resource path."""
    qrc_file = QFile(qrc_path)
    if qrc_file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
        stream = QTextStream(qrc_file)
        content = stream.readAll()
        qrc_file.close()
        return content
    return ""


def _compile_scss_to_string(scss_path: str, search_paths: list[str]) -> str:
    """Compile SCSS file to a QSS string."""
    from scss import Compiler  # type: ignore[import-untyped]

    scss_file = Path(scss_path)
    if not scss_file.exists():
        return ""

    compiler = Compiler(search_path=search_paths)
    return cast(str, compiler.compile(str(scss_file)))  # pyright: ignore[reportUnknownMemberType]


def _apply_stylesheet(app: QApplication, config: EntryConfig) -> QssWatcher | ScssWatcher | None:
    """
    Apply stylesheet to the application based on config.

    Returns a watcher if watch_stylesheet=True, otherwise None.
    """
    if not config.stylesheet:
        return None

    stylesheet_path = config.stylesheet
    is_qrc = stylesheet_path.startswith(":/")
    is_scss = stylesheet_path.endswith(".scss")

    # Determine search paths for SCSS
    if config.scss_search_paths:
        # Use explicit paths only
        search_paths = list(config.scss_search_paths)
    elif is_scss:
        # Auto-add the SCSS file's parent folder
        search_paths = [str(Path(stylesheet_path).parent)]
    else:
        search_paths = []

    if config.watch_stylesheet and not is_qrc:
        # Set up a watcher - it will handle initial load too
        if is_scss:
            # Create a temp file for compiled QSS
            temp_dir = Path(tempfile.gettempdir()) / "qtpie_scss"
            temp_dir.mkdir(exist_ok=True)
            qss_path = str(temp_dir / f"{Path(stylesheet_path).stem}.qss")
            return ScssWatcher(app, stylesheet_path, qss_path, search_paths or None)
        else:
            # QSS file
            return QssWatcher(app, stylesheet_path)

    # One-shot load (no watching)
    if is_qrc:
        content = _load_qrc_stylesheet(stylesheet_path)
    elif is_scss:
        content = _compile_scss_to_string(stylesheet_path, search_paths)
    else:
        # Regular QSS file
        qss_file = Path(stylesheet_path)
        content = qss_file.read_text() if qss_file.exists() else ""

    if content:
        app.setStyleSheet(content)

    return None


def _run_entrypoint(target: Any, config: EntryConfig) -> None:
    """Execute the entry point."""
    App = _get_app_class()
    run_app_fn = _get_run_app_fn()

    # Create the App instance
    app_kwargs: dict[str, Any] = {}
    if config.dark_mode:
        app_kwargs["dark_mode"] = True
    if config.light_mode:
        app_kwargs["light_mode"] = True

    # Determine what kind of target we have
    is_function = callable(target) and not isinstance(target, type)
    is_class = isinstance(target, type)
    is_app_subclass = is_class and issubclass(target, QApplication)

    window: QWidget | None = None
    app: QApplication

    # Keep watcher alive for duration of app
    _watcher: QssWatcher | ScssWatcher | None = None

    if is_app_subclass:
        # Target is an App or QApplication subclass
        app = cast(QApplication, target())

        # Apply stylesheet to the app
        _watcher = _apply_stylesheet(app, config)

        # Call create_window if it exists and is overridden
        create_window_method: Callable[[], QWidget | None] | None = getattr(app, "create_window", None)
        if create_window_method is not None and callable(create_window_method):
            result = create_window_method()
            if isinstance(result, QWidget):
                window = result
    else:
        # Create a default App
        app = App(**app_kwargs)

        # Apply stylesheet to the app
        _watcher = _apply_stylesheet(app, config)

        if is_function:
            # Target is a function
            func = cast(Callable[..., Any], target)
            if asyncio.iscoroutinefunction(func):
                # Async function - need to run it in the event loop
                # We'll run it before the main loop
                import qasync  # type: ignore[import-untyped]

                loop = qasync.QEventLoop(app)
                asyncio.set_event_loop(loop)
                with loop:
                    result: Any = loop.run_until_complete(func())  # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
                    if isinstance(result, QWidget):
                        window = result
                    # Now continue with the event loop
                    quit_event = asyncio.Event()
                    app.aboutToQuit.connect(quit_event.set)
                    if window is not None:
                        _apply_window_config(window, config)
                        window.show()
                    loop.run_until_complete(quit_event.wait())  # pyright: ignore[reportUnknownMemberType]
                return
            else:
                # Sync function
                result = func()
                if isinstance(result, QWidget):
                    window = result
        elif is_class:
            # Target is a widget class (decorated with @widget or @window)
            widget_cls = cast(type[QWidget], target)
            window = widget_cls()

        # Handle window= parameter
        if config.window is not None and window is None:
            window = config.window()

    # Apply config to window
    if window is not None:
        _apply_window_config(window, config)
        window.show()

    # Run the app using the standalone helper
    run_app_fn(app)


def _apply_window_config(window: QWidget, config: EntryConfig) -> None:
    """Apply configuration to the window."""
    if config.title is not None:
        window.setWindowTitle(config.title)
    if config.size is not None:
        window.resize(*config.size)


# Overload order matters: type[T] must come before Callable since classes are callable
@overload
def entrypoint[T](
    _target: type[T],
    *,
    dark_mode: bool = ...,
    light_mode: bool = ...,
    title: str | None = ...,
    size: tuple[int, int] | None = ...,
    stylesheet: str | None = ...,
    watch_stylesheet: bool = ...,
    scss_search_paths: list[str] | None = ...,
    window: type[QWidget] | None = ...,
) -> type[T]: ...


@overload
def entrypoint[T](
    _target: Callable[..., T],
    *,
    dark_mode: bool = ...,
    light_mode: bool = ...,
    title: str | None = ...,
    size: tuple[int, int] | None = ...,
    stylesheet: str | None = ...,
    watch_stylesheet: bool = ...,
    scss_search_paths: list[str] | None = ...,
    window: type[QWidget] | None = ...,
) -> Callable[..., T]: ...


@overload
def entrypoint[T](
    _target: None = None,
    *,
    dark_mode: bool = ...,
    light_mode: bool = ...,
    title: str | None = ...,
    size: tuple[int, int] | None = ...,
    stylesheet: str | None = ...,
    watch_stylesheet: bool = ...,
    scss_search_paths: list[str] | None = ...,
    window: type[QWidget] | None = ...,
) -> Callable[[Callable[..., T] | type[T]], Callable[..., T] | type[T]]: ...


def entrypoint(
    _target: Callable[..., Any] | type | None = None,
    *,
    dark_mode: bool = False,
    light_mode: bool = False,
    title: str | None = None,
    size: tuple[int, int] | None = None,
    stylesheet: str | None = None,
    watch_stylesheet: bool = False,
    scss_search_paths: list[str] | None = None,
    window: type[QWidget] | None = None,
) -> Any:
    """
    Decorator that marks a function or class as the application entry point.

    When the decorated item's module is __main__ (i.e., the file is run directly),
    this decorator will automatically create an App, run the entry point, and
    start the event loop.

    When imported (module is not __main__), the decorator does nothing except
    store configuration, allowing the class/function to be used normally.

    Args:
        dark_mode: Enable dark mode color scheme.
        light_mode: Enable light mode color scheme.
        title: Window title to set.
        size: Window size as (width, height) tuple.
        stylesheet: Path to stylesheet. Can be:
            - QRC path (e.g., ":/styles/app.qss") - loads from Qt resources
            - QSS file (e.g., "styles.qss") - loads from filesystem
            - SCSS file (e.g., "styles.scss") - compiles and applies
        watch_stylesheet: If True, hot-reload stylesheet on file changes.
            Not applicable to QRC paths.
        scss_search_paths: Directories for SCSS @import resolution.
            If not provided, the SCSS file's parent folder is used.
        window: A widget class to instantiate as the main window.

    Examples:
        # Simplest - function returning a widget
        @entrypoint
        def main():
            return QLabel("Hello World!")

        # With configuration
        @entrypoint(dark_mode=True, title="My App", size=(800, 600))
        def main():
            return MyWidget()

        # On a @widget class
        @entrypoint
        @widget
        class MyApp(QWidget):
            label: QLabel = make(QLabel, "Hello!")

        # On a @window class
        @entrypoint(dark_mode=True)
        @window(title="My Application")
        class MyApp(QMainWindow):
            ...

        # Async function
        @entrypoint
        async def main():
            data = await fetch_data()
            return DataViewer(data)

        # App subclass with lifecycle hooks
        @entrypoint
        class MyApp(App):
            def setup(self):
                self.load_stylesheet("styles.qss")

            def create_window(self):
                return MyMainWindow()
    """
    config = EntryConfig(
        dark_mode=dark_mode,
        light_mode=light_mode,
        title=title,
        size=size,
        stylesheet=stylesheet,
        watch_stylesheet=watch_stylesheet,
        scss_search_paths=tuple(scss_search_paths) if scss_search_paths else (),
        window=window,
    )

    def decorator(target: Callable[..., Any] | type) -> Callable[..., Any] | type:
        # Store config on target
        setattr(target, ENTRY_CONFIG_ATTR, config)

        # Check if we should auto-run
        if _should_auto_run(target):
            _run_entrypoint(target, config)

        return target

    if _target is not None:
        # Called without parentheses: @entrypoint
        return decorator(_target)

    # Called with parentheses: @entrypoint(...)
    return decorator
