from __future__ import annotations

from dataclasses import dataclass

from ..backends.base import Backend
from ..errors import ValidationError
from ..ir.types import Instruction

# Spec for supported ops in our current IR
# name -> (num_qubits, num_params, allows_clbits)
GATE_SPECS = {
    "x": (1, 0, False),
    "sx": (1, 0, False),
    "rz": (1, 1, False),
    "cx": (2, 0, False),
    "measure": (1, 0, True),  # (q, c) pair
}


@dataclass(frozen=True)
class ValidationReport:
    ok: bool
    needs_routing: (
        bool  # True if there exists a CX that is not directly allowed by coupling map
    )
    messages: list[str]


def _check_gate_shape(inst: Instruction) -> None:
    name = inst.name
    if name not in GATE_SPECS:
        raise ValidationError(f"Unknown operation: {name}")

    q_needed, p_needed, allow_c = GATE_SPECS[name]

    if len(inst.qubits) != q_needed:
        raise ValidationError(
            f"Gate '{name}' expects {q_needed} qubit(s), got {len(inst.qubits)}: {inst.qubits}"
        )

    if len(inst.params) != p_needed:
        raise ValidationError(
            f"Gate '{name}' expects {p_needed} parameter(s), got {len(inst.params)}: {inst.params}"
        )

    if inst.clbits is not None and not allow_c:
        raise ValidationError(
            f"Gate '{name}' does not take classical bits (got {inst.clbits})"
        )

    if inst.clbits is None and allow_c:
        # measure must provide clbits (we encode them in Instruction.clbits)
        raise ValidationError("Measure must specify destination classical bit(s)")


def _check_index_bounds(inst: Instruction, n_qubits: int, n_clbits: int) -> None:
    # Qubit indices must be within 0..n_qubits-1 and distinct for multi-qubit ops
    for q in inst.qubits:
        if q < 0 or q >= n_qubits:
            raise ValidationError(f"Qubit index out of range in op {inst}: q={q}")

    if len(inst.qubits) != len(set(inst.qubits)):
        raise ValidationError(f"Repeated qubit index in op {inst}")

    # Classical indices (if any) must be within 0..n_clbits-1 and distinct
    if inst.clbits is not None:
        for c in inst.clbits:
            if c < 0 or c >= n_clbits:
                raise ValidationError(
                    f"Classical bit index out of range in op {inst}: c={c}"
                )
        if len(inst.clbits) != len(set(inst.clbits)):
            raise ValidationError(f"Repeated classical bit index in op {inst}")


def _cx_needs_routing(
    control: int, target: int, coupling_map: list[tuple[int, int]]
) -> bool:
    """Return True if (control,target) is not directly supported by coupling_map (undirected)."""
    if not coupling_map:
        # If backend provides no map, assume fully connected (dev convenience)
        return False
    return (control, target) not in coupling_map and (
        target,
        control,
    ) not in coupling_map


def validate(circuit, backend: Backend) -> ValidationReport:
    """
    Validate a circuit against basic rules and the backend's size/basis.

    - Checks gate names, arity, parameter counts.
    - Checks qubit/clbit index ranges and duplicates.
    - Checks that every op name is in backend.target.basis_gates.
    - Does NOT fail for CX connectivity; instead marks needs_routing=True when a CX
      doesn't match the coupling map (routing pass will handle it in 5.3).
    """
    msgs: list[str] = []
    n_qubits = circuit.n_qubits
    n_clbits = getattr(circuit, "n_clbits", 0)
    tgt = backend.target

    if n_qubits > tgt.n_qubits:
        raise ValidationError(
            f"Circuit uses {n_qubits} qubits but backend supports only {tgt.n_qubits}"
        )

    needs_routing = False
    basis = set(tgt.basis_gates)

    for inst in circuit.instructions:
        # Name is supported by our IR and by backend basis
        _check_gate_shape(inst)
        if inst.name not in basis:
            raise ValidationError(
                f"Gate '{inst.name}' not in backend basis_gates: {sorted(basis)}"
            )

        # Index ranges / duplicates
        _check_index_bounds(inst, n_qubits, n_clbits)

        # Connectivity hint (don’t fail; mark for routing)
        if inst.name == "cx":
            c, t = inst.qubits
            if _cx_needs_routing(c, t, tgt.coupling_map):
                needs_routing = True

    if needs_routing:
        msgs.append("Routing required: at least one CX not on a coupling edge.")

    return ValidationReport(ok=True, needs_routing=needs_routing, messages=msgs)
