import time
import numpy as np
import scipy.sparse as sp
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt

from qutip import basis, sigmaz, expect
from qutip.solver.heom import HEOMSolver, DrudeLorentzBath
# system
epsilon = 1.0
H = 0.5 * epsilon * sigmaz()
rho0 = basis(2, 0) * basis(2, 0).dag()

# bath
lam = 0.1
gamma = 0.5
T = 1.0
Nk = 8
Q = sigmaz()

tlist = np.linspace(0, 20, 500)
depth = 2
n = 2
# reference run using the standard qutip solver
bath = DrudeLorentzBath(Q=Q, lam=lam, gamma=gamma, T=T, Nk=Nk)
solver = HEOMSolver(H, bath, max_depth=depth, options={"progress_bar": False})

n_ados = solver._n_ados
n_total = n**2 * n_ados

print(f"ADOs: {n_ados}")
print(f"state vector length: {n_total}")

t0 = time.perf_counter()
result_qutip = solver.run(rho0, tlist)
t_qutip = time.perf_counter() - t0

print(f"qutip runtime: {t_qutip:.3f} s")

sz_qutip = expect(sigmaz(), result_qutip.states)
# extract the RHS matrix
# solver.rhs(0) returns the full HEOM RHS as a qutip Qobj
# .full() converts it to a numpy array
# for a time-independent system this matrix does not change, so rhs(0) is enough
RHS = sp.csr_matrix(solver.rhs(0).full())

print(f"RHS shape: {RHS.shape}")
print(f"non-zeros: {RHS.nnz} out of {n_total**2} ({100*RHS.nnz/n_total**2:.1f}%)")
# build the extended initial state vector
# HEOM state = [vec(rho_s), 0, 0, ...] where all ADOs start at zero
# vec uses column-major order (qutip's convention)
rho0_he = np.zeros(n_total, dtype=complex)
rho0_he[:n**2] = rho0.full().ravel("F")

print(f"first 4 entries: {rho0_he[:4].real}  (should be [1, 0, 0, 0])")
# solve the same ODE using an external integrator
# this is the part that will be replaced by petsc later
# the ODE is: d/dt rho_he = RHS @ rho_he

def rhs_fn(t, y):
    return RHS @ y

t0 = time.perf_counter()
sol = solve_ivp(
    rhs_fn,
    t_span=(tlist[0], tlist[-1]),
    y0=rho0_he,
    t_eval=tlist,
    method="RK45",
    rtol=1e-8,
    atol=1e-10,
)
t_bridge = time.perf_counter() - t0

print(f"bridge runtime: {t_bridge:.3f} s")
print(f"success: {sol.success}")
# extract rho_s(t) from the bridge solution
# first n^2 entries of each column are vec(rho_s), reshape back to n x n

def extract_rho_s(y):
    return y[:n**2].reshape((n, n), order="F")

sz_bridge = np.array([
    extract_rho_s(sol.y[:, i])[0, 0].real - extract_rho_s(sol.y[:, i])[1, 1].real
    for i in range(len(tlist))
])
# validation
max_err = np.max(np.abs(sz_qutip - sz_bridge))
print(f"max error vs qutip: {max_err:.2e}")

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

axes[0].plot(tlist, sz_qutip, lw=2, label="QuTiP")
axes[0].plot(tlist, sz_bridge, lw=1.5, ls="--", label="bridge (scipy RK45)")
axes[0].set_xlabel("time")
axes[0].set_ylabel(r"$\langle \sigma_z \rangle$")
axes[0].legend()
axes[0].grid(alpha=0.3)

axes[1].semilogy(tlist, np.abs(sz_qutip - sz_bridge) + 1e-16)
axes[1].set_xlabel("time")
axes[1].set_ylabel("pointwise error")
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.savefig("bridge_validation.png", dpi=150, bbox_inches="tight")
plt.show()
# petsc bridge
# swap USE_PETSC to True after installing petsc4py
# install: conda install -c conda-forge petsc petsc4py mpi4py

USE_PETSC = False

if USE_PETSC:
    import petsc4py
    petsc4py.init()
    from petsc4py import PETSc

    # build petsc matrix from CSR data
    A = PETSc.Mat().createAIJ(
        size=(n_total, n_total),
        csr=(RHS.indptr, RHS.indices, RHS.data),
        comm=PETSc.COMM_WORLD,
    )
    A.assemblyBegin()
    A.assemblyEnd()

    x = A.createVecRight()
    f = A.createVecLeft()
    x.setValues(range(n_total), rho0_he)
    x.assemblyBegin()
    x.assemblyEnd()

    def rhs_petsc(ts, t, x, f):
        A.mult(x, f)

    ts = PETSc.TS().create(comm=PETSc.COMM_WORLD)
    ts.setType(PETSc.TS.Type.RK)
    ts.setRHSFunction(rhs_petsc, f)
    ts.setTime(tlist[0])
    ts.setMaxTime(tlist[-1])
    ts.setTimeStep(tlist[1] - tlist[0])
    ts.setTolerances(rtol=1e-8, atol=1e-10)
    ts.setFromOptions()

    t0 = time.perf_counter()
    ts.solve(x)
    t_petsc = time.perf_counter() - t0

    y_final = np.array(x.getArray())
    rho_final = extract_rho_s(y_final)
    sz_final = rho_final[0, 0].real - rho_final[1, 1].real

    print(f"petsc runtime: {t_petsc:.3f} s")
    print(f"<sigma_z> at t_final: {sz_final:.6f}")
    print(f"qutip reference:      {sz_qutip[-1]:.6f}")
else:
    print("petsc bridge skipped. set USE_PETSC = True to run.")