#!/usr/bin/env bash
set -euo pipefail

# Contest-style Vidal/MPI MPS cases.
#
# Usage:
#   tools/run_vidal_mpi_contest_cases.sh main1
#   tools/run_vidal_mpi_contest_cases.sh main2
#   tools/run_vidal_mpi_contest_cases.sh strong
#   tools/run_vidal_mpi_contest_cases.sh all
#
# Common overrides:
#   PYTHON_BIN=.venv/bin/python
#   MPIEXEC=mpiexec
#   MPIEXEC_FULL="mpirun -np 4 -hostfile /home/yx/qibotn/hostfile -perhost 2"
#   HOSTFILE=hostfile                 # optional; used only if the file exists
#   RANKS=8
#   TORCH_THREADS=8
#   CUT_RATIO=1e-12
#   OBS_FILTER="boundary_ZZ_q2 ring_xz dense3_spread complex_iZ0"
#
# Per-case overrides:
#   MAIN1_NQ=128 MAIN1_LAYERS=50 MAIN1_BOND=1024 MAIN1_SEED=31001
#   MAIN2_NQ=128 MAIN2_LAYERS=64 MAIN2_BOND=2048 MAIN2_SEED=31002
#   STRONG_NQ=256 STRONG_LAYERS=64 STRONG_BOND=2048 STRONG_SEED=41001

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

PYTHON_BIN="${PYTHON_BIN:-.venv/bin/python}"
MPIEXEC="${MPIEXEC:-mpiexec}"
HOSTFILE="${HOSTFILE:-}"
RANKS="${RANKS:-4}"
TORCH_THREADS="${TORCH_THREADS:-1}"
CUT_RATIO="${CUT_RATIO:-1e-12}"
OBS_FILTER="${OBS_FILTER:-}"

RUNNER_DIR="$ROOT_DIR/.tmp"
mkdir -p "$RUNNER_DIR"
RUNNER="$(mktemp "$RUNNER_DIR/qibotn_vidal_contest.XXXXXX.py")"
cleanup() {
  rm -f "$RUNNER"
}
trap cleanup EXIT

cat > "$RUNNER" <<'PY'
from __future__ import annotations

import argparse
import math
import time

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

from qibotn.backends.vidal import VidalBackend


def set_torch_threads(nthreads):
    try:
        import torch

        torch.set_num_threads(nthreads)
    except Exception:
        pass


def build_circuit(kind, nqubits, nlayers, seed):
    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 in ("rxx_rzz", "scramble"):
                circuit.add(gates.RX(q, theta=rng.uniform(-math.pi, math.pi)))

        if kind == "reversed_cnot":
            for q in range(0, nqubits - 1, 2):
                circuit.add(gates.CNOT(q + 1, q) if layer % 2 else gates.CNOT(q, q + 1))
            for q in range(1, nqubits - 1, 2):
                circuit.add(gates.CNOT(q + 1, q) if layer % 2 == 0 else gates.CNOT(q, q + 1))
        elif kind == "rxx_rzz":
            for q in range(layer % 2, nqubits - 1, 2):
                circuit.add(gates.RXX(q, q + 1, theta=rng.uniform(-0.9, 0.9)))
                circuit.add(gates.RZZ(q, q + 1, theta=rng.uniform(-0.9, 0.9)))
        elif kind == "scramble":
            for q in range(layer % 2, nqubits - 1, 2):
                circuit.add(gates.RXX(q, q + 1, theta=rng.uniform(-0.8, 0.8)))
                circuit.add(gates.RZZ(q, q + 1, theta=rng.uniform(-0.8, 0.8)))
                if layer % 5 == 4:
                    circuit.add(gates.SWAP(q, q + 1))
        else:
            raise ValueError(f"Unknown circuit kind {kind!r}.")

    return circuit


def ring_xz(nqubits):
    form = 0
    for q in range(nqubits):
        form += 0.5 * X(q) * Z((q + 1) % nqubits)
    return hamiltonians.SymbolicHamiltonian(form=form)


def open_zz(nqubits):
    form = 0
    for q in range(nqubits - 1):
        form += (1.0 / (nqubits - 1)) * Z(q) * Z(q + 1)
    return hamiltonians.SymbolicHamiltonian(form=form)


