"""Plain-English explanation generator for dependency conflicts.

Produces a single-line human-readable description of a conflict using
string templates.  No external APIs or LLM calls are used.
"""

from __future__ import annotations

import re

from depwhy.graph.models import Conflict, Constraint

# Sentinel used by the graph builder and detector to represent the user's
# direct requirements list (i.e. a constraint that comes from the top-level
# requirements file rather than from a transitive dependency).
_ROOT_NODE = "__root__"

_MAX_LINE = 120


def _is_user_pin(constraint: Constraint) -> bool:
    """Return True if *constraint* is an explicit user pin.

    A user pin is a constraint whose ``required_by`` equals the ``__root__``
    sentinel *and* whose specifier begins with ``==`` (an exact-version pin
    rather than a range).

    Args:
        constraint: The constraint to inspect.

    Returns:
        True when the constraint is an exact-version pin from the user's
        requirements file; False otherwise.
    """
    return constraint.required_by == _ROOT_NODE and constraint.specifier.startswith("==")


def _extract_pinned_version(specifier: str) -> str:
    """Extract the version string from an ``==x.y.z`` specifier.

    Args:
        specifier: A PEP 440 specifier string starting with ``==``.

    Returns:
        The version part, e.g. ``"2.0.0"`` from ``"==2.0.0"``.
    """
    return re.sub(r"^==", "", specifier)


def generate_explanation(conflict: Conflict) -> str:
    """Generate a plain-English one-line explanation for a dependency conflict.

    Three templates are used, selected by the shape of the conflict:

    * **User pin**: one constraint comes from the user's requirements file as
      an exact ``==`` pin.  Template::

          "{depender} requires {pkg}{spec}, but you have {pkg}=={pin} pinned"

    * **Two-way transitive conflict**: exactly two constraints, neither being a
      user pin.  Template::

          "{depender_a} requires {pkg}{spec_a}, but {depender_b} requires
           {pkg}{spec_b} — no version satisfies both"

    * **Multi-way conflict**: more than two constraints.  Template::

          "{N} packages require incompatible versions of {pkg}
           — no single version satisfies all constraints"

    The result is kept under :data:`_MAX_LINE` characters where possible.
    Internal graph terminology (nodes, edges, specifier sets) is never used in
    the returned string.

    Args:
        conflict: The :class:`~depwhy.graph.models.Conflict` to describe.

    Returns:
        A plain-English one-liner suitable for display to the user.
    """
    pkg = conflict.package
    constraints = conflict.constraints

    # Identify user-pin constraints (from __root__ with == specifier)
    user_pins = [c for c in constraints if _is_user_pin(c)]
    non_pins = [c for c in constraints if not _is_user_pin(c)]

    if user_pins:
        # Template 1: user has an explicit pin
        pin_constraint = user_pins[0]
        user_pin_version = _extract_pinned_version(pin_constraint.specifier)

        # Find a non-pin constraint to use as the depender example.
        # Fall back to the second user-pin if all constraints are pins.
        if non_pins:
            other = non_pins[0]
        else:
            # All constraints are user pins — pick any that is not pin_constraint
            others = [c for c in user_pins if c is not pin_constraint]
            other = others[0] if others else pin_constraint

        depender = other.required_by
        spec = other.specifier
        msg = f"{depender} requires {pkg}{spec}, but you have {pkg}=={user_pin_version} pinned"
        return msg[:_MAX_LINE] if len(msg) > _MAX_LINE else msg

    if len(constraints) == 2:
        # Template 2: two-way transitive conflict
        a, b = constraints[0], constraints[1]
        msg = (
            f"{a.required_by} requires {pkg}{a.specifier},"
            f" but {b.required_by} requires {pkg}{b.specifier}"
            f" \u2014 no version satisfies both"
        )
        return msg[:_MAX_LINE] if len(msg) > _MAX_LINE else msg

    # Template 3: multi-way conflict (>2 constraints or edge cases)
    n = len(constraints)
    msg = (
        f"{n} packages require incompatible versions of {pkg}"
        f" \u2014 no single version satisfies all constraints"
    )
    return msg
