"""Configuration schema for scratchpads plugin."""

import logging

from pyprland.validation import ConfigField, ConfigItems, ConfigValidator

# Null logger for validation (warnings become part of error list)
_null_logger = logging.getLogger("scratchpads.schema")
_null_logger.addHandler(logging.NullHandler())


def _validate_against_schema(config: dict, prefix: str, schema: ConfigItems) -> list[str]:
    """Validate config against schema and return errors.

    Args:
        config: Configuration dictionary to validate
        prefix: Error message prefix (e.g., "scratchpads.myterm")
        schema: Schema to validate against

    Returns:
        List of error messages (empty if valid)
    """
    validator = ConfigValidator(config, prefix, _null_logger)
    errors = validator.validate(schema)
    errors.extend(validator.warn_unknown_keys(schema))
    return errors


# Single source of truth for valid animation values (camelCase for user-facing display)
ANIMATION_CHOICES: tuple[str, ...] = (
    "",
    "fromTop",
    "fromBottom",
    "fromLeft",
    "fromRight",
    "fromTopLeft",
    "fromTopRight",
    "fromBottomLeft",
    "fromBottomRight",
)

_ANIMATION_VALID = {v.lower() for v in ANIMATION_CHOICES}


def normalize_animation(value: str) -> str:
    """Normalize an animation value: lowercase and strip '-', '_', spaces."""
    return value.lower().replace("-", "").replace("_", "").replace(" ", "")


def _validate_animation(value: str) -> list[str]:
    """Case-insensitive animation validation (ignores '-', '_' and spaces)."""
    if not isinstance(value, str) or normalize_animation(value) not in _ANIMATION_VALID:
        opts = ", ".join(repr(v) for v in ANIMATION_CHOICES if v)
        return [f"invalid value '{value}' -> Valid: '', {opts}"]
    return []


# Schema for individual scratchpad configuration
SCRATCHPAD_SCHEMA = ConfigItems(
    # Required
    ConfigField("command", str, required=True, description="Command to run (omit for unmanaged scratchpads)", category="basic"),
    # Basic
    ConfigField("class", str, default="", recommended=True, description="Window class for matching", category="basic"),
    ConfigField(
        "animation",
        str,
        default="fromTop",
        description="Animation type",
        choices=list(ANIMATION_CHOICES),
        validator=_validate_animation,
        category="basic",
    ),
    ConfigField("size", str, default="80% 80%", recommended=True, description="Window size (e.g. '80% 80%')", category="basic"),
    # Positioning
    ConfigField("position", str, default="", description="Explicit position override", category="positioning"),
    ConfigField("margin", int, default=60, description="Pixels from screen edge", category="positioning"),
    ConfigField("offset", str, default="100%", description="Hide animation distance", category="positioning"),
    ConfigField("max_size", str, default="", description="Maximum window size", category="positioning"),
    # Behavior
    ConfigField("lazy", bool, default=True, description="Start on first use", category="behavior"),
    ConfigField("pinned", bool, default=True, description="Sticky to monitor", category="behavior"),
    ConfigField("multi", bool, default=True, description="Allow multiple windows", category="behavior"),
    ConfigField("unfocus", str, default="", description="Action on unfocus ('hide' or empty)", category="behavior"),
    ConfigField("hysteresis", float, default=0.4, description="Delay before unfocus hide", category="behavior"),
    ConfigField("excludes", (list, str), default=[], description='Scratches to hide when shown (use "*" for all)', category="behavior"),
    ConfigField("restore_excluded", bool, default=False, description="Restore excluded on hide", category="behavior"),
    ConfigField("preserve_aspect", bool, default=False, description="Keep size/position across shows", category="behavior"),
    ConfigField("hide_delay", float, default=0.2, description="Delay before hide animation", category="behavior"),
    ConfigField("force_monitor", str, default="", description="Always show on specific monitor", category="behavior"),
    ConfigField("alt_toggle", bool, default=False, description="Alternative toggle for multi-monitor", category="behavior"),
    ConfigField(
        "allow_special_workspaces",
        bool,
        default=True,
        description="Allow over special workspaces",
        category="behavior",
    ),
    ConfigField("smart_focus", bool, default=False, description="Restore focus on hide", category="behavior"),
    ConfigField("close_on_hide", bool, default=False, description="Close instead of hide", category="behavior"),
    # Non-standard/troubleshooting
    ConfigField(
        "match_by",
        str,
        default="pid",
        description="Match method: pid, class, initialClass, title, initialTitle",
        category="advanced",
    ),
    ConfigField(
        "initialClass",
        str,
        default="",
        description="Match value when match_by='initialClass'",
        category="advanced",
    ),
    ConfigField(
        "initialTitle",
        str,
        default="",
        description="Match value when match_by='initialTitle'",
        category="advanced",
    ),
    ConfigField("title", str, default="", description="Match value when match_by='title'", category="advanced"),
    ConfigField("process_tracking", bool, default=True, description="Enable process management", category="advanced"),
    ConfigField(
        "skip_windowrules",
        list,
        default=[],
        description="Rules to skip",
        choices=[
            {"name": "aspect", "desc": "Avoid windowrules controlling the aspect"},
            {"name": "float", "desc": "Avoid windowrules controlling the floating state"},
            {"name": "group", "desc": "Avoid windowrules controlling group avoidance"},
            {"name": "workspace", "desc": "Avoid windowrules controlling the workspace"},
        ],
        category="advanced",
    ),
    # Template/inheritance
    ConfigField("use", str, default="", description="Inherit from another scratchpad definition", category="advanced"),
    ConfigField("monitor", dict, default={}, description="Per-monitor config overrides", category="overrides"),
)

