"""Signature-driven example-parameter helpers and dialog UI.

This module intentionally keeps the *policy* for autogenerated example
parameters in one place so widgets such as ``Spool`` do not need to know how
to interpret Python signatures.

Current support is conservative and default-driven:

- scalar defaults such as ``bool``, ``int``, ``float``, and ``str``
- ``None`` / ``Optional[...]`` values, edited through text fields
- tuple/list defaults, edited as Python literals
- ``numpy.datetime64`` / ``numpy.timedelta64`` defaults, edited as text

Unsupported parameters are skipped rather than guessed. In particular,
variadics (``*args`` / ``**kwargs``) are ignored, and weakly-typed complex
objects without useful defaults are left out of the generated form.
"""

from __future__ import annotations

import ast
import inspect
import types
import typing
from dataclasses import dataclass

import numpy as np
from AnyQt.QtWidgets import (
    QCheckBox,
    QDialog,
    QDialogButtonBox,
    QDoubleSpinBox,
    QFormLayout,
    QLabel,
    QLineEdit,
    QPushButton,
    QSpinBox,
    QVBoxLayout,
    QWidget,
)

from derzug.utils.display import format_display
from derzug.utils.parsing import parse_timedelta_text

__all__ = (
    "ExampleParameterSpec",
    "ExampleParametersDialog",
    "build_example_call_kwargs",
    "filter_example_overrides",
    "get_example_parameter_specs",
    "parse_example_parameter_text",
)


@dataclass(frozen=True)
class ExampleParameterSpec:
    """One supported UI-exposed function parameter."""

    name: str
    kind: str
    default: object
    annotation: object
    has_default: bool
    optional: bool


def get_example_parameter_specs(fn) -> tuple[ExampleParameterSpec, ...]:
    """Return supported parameter specs derived from a callable signature."""
    signature = inspect.signature(fn)
    annotations = inspect.get_annotations(fn, eval_str=True)
    specs: list[ExampleParameterSpec] = []
    for parameter in signature.parameters.values():
        if parameter.kind not in (
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
            inspect.Parameter.KEYWORD_ONLY,
        ):
            continue
        if parameter.name in annotations:
            parameter = parameter.replace(annotation=annotations[parameter.name])
        spec = _spec_from_parameter(parameter)
        if spec is not None:
            specs.append(spec)
    return tuple(specs)


def build_example_call_kwargs(
    fn,
    overrides: dict[str, object] | None,
) -> dict[str, object]:
    """Return supported kwargs for calling one example function."""
    overrides = overrides or {}
    specs = get_example_parameter_specs(fn)
    supported_names = {spec.name for spec in specs}
    return {key: value for key, value in overrides.items() if key in supported_names}


def filter_example_overrides(
    specs: tuple[ExampleParameterSpec, ...],
    values: dict[str, object],
) -> dict[str, object]:
    """Drop values equal to defaults so only meaningful overrides are persisted."""
    output: dict[str, object] = {}
    for spec in specs:
        if spec.name not in values:
            continue
        value = values[spec.name]
        if spec.has_default and _values_equal(value, spec.default):
            continue
        output[spec.name] = value
    return output


