"""
classical_solver.py -- Classical Fallback Solvers for QUBO

Provides classical optimization baselines for comparison with QAOA:
    - HiGHS MIP solver (exact for small problems)
    - Simulated Annealing (SA) heuristic
    - Both return solutions in the same format as qaoa_solve() for comparison.

Reference: qubo_highs.py -- solve_qubo_highs, _sa_solve_qubo
"""

from __future__ import annotations

import math
import random
import time
from typing import Any, Optional

import numpy as np

from qubo_builder import QUBO, extract_dominant_state


# =========================================================================
# HiGHS MIP Solver
# =========================================================================

def solve_highs(qubo: QUBO, time_limit: float = 60.0) -> dict:
    """Solve QUBO using HiGHS MIP solver.

    Converts QUBO to MIP via linearization of bilinear terms:
        y_{ij} = x_i · x_j  with McCormick inequalities:
        y_{ij} ≤ x_i, y_{ij} ≤ x_j, y_{ij} ≥ x_i + x_j - 1

    Returns solution matching qaoa_solve() format:
        {
            'optimal_state': str,      # Hachimoji state name
            'energy': float,
            'solution': list[int],
            'method': 'highs',
            'status': str,
            'runtime_s': float,
        }
    """
    t0 = time.time()

    try:
        import highspy
        HAS_HIGHS = True
    except ImportError:
        HAS_HIGHS = False

    if not HAS_HIGHS:
        # Fall back to simulated annealing
        return solve_sa(qubo, time_limit=time_limit)

    n = qubo.n

    # Separate linear and quadratic terms
    linear: dict[int, float] = {}
    quadratic: dict[tuple[int, int], float] = {}
    for (i, j), qij in qubo.matrix.items():
        if i == j:
            linear[i] = linear.get(i, 0.0) + qij
        else:
            key = (min(i, j), max(i, j))
            quadratic[key] = quadratic.get(key, 0.0) + qij

    # Variables: x_0..x_{n-1} (binary), y_n.. (continuous for bilinear)
    y_map: dict[tuple[int, int], int] = {}
    y_idx = n
    for (i, j) in quadratic:
        y_map[(i, j)] = y_idx
        y_idx += 1

    num_vars = y_idx
    num_rows = len(quadratic) * 3  # 3 McCormick constraints per pair

    # Build model
    model = highspy.HighsModel()
    lp = model.lp_
    lp.num_col_ = num_vars
    lp.num_row_ = num_rows

    # Objective: min Σ a_i x_i + Σ b_{ij} y_{ij}
    obj = np.zeros(num_vars)
    for i, coeff in linear.items():
        obj[i] = coeff
    for (i, j), coeff in quadratic.items():
        obj[y_map[(i, j)]] = coeff
    lp.col_cost_ = obj

    # Bounds: x binary [0,1], y [0,1]
    lp.col_lower_ = np.zeros(num_vars)
    lp.col_upper_ = np.ones(num_vars)

    # Integrality: x binary, y continuous
    lp.integrality_ = [highspy.HighsVarType.kInteger] * n + \
                       [highspy.HighsVarType.kContinuous] * (num_vars - n)

    # Build constraint matrix (CSC format)
    col_entries: dict[int, list[tuple[int, float]]] = {}
    row_idx = 0

    for (i, j), yi in y_map.items():
        # y_{ij} ≤ x_i
        if yi not in col_entries:
            col_entries[yi] = []
        if i not in col_entries:
            col_entries[i] = []
        col_entries[yi].append((row_idx, 1.0))
        col_entries[i].append((row_idx, -1.0))
        row_idx += 1

        # y_{ij} ≤ x_j
        if j not in col_entries:
            col_entries[j] = []
        col_entries[yi].append((row_idx, 1.0))
        col_entries[j].append((row_idx, -1.0))
        row_idx += 1

        # y_{ij} ≥ x_i + x_j - 1  =>  -y_{ij} + x_i + x_j ≤ 1
        col_entries[yi].append((row_idx, -1.0))
        col_entries[i].append((row_idx, 1.0))
        col_entries[j].append((row_idx, 1.0))
        row_idx += 1

    starts = []
    indices_list = []
    values_list = []
    nnz = 0
    for col in range(num_vars):
        starts.append(nnz)
        if col in col_entries:
            for ridx, val in col_entries[col]:
                indices_list.append(ridx)
                values_list.append(val)
                nnz += 1
    starts.append(nnz)

    lp.a_matrix_.format_ = highspy.MatrixFormat.kColwise
    lp.a_matrix_.start_ = np.array(starts, dtype=np.int32)
    lp.a_matrix_.index_ = np.array(indices_list, dtype=np.int32) if indices_list else np.array([], dtype=np.int32)
    lp.a_matrix_.value_ = np.array(values_list) if values_list else np.array([])

    # Row bounds: all ≤ 0 or ≤ 1
    row_lower = []
    row_upper = []
    for _ in range(len(quadratic)):
        row_lower.append(-1e30)
        row_upper.append(0.0)   # y - x ≤ 0
        row_lower.append(-1e30)
        row_upper.append(0.0)   # y - x' ≤ 0
        row_lower.append(-1e30)
        row_upper.append(1.0)   # -y + x + x' ≤ 1

    lp.row_lower_ = np.array(row_lower)
    lp.row_upper_ = np.array(row_upper)

    # Solve
    h = highspy.Highs()
    h.setOptionValue("time_limit", time_limit)
    h.setOptionValue("output_flag", False)
    h.passModel(model)
    h.run()

    sol = h.getSolution()
    x_vals = sol.col_value

    solution = [int(round(max(0, min(1, x_vals[i])))) for i in range(n)]
    energy = qubo.energy(solution)
    runtime = time.time() - t0

    # Get status
    status_val = h.getInfoValue("primal_solution_status")[1]
    status_map = {0: "unknown", 1: "infeasible", 2: "feasible", 3: "optimal"}
    status_str = status_map.get(status_val, f"status_{status_val}")

    return {
        "optimal_state": extract_dominant_state(solution),
        "energy": energy,
        "solution": solution,
        "method": "highs",
        "status": status_str,
        "runtime_s": round(runtime, 4),
    }


