"""
helix_cdc/routing/route_solver.py
=================================

Solve route QUBO using classical or quantum backends.

Backends:
- neal: D-Wave simulated annealing (CPU, deterministic)
- brute: Exhaustive enumeration (small problems only)
- dwave: D-Wave LEAP hybrid (requires auth)
- ibm: IBM QAOA on Aer simulator

This module is part of the unified 3-plane substrate:
1. Classical compute (SIMD/GPU/CPU) - executor
2. Compressed-domain compute (CDNA/HXZ) - substrate
3. Optimization compute (QUBO/QPU) - planner cortex
"""

from __future__ import annotations

import time
import hashlib
from typing import Dict, List, Tuple, Optional, Any
from dataclasses import dataclass, field

import numpy as np

from .qubo_route_builder import QUBOMetadata, qubo_to_bqm_format
from .qubo_adapters import routing_qubo_to_operator_format


@dataclass
class RouteSolution:
    """Solution from route QUBO solver."""

    # Selected items
    selected_specialists: List[str]
    selected_backend: str
    selected_world: str

    # Raw solution
    bits: List[int]
    energy: float

    # Constraint satisfaction
    k_actual: int
    constraints_satisfied: bool

    # Solver info
    backend: str
    solve_time_ms: float
    seed: int

    # Certificate
    solution_sha256: str


def solve_route_qubo(
    Q: Dict[Tuple[int, int], float],
    metadata: QUBOMetadata,
    backend: str = "neal",
    seed: int = 42,
    sweeps: int = 1000,
    num_reads: int = 1,
) -> RouteSolution:
    """
    Solve route QUBO using specified backend.

    Args:
        Q: QUBO dict {(i,j): weight}
        metadata: QUBOMetadata with variable mapping
        backend: "neal" | "brute" | "dwave" | "ibm"
        seed: Random seed for determinism
        sweeps: Number of annealing sweeps (neal/dwave)
        num_reads: Number of solution samples

    Returns:
        RouteSolution with selected specialists, backend, world
    """
    t0 = time.time()

    if backend == "brute":
        bits, energy = _solve_brute_force(Q, metadata.n_vars)
    elif backend == "neal":
        bits, energy = _solve_neal(Q, metadata.n_vars, seed, sweeps, num_reads)
    elif backend == "dwave":
        bits, energy = _solve_dwave(Q, metadata.n_vars, seed, sweeps, num_reads)
    elif backend == "ibm":
        bits, energy = _solve_ibm(Q, metadata.n_vars, seed, sweeps, num_reads)
    else:
        # Default to neal
        bits, energy = _solve_neal(Q, metadata.n_vars, seed, sweeps, num_reads)

    solve_time_ms = (time.time() - t0) * 1000

    return decode_solution(bits, energy, metadata, backend, solve_time_ms, seed)


def decode_solution(
    bits: List[int],
    energy: float,
    metadata: QUBOMetadata,
    backend: str,
    solve_time_ms: float,
    seed: int,
) -> RouteSolution:
    """
    Decode raw bitstring into named solution.

    Args:
        bits: Raw solution bitstring
        energy: QUBO energy
        metadata: QUBOMetadata for variable mapping
        backend: Solver backend name
        solve_time_ms: Solve time
        seed: Random seed used

    Returns:
        RouteSolution with named selections
    """
    bits_array = np.array(bits, dtype=np.int8)

    # Decode specialists
    specialist_bits = bits_array[metadata.specialist_range[0]:metadata.specialist_range[1]]
    selected_specialists = [
        metadata.specialist_ids[i]
        for i in range(len(specialist_bits))
        if specialist_bits[i] == 1
    ]
    k_actual = len(selected_specialists)

    # Decode backend
    backend_bits = bits_array[metadata.backend_range[0]:metadata.backend_range[1]]
    backend_idx = np.argmax(backend_bits) if backend_bits.sum() > 0 else 0
    selected_backend = metadata.backend_ids[backend_idx]

    # Decode world
    world_bits = bits_array[metadata.world_range[0]:metadata.world_range[1]]
    world_idx = np.argmax(world_bits) if world_bits.sum() > 0 else 0
    selected_world = metadata.world_ids[world_idx]

    # Check constraint satisfaction
    backend_sum = int(backend_bits.sum())
    world_sum = int(world_bits.sum())
    constraints_satisfied = (
        k_actual == metadata.k_target and
        backend_sum == 1 and
        world_sum == 1
    )

    # Solution hash
    solution_sha256 = hashlib.sha256(bits_array.tobytes()).hexdigest()

    return RouteSolution(
        selected_specialists=selected_specialists,
        selected_backend=selected_backend,
        selected_world=selected_world,
        bits=bits,
        energy=energy,
        k_actual=k_actual,
        constraints_satisfied=constraints_satisfied,
        backend=backend,
        solve_time_ms=solve_time_ms,
        seed=seed,
        solution_sha256=solution_sha256,
    )


