"""Compare QMatchaTeaBackend with the VidalBackend fast path."""

from __future__ import annotations

import argparse
import json
import math
import time

import numpy as np
import torch
from qibo import Circuit, gates, hamiltonians
from qibo.symbols import X, Y, Z

from qibotn.backends.qmatchatea import QMatchaTeaBackend
from qibotn.backends.vidal import VidalBackend


def build_circuit(nqubits, nlayers, seed, kind):
    rng = np.random.default_rng(seed)
    circuit = Circuit(nqubits)
    for layer in range(nlayers):
        for q in range(nqubits):
            circuit.add(gates.RY(q, theta=rng.uniform(-math.pi, math.pi)))
            circuit.add(gates.RZ(q, theta=rng.uniform(-math.pi, math.pi)))
        if kind == "brickwall":
            for q in range(0, nqubits - 1, 2):
                circuit.add(gates.CNOT(q, q + 1))
            for q in range(1, nqubits - 1, 2):
                circuit.add(gates.CNOT(q, q + 1))
        elif kind == "shifted-cz":
            for q in range(layer % 2, nqubits - 1, 2):
                circuit.add(gates.CZ(q, q + 1))
        elif kind == "reversed-cnot":
            for q in range(0, nqubits - 1, 2):
                circuit.add(gates.CNOT(q + 1, q))
            for q in range(1, nqubits - 1, 2):
                circuit.add(gates.CNOT(q, q + 1))
        else:
            raise ValueError(f"Unknown circuit kind {kind!r}.")
    return circuit


def build_observable(nqubits, kind):
    form = 0
    if kind == "ring-xz":
        for q in range(nqubits):
            form += 0.5 * X(q) * Z((q + 1) % nqubits)
    elif kind == "open-zz":
        for q in range(nqubits - 1):
            form += Z(q) * Z(q + 1) / (nqubits - 1)
    elif kind == "mixed":
        form += 0.25 * X(0) - 0.5 * Z(nqubits - 1)
        for q in range(0, nqubits - 1, 3):
            form += 0.125 * Y(q) * Y(q + 1)
    else:
        raise ValueError(f"Unknown observable kind {kind!r}.")
    return hamiltonians.SymbolicHamiltonian(form=form)


def run_backend(backend, circuit, observable):
    start = time.perf_counter()
    value = backend.expectation(circuit, observable, preprocess=False, compile_circuit=True)
    return float(np.real(value)), time.perf_counter() - start


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--nqubits", type=int, default=34)
    parser.add_argument("--nlayers", type=int, default=20)
    parser.add_argument("--bond", "--bonds", dest="bond", type=int, default=512)
    parser.add_argument("--seed", type=int, default=42)
    parser.add_argument("--tensor-module", choices=("torch", "numpy"), default="torch")
    parser.add_argument("--torch-threads", type=int, default=32)
    parser.add_argument(
        "--circuit-kind",
        choices=("brickwall", "shifted-cz", "reversed-cnot"),
        default="brickwall",
    )
    parser.add_argument(
        "--observable-kind",
        choices=("ring-xz", "open-zz", "mixed"),
        default="ring-xz",
    )
    parser.add_argument("--reference-file")
    parser.add_argument("--skip-qmatchatea", action="store_true")
    args = parser.parse_args()

    torch.set_num_threads(args.torch_threads)
    circuit = build_circuit(args.nqubits, args.nlayers, args.seed, args.circuit_kind)
    observable = build_observable(args.nqubits, args.observable_kind)

    exact = None
    if args.reference_file:
        with open(args.reference_file, "r", encoding="utf-8") as f:
            exact = float(json.load(f)["expectation"])

    print(
        f"nqubits={args.nqubits} nlayers={args.nlayers} bond={args.bond} "
        f"circuit={args.circuit_kind} observable={args.observable_kind} "
        f"tensor_module={args.tensor_module} torch_threads={args.torch_threads}"
    )
    if exact is not None:
        print(f"exact={exact:.16e}")
    print("backend value abs_error seconds")

    if not args.skip_qmatchatea:
        qmt = QMatchaTeaBackend()
        qmt.configure_tn_simulation(
            ansatz="MPS",
            max_bond_dimension=args.bond,
            cut_ratio=1e-12,
            svd_control="E!",
            tensor_module=args.tensor_module,
            compile_circuit=True,
            track_memory=False,
        )
        value, seconds = run_backend(qmt, circuit, observable)
        error = float("nan") if exact is None else abs(value - exact)
        print(f"qmatchatea {value:.16e} {error:.6e} {seconds:.3f}")

    vidal = VidalBackend()
    vidal.configure_tn_simulation(
        ansatz="MPS",
        max_bond_dimension=args.bond,
        cut_ratio=1e-12,
        tensor_module=args.tensor_module,
        compile_circuit=True,
        fallback=True,
    )
    value, seconds = run_backend(vidal, circuit, observable)
    error = float("nan") if exact is None else abs(value - exact)
    print(f"vidal {value:.16e} {error:.6e} {seconds:.3f}")


if __name__ == "__main__":
    main()