# =========================================================================
# Simulated Annealing
# =========================================================================

def solve_sa(
    qubo: QUBO,
    time_limit: float = 5.0,
    initial_temp: float = 10.0,
    cooling_rate: float = 0.9995,
    seed: int = 42,
) -> dict:
    """Solve QUBO with Simulated Annealing.

    Standard SA: flip random bits, accept if energy decreases or
    with probability exp(-ΔE/T).

    Returns solution matching qaoa_solve() format:
        {
            'optimal_state': str,      # Hachimoji state name
            'energy': float,
            'solution': list[int],
            'method': 'sa',
            'iterations': int,
            'runtime_s': float,
        }
    """
    t0 = time.time()
    rng = random.Random(seed)

    n = qubo.n
    Q_dict = dict(qubo.matrix)

    def _energy(x):
        e = qubo.offset
        for (i, j), qij in Q_dict.items():
            e += qij * x[i] * x[j]
        return e

    # Initialize random solution
    x = [rng.randint(0, 1) for _ in range(n)]
    current_energy = _energy(x)
    best_x = x[:]
    best_energy = current_energy

    T = initial_temp
    iterations = 0
    n_vals = list(range(n))

    while (time.time() - t0) < time_limit:
        i = rng.choice(n_vals)
        x[i] = 1 - x[i]  # flip bit
        new_energy = _energy(x)
        delta = new_energy - current_energy

        if delta < 0 or rng.random() < math.exp(-delta / max(T, 1e-10)):
            current_energy = new_energy
            if current_energy < best_energy:
                best_x = x[:]
                best_energy = current_energy
        else:
            x[i] = 1 - x[i]  # revert

        T *= cooling_rate
        iterations += 1

    runtime = time.time() - t0

    return {
        "optimal_state": extract_dominant_state(best_x),
        "energy": best_energy,
        "solution": best_x,
        "method": "sa",
        "iterations": iterations,
        "runtime_s": round(runtime, 4),
    }


# =========================================================================
# Unified Solver Interface
# =========================================================================

def solve_classical(qubo: QUBO, method: str = "highs", **kwargs) -> dict:
    """Solve QUBO with classical methods for comparison.

    Methods:
        - "highs": HiGHS MIP solver (exact, falls back to SA)
        - "sa": Simulated annealing heuristic

    Returns solution matching qaoa_solve() format for comparison.
    """
    if method == "highs":
        return solve_highs(qubo, **{k: v for k, v in kwargs.items() if k in ["time_limit"]})
    elif method == "sa":
        return solve_sa(qubo, **{k: v for k, v in kwargs.items() if k in ["time_limit", "initial_temp", "cooling_rate", "seed"]})
    else:
        raise ValueError(f"Unknown method: {method}. Use 'highs' or 'sa'.")


def compare_solvers(qubo: QUBO, time_limit: float = 2.0) -> dict:
    """Run all classical solvers and compare results.

    Returns:
        {
            "highs": {...},
            "sa": {...},
            "best": {"method": str, "energy": float},
            "agreement": bool,  # whether all solvers agree on state
        }
    """
    highs_result = solve_highs(qubo, time_limit=time_limit)
    sa_result = solve_sa(qubo, time_limit=time_limit)

    # Find best
    results = {"highs": highs_result, "sa": sa_result}
    best_method = min(results, key=lambda m: results[m]["energy"])

    # Check agreement
    states = [r["optimal_state"] for r in results.values()]
    agreement = len(set(states)) == 1

    return {
        "highs": highs_result,
        "sa": sa_result,
        "best": {"method": best_method, "energy": results[best_method]["energy"]},
        "agreement": agreement,
        "all_states": {m: r["optimal_state"] for m, r in results.items()},
    }


if __name__ == "__main__":
    import sys
    sys.path.insert(0, "/mnt/agents/output/rebuild/stage4-optimize")
    from finsler_metric import make_uniform_hachimoji_states
    from qubo_builder import finsler_to_qubo

    states = make_uniform_hachimoji_states()
    qubo = finsler_to_qubo(states)

    print("Testing classical solvers on 8-state Hachimoji QUBO...")
    comparison = compare_solvers(qubo, time_limit=1.0)

    print(f"\nHiGHS: state={comparison['highs']['optimal_state']}, "
          f"energy={comparison['highs']['energy']:.6f}, "
          f"status={comparison['highs']['status']}")
    print(f"SA:    state={comparison['sa']['optimal_state']}, "
          f"energy={comparison['sa']['energy']:.6f}, "
          f"iterations={comparison['sa']['iterations']}")
    print(f"\nAgreement: {comparison['agreement']}")
    print(f"Best: {comparison['best']['method']} with energy {comparison['best']['energy']:.6f}")
