"""Task discovery and filtering — find and resolve tasks from a tasks directory.

Called by: cli.py, eval.py, optimize.py
"""

from __future__ import annotations

import difflib
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Iterator, KeysView

import click


@dataclass(frozen=True)
class Task:
    """A discovered task — typed alternative to raw dicts.

    Supports dict-style access (t["task_name"], t.get("task_path")) for
    backward compatibility with code that treats tasks as dicts.
    """

    name: str
    path: Path
    tags: list[str] = field(default_factory=list)

    # -- dict compatibility layer --
    # Lets existing code like t["task_name"] and t.get("task_path") keep working.

    def __getitem__(self, key: str) -> Any:
        return self._as_dict()[key]

    def get(self, key: str, default: Any = None) -> Any:
        return self._as_dict().get(key, default)

    def __contains__(self, key: object) -> bool:
        return key in self._as_dict()

    def __iter__(self) -> Iterator[str]:
        return iter(self._as_dict())

    def keys(self) -> KeysView[str]:
        return self._as_dict().keys()

    def _as_dict(self) -> dict[str, Any]:
        return {
            "task_name": self.name,
            "task_path": str(self.path),
        }


def _did_you_mean(name: str, available: list[str], n: int = 3) -> str:
    """Return a 'Did you mean: ...' suffix, or empty string if no close match."""
    matches = difflib.get_close_matches(name, available, n=n, cutoff=0.5)
    if matches:
        return " Did you mean: " + ", ".join(matches) + "?"
    return ""


def _parse_tag_groups(task_tags: list[str]) -> list[set[str]]:
    """Parse tag arguments into AND-groups for OR-of-AND filtering.

    Each element may be comma-separated (AND within the group).
    Multiple elements are combined with OR across groups.

    Examples:
        ["gmail", "gcal"]           → [{gmail}, {gcal}]         — OR
        ["gmail,adversarial"]       → [{gmail, adversarial}]    — AND
        ["gmail,adversarial", "gcal"] → [{gmail, adversarial}, {gcal}] — mixed
    """
    groups: list[set[str]] = []
    for entry in task_tags:
        group = {t.strip() for t in entry.split(",") if t.strip()}
        if group:
            groups.append(group)
    return groups


def discover_tasks(
    tasks_dir: Path,
    task_tags: list[str] | None = None,
) -> list[Task]:
    """Scan tasks directory and return task entries.

    Args:
        tasks_dir: Directory containing task subdirectories.
        task_tags: If provided, each element is a comma-separated AND-group.
            Multiple ``--task-tag`` flags are combined with OR across groups.
            E.g. ``--task-tag gmail,adversarial --task-tag gcal`` matches tasks
            that have (gmail AND adversarial) OR gcal.

    Returns:
        List of Task objects (also support dict-style access via t["task_name"]).
    """
    import tomllib

    tag_groups = _parse_tag_groups(task_tags) if task_tags else None
    tasks: list[Task] = []
    tasks_dir = Path(tasks_dir)
    for task_dir in sorted(tasks_dir.iterdir()):
        toml_path = task_dir / "task.toml"
        if task_dir.is_dir() and toml_path.exists():
            try:
                cfg = tomllib.loads(toml_path.read_text())
                name = cfg.get("name", task_dir.name)
            except Exception:
                name = task_dir.name
                cfg = {}

            task_tags_list = cfg.get("metadata", {}).get("tags", [])

            # OR-of-AND: task must satisfy at least one AND-group
            if tag_groups:
                task_tag_set = set(task_tags_list)
                if not any(group.issubset(task_tag_set) for group in tag_groups):
                    continue

            tasks.append(Task(
                name=name,
                path=task_dir,
                tags=task_tags_list,
            ))
    return tasks


def resolve_tasks(
    tasks_dir: Path,
    task_name: str | None = None,
    task_tags: tuple[str, ...] | list[str] = (),
) -> list[Task]:
    """Discover and filter tasks.

    Args:
        tasks_dir: Root directory containing task subdirectories.
        task_name: Optional single task name filter (mutually exclusive with task_tags).
        task_tags: Optional tag list with OR-of-AND semantics. Each element may
            contain commas for AND within a group; multiple elements are OR'd.

    Returns:
        List of Task objects, filtered as requested.

    Raises:
        click.BadParameter: If both --task and --task-tag are provided.
        SystemExit: If no tasks found or task_name doesn't match.
    """
    if task_name and task_tags:
        raise click.BadParameter(
            "Use only one of --task or --task-tag to filter tasks."
        )

    all_tasks = discover_tasks(
        tasks_dir, task_tags=list(task_tags) if task_tags else None
    )
    if not all_tasks:
        hint = (
            f"No tasks found in {tasks_dir}."
            f" Check that the path contains task subdirectories (each with a task.toml)."
        )
        if task_tags:
            hint += f" Tags filter active: {list(task_tags)}."
        hint += " Run 'clawbench check --tasks-dir <path>' to validate."
        raise SystemExit(hint)

    if task_name:
        _get_name = lambda t: t.name if isinstance(t, Task) else t["task_name"]
        matched = [t for t in all_tasks if _get_name(t) == task_name]
        if not matched:
            available = sorted(_get_name(t) for t in all_tasks)
            suggestion = _did_you_mean(task_name, available)
            raise SystemExit(
                f"Task '{task_name}' not found in {tasks_dir}.{suggestion}\n"
                f"Available ({len(available)}): {', '.join(available)}"
            )
        return matched

    return all_tasks