def range2_xx(nqubits):
    form = 0
    for q in range(nqubits - 2):
        form += (1.0 / (nqubits - 2)) * X(q) * X(q + 2)
    return hamiltonians.SymbolicHamiltonian(form=form)


def dense_observable(nqubits, qubits, seed, dim):
    rng = np.random.default_rng(seed)
    raw = rng.normal(size=(dim, dim)) + 1j * rng.normal(size=(dim, dim))
    matrix = (raw + raw.conj().T) / 2.0
    matrix = matrix / np.linalg.norm(matrix)
    return {"matrix": matrix, "qubits": list(qubits)}


def observables_for_case(nqubits, seed):
    q1 = nqubits // 4
    q2 = nqubits // 2
    q3 = (3 * nqubits) // 4
    last = nqubits - 1

    return [
        ("boundary_ZZ_q1", hamiltonians.SymbolicHamiltonian(form=Z(q1 - 1) * Z(q1))),
        ("boundary_ZZ_q2", hamiltonians.SymbolicHamiltonian(form=Z(q2 - 1) * Z(q2))),
        ("boundary_ZZ_q3", hamiltonians.SymbolicHamiltonian(form=Z(q3 - 1) * Z(q3))),
        (
            "long_Z_5_sites",
            hamiltonians.SymbolicHamiltonian(form=Z(0) * Z(q1) * Z(q2) * Z(q3) * Z(last)),
        ),
        (
            "mixed_XZYZX",
            hamiltonians.SymbolicHamiltonian(form=X(0) * Z(q1) * Y(q2) * Z(q3) * X(last)),
        ),
        ("ring_xz", ring_xz(nqubits)),
        ("open_zz", open_zz(nqubits)),
        ("range2_xx", range2_xx(nqubits)),
        ("complex_iZ0", hamiltonians.SymbolicHamiltonian(form=1.0j * Z(0))),
        ("dense2_mid", dense_observable(nqubits, (q2 - 1, q2), seed + 101, 4)),
        ("dense3_spread", dense_observable(nqubits, (q1, q2, q3), seed + 202, 8)),
    ]


def run_case(args):
    set_torch_threads(args.torch_threads)
    comm = MPI.COMM_WORLD
    rank = comm.Get_rank()
    size = comm.Get_size()

    circuit = build_circuit(args.kind, args.nqubits, args.nlayers, args.seed)
    observables = observables_for_case(args.nqubits, args.seed)
    if args.obs_filter:
        wanted = set(args.obs_filter.split(","))
        observables = [(name, obs) for name, obs in observables if name in wanted]
        if not observables:
            raise ValueError(f"OBS_FILTER matched no observables: {args.obs_filter!r}")

    if rank == 0:
        print("=" * 88, flush=True)
        print(
            "case "
            f"label={args.label} kind={args.kind} ranks={size} "
            f"nqubits={args.nqubits} nlayers={args.nlayers} gates={len(circuit.queue)} "
            f"bond={args.bond} cut_ratio={args.cut_ratio:g} "
            f"torch_threads={args.torch_threads} seed={args.seed} "
            f"obs_filter={args.obs_filter or 'all'}",
            flush=True,
        )
        print(
            "observable value seconds trunc_sum trunc_max status",
            flush=True,
        )

    for obs_name, observable in observables:
        backend = VidalBackend()
        backend.configure_tn_simulation(
            max_bond_dimension=args.bond,
            cut_ratio=args.cut_ratio,
            tensor_module="torch",
            mpi_approach="CT",
            mpi_num_procs=size,
            fallback=False,
        )

        comm.Barrier()
        start = time.perf_counter()
        try:
            value = backend.expectation(
                circuit,
                observable,
                preprocess=True,
                compile_circuit=False,
            )
            status = "ok"
        except Exception as exc:  # pragma: no cover - printed for manual runs
            value = np.nan
            status = type(exc).__name__ + ":" + str(exc).split("\n", 1)[0]
        seconds = time.perf_counter() - start

        if rank == 0:
            print(
                f"{obs_name} {value!r} {seconds:.3f} "
                f"{backend.last_truncation_error:.6e} "
                f"{backend.last_max_truncation_error:.6e} {status}",
                flush=True,
            )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--label", required=True)
    parser.add_argument("--kind", choices=("reversed_cnot", "rxx_rzz", "scramble"), required=True)
    parser.add_argument("--nqubits", type=int, required=True)
    parser.add_argument("--nlayers", type=int, required=True)
    parser.add_argument("--bond", type=int, required=True)
    parser.add_argument("--cut-ratio", type=float, required=True)
    parser.add_argument("--seed", type=int, required=True)
    parser.add_argument("--torch-threads", type=int, required=True)
    parser.add_argument("--obs-filter", default="")
    run_case(parser.parse_args())