class ExampleParametersDialog(QDialog):
    """Modal dialog auto-generated from one example function signature."""

    def __init__(
        self,
        *,
        example_name: str,
        specs: tuple[ExampleParameterSpec, ...],
        saved_values: dict[str, object] | None = None,
        parent=None,
    ) -> None:
        super().__init__(parent)
        self._example_name = example_name
        self._specs = specs
        self._saved_values = saved_values or {}
        self._inputs: dict[str, QWidget] = {}
        self._parsed_values: dict[str, object] | None = None
        self.setWindowTitle(f"Example Parameters: {example_name}")
        self.setModal(True)
        self.resize(420, 320)

        layout = QVBoxLayout(self)
        intro = QLabel(
            (
                "These controls are generated from the selected example "
                "function signature."
            ),
            self,
        )
        intro.setWordWrap(True)
        layout.addWidget(intro)

        if not specs:
            empty = QLabel("This example exposes no configurable parameters.", self)
            empty.setWordWrap(True)
            layout.addWidget(empty)
        else:
            form_host = QWidget(self)
            form = QFormLayout(form_host)
            form.setContentsMargins(0, 0, 0, 0)
            form.setSpacing(8)
            for spec in specs:
                widget = self._create_editor(spec)
                self._inputs[spec.name] = widget
                form.addRow(f"{spec.name}:", widget)
            layout.addWidget(form_host, 1)

        self._error_label = QLabel("", self)
        self._error_label.setWordWrap(True)
        self._error_label.setStyleSheet("color: #b91c1c;")
        self._error_label.hide()
        layout.addWidget(self._error_label)

        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel, self)
        self._reset_button = QPushButton("Reset to Defaults", self)
        buttons.addButton(self._reset_button, QDialogButtonBox.ButtonRole.ResetRole)
        self._apply_button = QPushButton("Apply", self)
        buttons.addButton(self._apply_button, QDialogButtonBox.ButtonRole.AcceptRole)
        buttons.rejected.connect(self.reject)
        self._reset_button.clicked.connect(self._reset_to_defaults)
        self._apply_button.clicked.connect(self._apply)
        if not specs:
            self._apply_button.setText("Close")
            self._apply_button.clicked.disconnect()
            self._apply_button.clicked.connect(self.accept)
            self._reset_button.hide()
        layout.addWidget(buttons)

    @property
    def parsed_values(self) -> dict[str, object]:
        """Return the parsed values after a successful Apply/OK."""
        return dict(self._parsed_values or {})

    def _create_editor(self, spec: ExampleParameterSpec) -> QWidget:
        """Create one editor widget from a parameter spec."""
        current_value = self._saved_values.get(spec.name, spec.default)
        if spec.kind == "bool":
            checkbox = QCheckBox(self)
            checkbox.setChecked(bool(current_value))
            return checkbox
        if spec.kind == "int":
            spin = QSpinBox(self)
            spin.setRange(-(2**31), 2**31 - 1)
            spin.setValue(int(current_value if current_value is not None else 0))
            return spin
        if spec.kind == "float":
            spin = QDoubleSpinBox(self)
            spin.setDecimals(6)
            spin.setRange(-1e12, 1e12)
            spin.setValue(float(current_value if current_value is not None else 0.0))
            return spin
        line_edit = QLineEdit(self)
        line_edit.setText(_format_parameter_text(current_value))
        line_edit.setPlaceholderText(_format_parameter_text(spec.default))
        return line_edit

    def _reset_to_defaults(self) -> None:
        """Reset all editor widgets to their signature defaults."""
        for spec in self._specs:
            widget = self._inputs[spec.name]
            value = spec.default
            if isinstance(widget, QCheckBox):
                widget.setChecked(bool(value))
            elif isinstance(widget, QSpinBox):
                widget.setValue(int(value if value is not None else 0))
            elif isinstance(widget, QDoubleSpinBox):
                widget.setValue(float(value if value is not None else 0.0))
            elif isinstance(widget, QLineEdit):
                widget.setText(_format_parameter_text(value))
        self._clear_error()

    def _apply(self) -> None:
        """Parse the form values and accept the dialog when valid."""
        try:
            parsed = self._parse_values()
        except ValueError as exc:
            self._error_label.setText(str(exc))
            self._error_label.show()
            return
        self._parsed_values = parsed
        self._clear_error()
        self.accept()

    def _clear_error(self) -> None:
        """Hide the inline validation error label."""
        self._error_label.hide()
        self._error_label.clear()

    def _parse_values(self) -> dict[str, object]:
        """Return parsed Python values from the current editor states."""
        output: dict[str, object] = {}
        for spec in self._specs:
            widget = self._inputs[spec.name]
            if isinstance(widget, QCheckBox):
                output[spec.name] = widget.isChecked()
                continue
            if isinstance(widget, QSpinBox):
                output[spec.name] = int(widget.value())
                continue
            if isinstance(widget, QDoubleSpinBox):
                output[spec.name] = float(widget.value())
                continue
            if isinstance(widget, QLineEdit):
                output[spec.name] = parse_example_parameter_text(
                    widget.text(),
                    default=spec.default,
                    annotation=spec.annotation,
                    optional=spec.optional,
                )
        return output