def _solve_brute_force(
    Q: Dict[Tuple[int, int], float],
    n: int,
) -> Tuple[List[int], float]:
    """
    Solve QUBO by brute force enumeration.

    Only feasible for small n (< 20).
    """
    if n > 20:
        raise ValueError(f"Brute force infeasible for n={n} (max 20)")

    min_energy = float("inf")
    best_bits = [0] * n

    # Enumerate all 2^n combinations
    for mask in range(2 ** n):
        bits = [(mask >> i) & 1 for i in range(n)]

        # Compute energy E = Σᵢⱼ Qᵢⱼ * xᵢ * xⱼ
        energy = 0.0
        for (i, j), v in Q.items():
            energy += v * bits[i] * bits[j]

        if energy < min_energy:
            min_energy = energy
            best_bits = bits

    return best_bits, min_energy


def _solve_neal(
    Q: Dict[Tuple[int, int], float],
    n: int,
    seed: int,
    sweeps: int,
    num_reads: int,
) -> Tuple[List[int], float]:
    """
    Solve QUBO using D-Wave neal simulated annealing.
    """
    try:
        import dimod
        from dwave.samplers import SimulatedAnnealingSampler
    except ImportError:
        # Fallback to brute force for small problems
        if n <= 20:
            return _solve_brute_force(Q, n)
        raise RuntimeError(
            "neal backend requires dwave-samplers: pip install dwave-samplers dimod"
        )

    # Convert to BQM format
    linear, quadratic = qubo_to_bqm_format(Q)

    # Build BQM
    bqm = dimod.BinaryQuadraticModel(linear, quadratic, 0.0, vartype=dimod.BINARY)

    # Solve
    sampler = SimulatedAnnealingSampler()
    sampleset = sampler.sample(
        bqm,
        num_reads=num_reads,
        seed=seed,
        sweeps=sweeps,
        beta_range=(0.1, 3.0),
    )

    # Extract best solution
    best = sampleset.first
    bits = [int(best.sample.get(i, 0)) for i in range(n)]
    energy = float(best.energy)

    return bits, energy


def _solve_dwave(
    Q: Dict[Tuple[int, int], float],
    n: int,
    seed: int,
    sweeps: int,
    num_reads: int,
) -> Tuple[List[int], float]:
    """
    Solve QUBO using D-Wave LEAP hybrid.

    Falls back to neal if no auth.

    Uses the unified qubo_adapters for format conversion.
    """
    try:
        # Try to import and use DWaveOperator
        import sys
        from pathlib import Path

        base = Path(__file__).resolve().parent.parent.parent
        sys.path.insert(0, str(base / "tools"))

        from quantum_dwave_operator import DWaveOperator

        # Convert QUBO to operator format using unified adapter
        operator_qubo = routing_qubo_to_operator_format(Q, n, label="route_qubo")

        op = DWaveOperator()
        receipt = op.solve_qubo(operator_qubo, backend="hybrid", num_reads=num_reads)

        bitstring = receipt["results"]["bitstring"]
        bits = [int(b) for b in bitstring]
        energy = receipt["results"]["energy"]

        return bits, energy

    except Exception:
        # Fallback to neal
        return _solve_neal(Q, n, seed, sweeps, num_reads)


def _solve_ibm(
    Q: Dict[Tuple[int, int], float],
    n: int,
    seed: int,
    sweeps: int,
    num_reads: int,
) -> Tuple[List[int], float]:
    """
    Solve QUBO using IBM QAOA on Aer simulator.

    Falls back to neal if Qiskit unavailable.

    Uses the unified qubo_adapters for format conversion.
    """
    try:
        import sys
        from pathlib import Path

        base = Path(__file__).resolve().parent.parent.parent
        sys.path.insert(0, str(base / "tools"))

        from quantum_ibm_operator import IBMQuantumOperator

        # Convert QUBO to operator format using unified adapter
        operator_qubo = routing_qubo_to_operator_format(Q, n, label="route_qubo")

        op = IBMQuantumOperator()
        reps = max(1, sweeps // 200)
        receipt = op.solve_qubo_qaoa(
            operator_qubo,
            backend="simulator",
            layers=reps,
            shots=max(100, num_reads * 100),
        )

        bitstring = receipt["results"]["bitstring"]
        bits = [int(b) for b in bitstring]
        energy = receipt["results"]["energy"]

        return bits, energy

    except Exception:
        # Fallback to neal
        return _solve_neal(Q, n, seed, sweeps, num_reads)


def evaluate_energy(
    bits: List[int],
    Q: Dict[Tuple[int, int], float],
) -> float:
    """
    Evaluate QUBO energy for given bitstring.

    Used for certificate verification.
    """
    energy = 0.0
    for (i, j), v in Q.items():
        energy += v * bits[i] * bits[j]
    return energy