if __name__ == "__main__":
    main()
PY

if [[ -n "${MPIEXEC_FULL:-}" ]]; then
  read -r -a mpi_prefix <<< "$MPIEXEC_FULL"
else
  mpi_prefix=("$MPIEXEC")
  if [[ -n "$HOSTFILE" && -f "$HOSTFILE" ]]; then
    mpi_prefix+=("-hostfile" "$HOSTFILE")
  fi
  mpi_prefix+=("-n" "$RANKS")
fi

run_case() {
  local label="$1"
  local kind="$2"
  local nq="$3"
  local layers="$4"
  local bond="$5"
  local seed="$6"

  echo
  echo "Running $label: kind=$kind nqubits=$nq layers=$layers bond=$bond seed=$seed"
  echo "MPI: ${mpi_prefix[*]}"
  "${mpi_prefix[@]}" "$PYTHON_BIN" -u "$ROOT_DIR/tools/vidal_mpi_contest_runner.py" \
    --label "$label" \
    --kind "$kind" \
    --nqubits "$nq" \
    --nlayers "$layers" \
    --bond "$bond" \
    --cut-ratio "$CUT_RATIO" \
    --seed "$seed" \
    --torch-threads "$TORCH_THREADS" \
    --obs-filter "$(tr ' ' ',' <<< "$OBS_FILTER")"
}

case "${1:-help}" in
  main1)
    run_case \
      "main1-reversed-cnot" \
      "reversed_cnot" \
      "${MAIN1_NQ:-128}" \
      "${MAIN1_LAYERS:-50}" \
      "${MAIN1_BOND:-1024}" \
      "${MAIN1_SEED:-31001}"
    ;;
  main2)
    run_case \
      "main2-rxx-rzz" \
      "rxx_rzz" \
      "${MAIN2_NQ:-128}" \
      "${MAIN2_LAYERS:-64}" \
      "${MAIN2_BOND:-2048}" \
      "${MAIN2_SEED:-31002}"
    ;;
  strong)
    run_case \
      "strong-scramble" \
      "scramble" \
      "${STRONG_NQ:-256}" \
      "${STRONG_LAYERS:-64}" \
      "${STRONG_BOND:-2048}" \
      "${STRONG_SEED:-41001}"
    ;;
  all)
    "$0" main1
    "$0" main2
    "$0" strong
    ;;
  smoke)
    MAIN1_NQ="${MAIN1_NQ:-32}" \
    MAIN1_LAYERS="${MAIN1_LAYERS:-6}" \
    MAIN1_BOND="${MAIN1_BOND:-128}" \
    "$0" main1
    ;;
  help|*)
    cat >&2 <<'EOF'
Usage: tools/run_vidal_mpi_contest_cases.sh [main1|main2|strong|all|smoke]

Cases:
  main1   128 qubits, 50 layers, reversed-CNOT brickwall, chi=1024
  main2   128 qubits, 64 layers, RXX/RZZ brickwall, chi=2048
  strong  256 qubits, 64 layers, RXX/RZZ + periodic SWAP scramble, chi=2048
  smoke   Small syntax/runtime check of main1

Common overrides:
  PYTHON_BIN=.venv/bin/python
  MPIEXEC=mpiexec
  MPIEXEC_FULL="mpirun -np 4 -hostfile /home/yx/qibotn/hostfile -perhost 2"
  HOSTFILE=hostfile
  RANKS=8
  TORCH_THREADS=8
  CUT_RATIO=1e-12
  OBS_FILTER="boundary_ZZ_q2 ring_xz dense3_spread complex_iZ0"

Per-case overrides:
  MAIN1_NQ=128 MAIN1_LAYERS=50 MAIN1_BOND=1024 MAIN1_SEED=31001
  MAIN2_NQ=128 MAIN2_LAYERS=64 MAIN2_BOND=2048 MAIN2_SEED=31002
  STRONG_NQ=256 STRONG_LAYERS=64 STRONG_BOND=2048 STRONG_SEED=41001
EOF
    exit 2
    ;;
esac