# Schema for template sections (like [scratchpads.common]) - same fields but 'command' is not required
_TEMPLATE_SCHEMA = ConfigItems(
    *(f for f in SCRATCHPAD_SCHEMA if f.name != "command"),
    ConfigField("command", str, default="", description="Command to run", category="basic"),
)

# Schema for monitor overrides (excludes non-overridable fields)
_MONITOR_OVERRIDE_SCHEMA = ConfigItems(*(f for f in SCRATCHPAD_SCHEMA if f.name not in {"command", "use", "monitor"}))


def _validate_monitor_overrides(name: str, scratch_config: dict, errors: list[str]) -> None:
    """Validate monitor sub-subsections (per-monitor overrides)."""
    monitor_overrides = scratch_config.get("monitor")
    if not monitor_overrides or not isinstance(monitor_overrides, dict):
        return

    for monitor_name, override_config in monitor_overrides.items():
        prefix = f"scratchpads.{name}.monitor.{monitor_name}"
        if not isinstance(override_config, dict):
            errors.append(f"[{prefix}] expected dict, got {type(override_config).__name__}")
            continue

        errors.extend(_validate_against_schema(override_config, prefix, _MONITOR_OVERRIDE_SCHEMA))


def get_template_names(config: dict) -> set[str]:
    """Collect names of sections referenced via ``use`` by other sections.

    Works with both raw ``dict`` (static / GUI validation) and
    :class:`Configuration` objects (runtime validation).

    Args:
        config: The full scratchpads configuration dictionary.

    Returns:
        Set of section names that are referenced as templates.
    """
    template_names: set[str] = set()
    for name, section in config.items():
        if not isinstance(section, dict) or "." in name:
            continue
        use = section.get("use")
        if use:
            if isinstance(use, str):
                template_names.add(use)
            elif isinstance(use, list):
                template_names.update(use)
    return template_names


def is_pure_template(name: str, config: dict, template_names: set[str]) -> bool:
    """Return ``True`` if *name* is a pure template (not a real scratchpad).

    A section is considered a pure template when it is referenced by at least
    one other section via ``use`` **and** it does not define a ``command``
    of its own.

    Args:
        name: Section name to check.
        config: The full scratchpads configuration dictionary.
        template_names: Pre-computed set from :func:`get_template_names`.
    """
    section = config.get(name)
    if not isinstance(section, dict):
        return False
    return name in template_names and not section.get("command")


def validate_scratchpad_config(name: str, scratch_config: dict, *, is_template: bool = False) -> list[str]:
    """Validate a single scratchpad's configuration.

    Args:
        name: The scratchpad name (for error messages)
        scratch_config: The scratchpad's config dict
        is_template: If True, this is a template section (referenced via 'use'),
            so 'command' is not required and cross-field checks are relaxed.

    Returns:
        List of error messages (empty if valid)
    """
    errors: list[str] = []
    prefix = f"scratchpads.{name}"

    # Use relaxed schema for templates (command not required)
    schema = _TEMPLATE_SCHEMA if is_template else SCRATCHPAD_SCHEMA

    # Standard schema validation via ConfigValidator
    errors.extend(_validate_against_schema(scratch_config, prefix, schema))

    # Cross-field validations (scratchpad-specific, skip for templates)
    if not is_template:
        # Note: Using inline default because we're validating raw user config before schema is applied
        match_by = scratch_config.get("match_by", "pid")
        if match_by != "pid" and match_by not in scratch_config:
            errors.append(f"[{prefix}] match_by='{match_by}' requires '{match_by}' to be defined")

        # Validate unmanaged scratchpads (no command) require class for matching
        if not scratch_config.get("command") and not scratch_config.get("class"):
            errors.append(f"[{prefix}] unmanaged scratchpads (no command) require 'class' to be defined")

    _validate_monitor_overrides(name, scratch_config, errors)

    return errors