def parse_example_parameter_text(
    text: str,
    *,
    default: object,
    annotation: object,
    optional: bool,
) -> object:
    """Parse text input using the default value and annotation as hints.

    The parser prefers stable round-trips over aggressive coercion. For
    example, string-typed fields stay strings, while tuple/list-like text uses
    ``ast.literal_eval`` so persisted workflow state remains human-readable.
    """
    stripped = text.strip()
    if stripped == "" and optional:
        return None
    if stripped == "" and default is None:
        return None
    if isinstance(default, np.timedelta64):
        return parse_timedelta_text(stripped)
    if isinstance(default, np.datetime64):
        return np.datetime64(stripped)
    inner = _unwrap_optional(annotation)
    if inner is bool:
        lowered = stripped.lower()
        if lowered in {"true", "1", "yes", "on"}:
            return True
        if lowered in {"false", "0", "no", "off"}:
            return False
        raise ValueError(f"Could not parse boolean value {text!r}")
    if inner is int:
        return int(ast.literal_eval(stripped))
    if inner is float:
        return float(ast.literal_eval(stripped))
    if inner is str:
        return stripped
    try:
        return ast.literal_eval(stripped)
    except Exception:
        return stripped


def _spec_from_parameter(
    parameter: inspect.Parameter,
) -> ExampleParameterSpec | None:
    """Return one supported parameter spec, or None if unsupported."""
    default = parameter.default
    annotation = parameter.annotation
    has_default = default is not inspect._empty
    optional = _is_optional(annotation) or default is None
    inner = _unwrap_optional(annotation)

    if default is None and annotation is not inspect._empty:
        return ExampleParameterSpec(
            parameter.name, "text", None, annotation, has_default, True
        )

    if isinstance(default, bool) or inner is bool:
        return ExampleParameterSpec(
            parameter.name,
            "bool",
            _default_or(default, False),
            annotation,
            has_default,
            optional,
        )
    if (isinstance(default, int) and not isinstance(default, bool)) or inner is int:
        return ExampleParameterSpec(
            parameter.name,
            "int",
            _default_or(default, 0),
            annotation,
            has_default,
            optional,
        )
    if isinstance(default, float) or inner is float:
        return ExampleParameterSpec(
            parameter.name,
            "float",
            _default_or(default, 0.0),
            annotation,
            has_default,
            optional,
        )
    if isinstance(default, str) or inner is str:
        return ExampleParameterSpec(
            parameter.name,
            "text",
            _default_or(default, ""),
            annotation,
            has_default,
            optional,
        )
    if isinstance(default, tuple | list | np.timedelta64 | np.datetime64):
        return ExampleParameterSpec(
            parameter.name, "text", default, annotation, has_default, optional
        )
    return None


def _default_or(value: object, fallback: object) -> object:
    """Return the parameter default when present, otherwise a type fallback."""
    return fallback if value is inspect._empty else value


def _unwrap_optional(annotation: object) -> object:
    """Return the inner type for `Optional[T]` / `T | None` annotations."""
    origin = typing.get_origin(annotation)
    if origin in (typing.Union, types.UnionType):
        args = [arg for arg in typing.get_args(annotation) if arg is not type(None)]
        if len(args) == 1:
            return args[0]
    return annotation


def _is_optional(annotation: object) -> bool:
    """Return True when the annotation allows None."""
    return type(None) in typing.get_args(annotation)


def _format_parameter_text(value: object) -> str:
    """Return the string shown inside a text-backed parameter editor."""
    if value is None:
        return ""
    if isinstance(value, str):
        return value
    if isinstance(value, (np.timedelta64 | np.datetime64)):
        return format_display(value)
    return repr(value)


def _values_equal(left: object, right: object) -> bool:
    """Return True when two stored/default values should be treated as equal."""
    if isinstance(left, np.generic):
        left = left.item()
    if isinstance(right, np.generic):
        right = right.item()
    if isinstance(left, np.ndarray) or isinstance(right, np.ndarray):
        return bool(np.array_equal(np.asarray(left), np.asarray(right)))
    return left == right
