"""Compute and cache a qibojit state-vector reference for the ring-XZ observable."""

import argparse
import json
import math
import time
from pathlib import Path

import numpy as np
import qibo
from qibo import Circuit, gates


def build_circuit(nqubits, nlayers, seed):
    rng = np.random.default_rng(seed)
    circuit = Circuit(nqubits)
    for _ in range(nlayers):
        for qubit in range(nqubits):
            circuit.add(gates.RY(qubit, theta=rng.uniform(-math.pi, math.pi)))
            circuit.add(gates.RZ(qubit, theta=rng.uniform(-math.pi, math.pi)))
        for qubit in range(0, nqubits - 1, 2):
            circuit.add(gates.CNOT(qubit, qubit + 1))
        for qubit in range(1, nqubits - 1, 2):
            circuit.add(gates.CNOT(qubit, qubit + 1))
    return circuit


def ring_xz_expectation(state, nqubits, chunk_size):
    value = 0.0
    for qubit in range(nqubits):
        next_qubit = (qubit + 1) % nqubits
        x_flip = 1 << (nqubits - 1 - qubit)
        z_shift = nqubits - 1 - next_qubit
        term = 0.0
        for start in range(0, state.size, chunk_size):
            stop = min(start + chunk_size, state.size)
            indices = np.arange(start, stop, dtype=np.int64)
            z_bit = (indices >> z_shift) & 1
            z_phase = 1 - 2 * z_bit
            term += np.vdot(state[indices ^ x_flip], z_phase * state[start:stop]).real
        value += 0.5 * term
    return float(value)


def default_output_path(nqubits, nlayers, seed):
    return Path("references") / (
        f"qibojit_ring_xz_n{nqubits}_l{nlayers}_seed{seed}.json"
    )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--nqubits", type=int, default=32)
    parser.add_argument("--nlayers", type=int, default=3)
    parser.add_argument("--seed", type=int, default=42)
    parser.add_argument("--output")
    parser.add_argument("--force", action="store_true")
    parser.add_argument("--allow-large", action="store_true")
    parser.add_argument("--max-state-gb", type=float, default=32.0)
    parser.add_argument("--chunk-size", type=int, default=1 << 20)
    args = parser.parse_args()

    output = Path(args.output) if args.output else default_output_path(
        args.nqubits, args.nlayers, args.seed
    )
    if output.exists() and not args.force:
        with open(output, "r", encoding="utf-8") as f:
            data = json.load(f)
        print(f"loaded {output}")
        print(f"expectation={float(data['expectation']):.16e}")
        return

    state_gb = (2**args.nqubits) * np.dtype(np.complex128).itemsize / (1024**3)
    if state_gb > args.max_state_gb and not args.allow_large:
        raise MemoryError(
            f"Estimated state vector alone is {state_gb:.1f} GiB. "
            "Pass --allow-large after confirming the node has enough memory."
        )

    qibo.set_backend("qibojit")
    circuit = build_circuit(args.nqubits, args.nlayers, args.seed)

    start = time.perf_counter()
    state = circuit().state(numpy=True).reshape(-1)
    expectation = ring_xz_expectation(state, args.nqubits, args.chunk_size)
    elapsed = time.perf_counter() - start

    data = {
        "backend": "qibojit",
        "observable": "0.5 * sum_i X_i Z_((i+1) mod n)",
        "nqubits": args.nqubits,
        "nlayers": args.nlayers,
        "seed": args.seed,
        "expectation": expectation,
        "seconds": elapsed,
        "state_vector_gib_estimate": state_gb,
    }
    output.parent.mkdir(parents=True, exist_ok=True)
    with open(output, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, sort_keys=True)
        f.write("\n")

    print(f"saved {output}")
    print(f"expectation={expectation:.16e}")
    print(f"seconds={elapsed:.3f}")


if __name__ == "__main__":
    main()
