import networkx as nx
import numpy as np
from qiskit.quantum_info import SparsePauliOp
from qiskit_algorithms.minimum_eigensolvers import NumPyMinimumEigensolver


def get_Heisenberg_PauliSumOp(nqubits, Jz, Jxy):
    """Create Heisenberg Hamiltonian using SparsePauliOp (Qiskit 1.0+)."""
    Ham = None
    for nn in range(nqubits - 1):
        # Build Pauli strings for Z, X, Y operators on qubits nn and nn+1
        z_labels = ["I"] * nqubits
        z_labels[nn] = "Z"
        z_labels[nn + 1] = "Z"
        z_string = "".join(z_labels)

        x_labels = ["I"] * nqubits
        x_labels[nn] = "X"
        x_labels[nn + 1] = "X"
        x_string = "".join(x_labels)

        y_labels = ["I"] * nqubits
        y_labels[nn] = "Y"
        y_labels[nn + 1] = "Y"
        y_string = "".join(y_labels)

        # Create SparsePauliOp operators
        temp_z = SparsePauliOp(z_string, coeffs=[1.0])
        temp_x = SparsePauliOp(x_string, coeffs=[1.0])
        temp_y = SparsePauliOp(y_string, coeffs=[1.0])

        # Combine terms
        term = Jz * temp_z + Jxy * (temp_x + temp_y)

        if Ham is None:
            Ham = term
        else:
            Ham = Ham + term

    return Ham


def get_Ham_from_graph(G):
    Ham = []
    for nn in list(G.nodes()):
        try:
            G.nodes[nn]["weight"]
        except NameError:
            # if not specified, node weights will be considered zero
            G.nodes[nn]["weight"] = 0

        if G.nodes[nn]["weight"] != 0:
            if nn == 0:
                temp = "Z"
            else:
                temp = "I"

            for pp in range(0, nn - 1):
                temp = temp + "I"

            if nn != 0:
                temp = temp + "Z"

            for pp in range(nn, len(G.nodes()) - 1):
                temp = temp + "I"

            Ham.append((G.nodes[nn]["weight"], temp, nn))

    for pair in list(G.edges()):
        try:
            G.edges[pair]["weight"]
        except NameError:
            # if not specified, edge weight will be set equal to 1.0
            G.edges[pair]["weight"] = 1.0

        if pair[0] == 0:
            temp = "Z"
        else:
            temp = "I"

        for pp in range(0, pair[0] - 1):
            temp = temp + "I"

        if pair[0] != 0:
            temp = temp + "Z"

        for pp in range(pair[0], pair[1] - 1):
            temp = temp + "I"

        temp = temp + "Z"

        for pp in range(pair[1], len(G.nodes()) - 1):
            temp = temp + "I"

        Ham.append((G.edges[pair]["weight"], temp, pair))

    return Ham


def get_Ham_from_PauliSumOp(H_pauliSum):
    """Extract Hamiltonian from SparsePauliOp (Qiskit 1.0+)."""
    Ham = []
    # SparsePauliOp has paulis.to_labels() and coeffs attributes
    labels = H_pauliSum.paulis.to_labels()
    coeffs = H_pauliSum.coeffs

    for nn in range(len(labels)):
        op_str = labels[nn]

        pair = []
        for ll in range(len(op_str)):
            if op_str[ll] == "Z":
                pair.append(ll)

        if len(pair) > 1:
            pair = tuple(pair)
        elif len(pair) == 1:
            pair = pair[0]
        else:
            pair = None

        Ham.append((np.real(coeffs[nn]), op_str, pair))

    return Ham


def get_Ham_from_QUBO(QUBO_mat):
    nodes = np.size(QUBO_mat[0])

    Ham = []
    for nn in range(nodes):
        if QUBO_mat[nn][nn] != 0:
            if nn == 0:
                temp = "Z"
            else:
                temp = "I"

            for pp in range(0, nn - 1):
                temp = temp + "I"

            if nn != 0:
                temp = temp + "Z"

            for pp in range(nn, len(QUBO_mat) - 1):
                temp = temp + "I"

            Ham.append((QUBO_mat[nn][nn], temp, nn))

    for p1 in range(nodes):
        for p2 in range(p1 + 1, nodes):
            if np.abs(QUBO_mat[p1][p2]) > 1e-5:
                if p1 == 0:
                    temp = "Z"
                else:
                    temp = "I"

                for pp in range(0, p1 - 1):
                    temp = temp + "I"

                if p1 != 0:
                    temp = temp + "Z"

                for pp in range(p1, p2 - 1):
                    temp = temp + "I"

                temp = temp + "Z"

                for pp in range(p2, nodes - 1):
                    temp = temp + "I"

                pair = tuple([p1, p2])
                Ham.append((QUBO_mat[p1][p2], temp, pair))

    return Ham


def convert_QUBO_to_Ising(QUBO_mat):
    nodes = np.size(QUBO_mat[0])
    Ising_mat = QUBO_mat / 4

    for nn in range(nodes):
        Ising_mat[nn][nn] = QUBO_mat[nn][nn] / 2 + sum(
            QUBO_mat[nn][nn + 1 :] / 4  # noqa: E203
        )

    for nn in range(nodes):
        Ising_mat[nn][nn] += sum(QUBO_mat[:nn, nn] / 4)

    return Ising_mat


def get_graph_from_Ham(H):
    G = nx.Graph()
    for nn in range(len(H)):
        Num_z = H[nn][1].count("Z")
        if Num_z > 2:
            print(
                """Error: cannot create networkX graph.
                Hamiltonian has more than pairwise connections"""
            )
        elif Num_z == 1:
            ind = H[nn][1].find("Z")
            G.add_nodes_from([(ind, {"weight": H[nn][0]})])
        else:
            G.add_edges_from([(H[nn][2][0], H[nn][2][1], {"weight": H[nn][0]})])

    return G


def get_PauliSumOp_from_Ham(H):
    """Create SparsePauliOp from Hamiltonian list (Qiskit 1.0+)."""
    # Build list of Pauli strings and coefficients
    labels = []
    coeffs = []

    for nn in range(len(H)):
        pauli_string = H[nn][1]
        coeff = H[nn][0]
        labels.append(pauli_string)
        coeffs.append(coeff)

    # Create SparsePauliOp from labels and coefficients
    if labels:
        Ham = SparsePauliOp(labels, coeffs=coeffs)
    else:
        # Return empty operator if no terms
        Ham = SparsePauliOp(["I"], coeffs=[0.0])

    return Ham


def get_exact_en(H, nodes):
    if nodes > 24:
        Egs_exact = "!!problem too big for exact solution!!"
    else:
        Egs_exact = NumPyMinimumEigensolver().compute_minimum_eigenvalue(H).eigenvalue

    return np.real(Egs_exact)
