"""Complexity scoring heuristics for decomposed tasks.

Provides ``compute_complexity_score`` which evaluates a task dict and returns
a float in [1.0, 10.0] based on description length, action keywords, file
count, subtask count, and trivial-keyword penalties.

``is_trivial`` is a convenience predicate built on top of the score.
"""

from __future__ import annotations

import logging

log = logging.getLogger(__name__)

# ── Constants ────────────────────────────────────────────────────────

COMPLEXITY_THRESHOLD: float = 2

TRIVIAL_KEYWORDS: list[str] = [
    "add dependency",
    "update config",
    "bump version",
    "add import",
    "update requirements",
    "pin version",
    "add entry",
    "update changelog",
    "add comment",
]

# Action keywords that suggest non-trivial engineering work.
_ACTION_KEYWORDS: list[str] = [
    "implement",
    "refactor",
    "architect",
    "design",
    "migrate",
    "integrate",
]


# ── Core scoring ─────────────────────────────────────────────────────


def compute_complexity_score(
    task: dict,
    trivial_keywords: list[str] | None = None,
) -> float:
    """Compute a complexity score for *task*, clamped to [1.0, 10.0].

    Heuristics
    ----------
    * **Description score (0-4)**: word count bands.
    * **Action keyword boost (+1)**: description contains an action verb.
    * **File count score (0-2)**: based on ``task["files"]`` length.
    * **Subtask score (0-2)**: based on ``task["subtasks"]`` length.
    * **Keyword penalty (-3 to 0)**: description matches a trivial keyword.

    Parameters
    ----------
    task:
        A dict with optional keys ``description``, ``files``, ``subtasks``.
    trivial_keywords:
        Override the default ``TRIVIAL_KEYWORDS`` list.  Pass an empty list
        to disable the penalty entirely.

    Returns
    -------
    float
        Score in [1.0, 10.0].
    """
    if trivial_keywords is None:
        trivial_keywords = TRIVIAL_KEYWORDS

    description = task.get("description") or ""
    if not isinstance(description, str):
        description = ""

    desc_lower = description.lower()
    word_count = len(description.split()) if description.strip() else 0

    # ── Description score (0-4) ──────────────────────────────────
    if word_count <= 5:
        desc_score = 1.0
    elif word_count <= 20:
        desc_score = 2.0
    elif word_count <= 50:
        desc_score = 3.0
    else:
        desc_score = 4.0

    # ── Action keyword boost (+1) ────────────────────────────────
    action_boost = 0.0
    for kw in _ACTION_KEYWORDS:
        if kw in desc_lower:
            action_boost = 1.0
            break

    # ── File count score (0-2) ───────────────────────────────────
    files = task.get("files", [])
    if not isinstance(files, list):
        files = []
    # Filter out non-string entries gracefully
    file_count = sum(1 for f in files if isinstance(f, str))
    if file_count == 0:
        file_score = 0.0
    elif file_count <= 3:
        file_score = 1.0
    else:
        file_score = 2.0

    # ── Subtask score (0-2) ──────────────────────────────────────
    subtasks = task.get("subtasks", [])
    if not isinstance(subtasks, list):
        subtasks = []
    subtask_count = len(subtasks)
    if subtask_count == 0:
        subtask_score = 0.0
    elif subtask_count <= 2:
        subtask_score = 1.0
    else:
        subtask_score = 2.0

    # ── Trivial keyword penalty (-3 to 0) ────────────────────────
    trivial_penalty = 0.0
    for kw in trivial_keywords:
        if kw.lower() in desc_lower:
            trivial_penalty = -3.0
            break  # applied once, not per keyword

    # ── Sum and clamp ────────────────────────────────────────────
    raw = desc_score + action_boost + file_score + subtask_score + trivial_penalty

    score = max(1.0, min(10.0, raw))

    log.debug(
        "complexity score=%.1f (desc=%.1f action=%.1f files=%.1f "
        "subtasks=%.1f trivial=%.1f raw=%.1f) words=%d files=%d subtasks=%d",
        score,
        desc_score,
        action_boost,
        file_score,
        subtask_score,
        trivial_penalty,
        raw,
        word_count,
        file_count,
        subtask_count,
    )

    return score


# ── Convenience predicate ────────────────────────────────────────────


def is_trivial(
    task: dict,
    threshold: float = COMPLEXITY_THRESHOLD,
) -> bool:
    """Return ``True`` if *task*'s complexity score is below *threshold*.

    Special cases:
    * ``threshold <= 0`` — never trivial (returns ``False``).
    * ``threshold > 10`` — always trivial (returns ``True``), logs a warning.
    """
    if threshold <= 0:
        return False

    if threshold > 10:
        log.warning(
            "is_trivial called with threshold=%.1f (> 10); "
            "all tasks will be considered trivial",
            threshold,
        )
        return True

    return compute_complexity_score(task) < threshold
