"""Plot esterification LLE predictions from saved parameter sets with
selected kij adjustments."""

# pylint: disable=dangerous-default-value
from __future__ import annotations

import re
from pathlib import Path
import numpy as np
import pandas as pd


from equi_gel.cost_func_binodal import get_minimal_distance
from equi_gel.newton_raphson_tracing import (
    add_component_to_y_vec,
    build_initial_guess_binary,
    interpret_y_vector,
    trace_parameter,
)
from equi_gel.parameters import (
    add_mixing_association,
    get_parameter_set_from_tables,
    get_subset_of_parameters,
)
from equi_gel.plot_binary import (
    binary_axes,
    binary_coords,
    plot_binary,
    scatter_binary,
)
from equi_gel.plot_ternary import (
    close_envelope,
    plot_ternary,
    scatter_ternary,
    ternary_axes,
    ternary_coords,
)
from equi_gel.save_load_json import (
    find_lle_paths_by_components,
    load_lle_record,
)

EXP_MARKERS = ["o"]
EXP_MARKER_SIZE_BINARY = 12
EXP_MARKER_SIZE_TERNARY = 8
EXP_MARKER_LINEWIDTH = 0.5
TERNARY_LABELS = {
    "water": "W",
    "acetic_acid": "AA",
    "n_butanol": "B",
    "butyl_acetate": "BA",
}

COMPONENTS_ALL = ["water", "n_butanol", "butyl_acetate", "acetic_acid"]

PRESSURE = 1.13
T_INIT_BINARY_LLE = 270.0
T_TERNARY_LLE = 298.15
T_FINAL = 400.0
TARGET_TERNARY_TEMP = 298.15
TERNARY_TEMP_TOL = 0.5

SYSTEMS = [
    # {
    #     "name": "water_n_butanol",
    #     "components": ["water", "n_butanol"],
    #     "subset_idx": [1, 2],
    #     "kind": "binary",
    #     "reverse_order": True,
    #     "step_size_init": 0.01,
    #     "critical_point_tol": 5e-2,
    #     "max_step_fraction": 1,
    #     "which_idx_binary": 1,
    #     "which_source": "Stephenson",
    # },
    # {
    #     "name": "water_butyl_acetate",
    #     "components": ["water", "butyl_acetate"],
    #     "subset_idx": [1, 3],
    #     "kind": "binary",
    #     "reverse_order": False,
    #     "step_size_init": 0.01,
    #     "critical_point_tol": 5e-2,
    #     "max_step_fraction": 1,
    #     "stuck_progress_tol": 0.0005,
    #     "which_idx_binary": 1,
    #     "which_source": "Stephenson",
    # },
    {
        "name": "water_n_butanol_acetic_acid",
        "components": ["water", "n_butanol", "acetic_acid"],
        "subset_idx": [1, 2, 4],
        "binary_subset_idx": [1, 2],
        "kind": "ternary",
        "reverse_order": True,
        "trace_index": 2,
        "step_size_init": 0.01,
        "critical_point_tol": 5e-2,
        "which_source": "Ruiz",
    },
    # {
    #     "name": "water_n_butanol_butyl_acetate",
    #     "components": ["water", "n_butanol", "butyl_acetate"],
    #     "subset_idx": [1, 2, 3],
    #     "binary_subset_idx": [1, 2],
    #     "kind": "ternary",
    #     "reverse_order": True,
    #     "trace_index": 5,
    #     "step_size_init": 0.01,
    #     "critical_point_tol": 5e-2,
    #     "which_source": "Ruiz",
    # },
    {
        "name": "water_butyl_acetate_acetic_acid",
        "components": ["water", "butyl_acetate", "acetic_acid"],
        "subset_idx": [1, 3, 4],
        "binary_subset_idx": [1, 3],
        "kind": "ternary",
        "reverse_order": False,
        "trace_index": 5,
        "step_size_init": 0.0001,
        "critical_point_tol": 5e-2,
        "stuck_progress_tol": 0.0005,
        "which_source": "Ruiz",
    },
]


def first_token(value):
    """Return the leading token of a source string for compact legends."""
    return re.split(r"[ ,.-]", value)[0]


def grey_palette(n, *, start=0.0, end=0.7):
    """Return n grey shades as matplotlib-compatible color strings."""
    if n <= 1:
        return [f"{start:.2f}"]
    shades = np.linspace(start, end, n)
    return [f"{shade:.2f}" for shade in shades]


def load_exp_records(components):
    """Load experimental records and build permutation list."""
    exp_paths = find_lle_paths_by_components(components, is_exp=True)
    dict_exp_lst = [load_lle_record(path) for path in exp_paths]
    _permutation_list = [[list(range(len(components)))], []]

    for record in dict_exp_lst:
        record_components = [
            record.get(f"comp{i + 1}_name") for i in range(len(components))
        ]
        _permutation_list[1].append(
            [record_components.index(comp) for comp in components]
        )

    return dict_exp_lst, _permutation_list


def _extract_temp(record):
    temp = record.get("temperature_K")
    if isinstance(temp, list):
        return temp[0]
    return temp


def filter_exp_by_temperature(_exp_dicts, _permutation_list, target, tol):
    """Filter experimental data to a target temperature within tolerance."""
    filtered_exp = []
    filtered_perm = []
    for idx, record in enumerate(_exp_dicts):
        temp = _extract_temp(record)
        if temp is None:
            continue
        if abs(temp - target) <= tol:
            filtered_exp.append(record)
            filtered_perm.append(_permutation_list[1][idx])
    return filtered_exp, [_permutation_list[0], filtered_perm]


def trace_binary(params_full, _system):
    """Trace a binary system from the full parameter set."""
    params_binary = get_subset_of_parameters(
        params_full, _system["subset_idx"]
    )
    y_init = build_initial_guess_binary(
        T_INIT_BINARY_LLE,
        PRESSURE,
        params_binary,
        reverse_order=_system["reverse_order"],
    )
    trace_kwargs = {}
    if "stuck_progress_tol" in _system:
        trace_kwargs["stuck_progress_tol"] = _system["stuck_progress_tol"]
    y_lst, _, _, _, _, _ = trace_parameter(
        trace_index=-1,
        y_init=y_init,
        trace_start=T_INIT_BINARY_LLE,
        trace_target=T_FINAL,
        pressure=PRESSURE,
        params=params_binary,
        step_size_init=_system["step_size_init"],
        newton_tol=1e-5,
        critical_point_tol=_system["critical_point_tol"],
        max_step_fraction=_system["max_step_fraction"],
        **trace_kwargs,
    )
    return interpret_y_vector(y_lst, PRESSURE, params_binary)


def trace_ternary(params_full, _system):
    """Trace a ternary system from the full parameter set."""
    params_binary = get_subset_of_parameters(
        params_full, _system["binary_subset_idx"]
    )
    y_init_binary = build_initial_guess_binary(
        T_TERNARY_LLE,
        PRESSURE,
        params_binary,
        reverse_order=_system["reverse_order"],
    )
    y_init_ternary = add_component_to_y_vec(y_init_binary, 1e-5, 1e-5)
    params_ternary = get_subset_of_parameters(
        params_full, _system["subset_idx"]
    )
    trace_kwargs = {}
    if "stuck_progress_tol" in _system:
        trace_kwargs["stuck_progress_tol"] = _system["stuck_progress_tol"]
    y_lst, _, _, _, _, _ = trace_parameter(
        trace_index=_system["trace_index"],
        y_init=y_init_ternary,
        trace_start=1e-5,
        trace_target=1.0,
        pressure=PRESSURE,
        params=params_ternary,
        step_size_init=_system["step_size_init"],
        newton_tol=1e-5,
        fixed_index=[-1],
        critical_point_tol=_system["critical_point_tol"],
        **trace_kwargs,
    )
    return interpret_y_vector(y_lst, PRESSURE, params_ternary)


def plot_binary_system(_calc_cases, _exp_dicts, _permutation_list, idx_x_axis):
    """Plot binary data with model accuracy included in calc labels."""
    component_list = [
        _calc_cases[0]["calc_dict"][f"comp{i + 1}_name"]
        for i in range(
            _calc_cases[0]["calc_dict"]["params"]["number_components"]
        )
    ]
    xlabel = f"Mass fraction {component_list[idx_x_axis]} [-]"
    _fig, ax = binary_axes(xlabel=xlabel, ylabel="T [K]")

    for case in _calc_cases:
        calc_dict = case["calc_dict"]
        distance_analysis = get_minimal_distance(
            calc_dict,
            _exp_dicts,
            _permutation_list,
            binodal_sampling=8,
            which_idx_binary=idx_x_axis,
        )

        order = _permutation_list[0][0]
        phase_i = binary_coords(
            calc_dict["w_I_lst"],
            calc_dict["temperature_K"],
            order=order,
            which_idx=idx_x_axis,
        )
        phase_ii = binary_coords(
            calc_dict["w_II_lst"],
            calc_dict["temperature_K"],
            order=order,
            which_idx=idx_x_axis,
        )
        avg_dist = distance_analysis[0]
        label = f"{case['label']}, d={avg_dist:.2f}"
        plot_binary(
            ax,
            phase_i,
            color=case["color"],
            linestyle=case["linestyle"],
            label=label,
        )
        plot_binary(
            ax,
            phase_ii,
            color=case["color"],
            linestyle=case["linestyle"],
        )

    exp_color = "0.0"
    for dict_idx, current_dict in enumerate(_exp_dicts):
        order = _permutation_list[1][dict_idx]
        phase_i = binary_coords(
            current_dict["w_I_lst"],
            current_dict["temperature_K"],
            order=order,
            which_idx=idx_x_axis,
        )
        marker = EXP_MARKERS[dict_idx % len(EXP_MARKERS)]
        exp_label = "Experimental Data" if dict_idx == 0 else None
        scatter_binary(
            ax,
            phase_i,
            color=exp_color,
            marker=marker,
            s=EXP_MARKER_SIZE_BINARY,
            linewidths=EXP_MARKER_LINEWIDTH,
            label=exp_label,
        )
        if current_dict["type"] == "tie_line":
            phase_ii = binary_coords(
                current_dict["w_II_lst"],
                current_dict["temperature_K"],
                order=order,
                which_idx=idx_x_axis,
            )
            scatter_binary(
                ax,
                phase_ii,
                color=exp_color,
                marker=marker,
                s=EXP_MARKER_SIZE_BINARY,
                linewidths=EXP_MARKER_LINEWIDTH,
            )

    ax.set_xlim(-0.01, 1.0)
    ax.set_ylim(260, 430)
    ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5), frameon=False)
    return _fig


def plot_ternary_system(_calc_cases, _exp_dicts, _permutation_list):
    """Plot ternary data with model accuracy included in calc labels."""
    _exp_dicts, _permutation_list = filter_exp_by_temperature(
        _exp_dicts,
        _permutation_list,
        TARGET_TERNARY_TEMP,
        TERNARY_TEMP_TOL,
    )
    component_list = [
        _calc_cases[0]["calc_dict"][f"comp{i + 1}_name"]
        for i in range(
            _calc_cases[0]["calc_dict"]["params"]["number_components"]
        )
    ]
    labels = [TERNARY_LABELS.get(name, name) for name in component_list]
    _fig, ax = ternary_axes(labels=labels)

    for case in _calc_cases:
        calc_dict = case["calc_dict"]
        distance_analysis = get_minimal_distance(
            calc_dict,
            _exp_dicts,
            _permutation_list,
            binodal_sampling=8,
        )

        order = _permutation_list[0][0]
        phase_i = ternary_coords(calc_dict["w_I_lst"], order=order)
        phase_ii = ternary_coords(calc_dict["w_II_lst"], order=order)
        envelope = close_envelope(
            phase_i,
            phase_ii,
            reverse="right",
            close=False,
        )
        avg_dist = distance_analysis[0]
        name = case["label"]
        score = f"({avg_dist:.0f})"
        label = f"{name} {score}"
        plot_ternary(
            ax,
            envelope,
            label=label,
            linewidth=1.4,
            zorder=10,
            c=case["color"],
            linestyle=case["linestyle"],
        )

    exp_color = "0.0"
    for dict_idx, current_dict in enumerate(_exp_dicts):
        order = _permutation_list[1][dict_idx]
        phase_i = ternary_coords(current_dict["w_I_lst"], order=order)
        marker = EXP_MARKERS[dict_idx % len(EXP_MARKERS)]
        exp_label = "Exp. Data" if dict_idx == 0 else None
        scatter_ternary(
            ax,
            phase_i,
            idx=None,
            label=exp_label,
            linewidth=EXP_MARKER_LINEWIDTH,
            zorder=10,
            c=exp_color,
            s=EXP_MARKER_SIZE_TERNARY,
            marker=marker,
        )
        if current_dict["type"] == "tie_line":
            phase_ii = ternary_coords(current_dict["w_II_lst"], order=order)
            scatter_ternary(
                ax,
                phase_ii,
                idx=None,
                linewidth=EXP_MARKER_LINEWIDTH,
                zorder=10,
                c=exp_color,
                s=EXP_MARKER_SIZE_TERNARY,
                marker=marker,
            )

    ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5), frameon=False)
    return _fig


def save_figure(_fig, components):
    """Save figure as PDF in ./figures named by script and component list."""
    stem = Path(__file__).stem
    comp_slug = "_".join(components)
    out_dir = Path.cwd() / "figures"
    out_dir.mkdir(exist_ok=True)
    filename = out_dir / f"{stem}_{comp_slug}.pdf"
    _fig.savefig(filename, bbox_inches="tight")


optimized_params_bahrabadi_all_kij = {
    "comp12_kij_intercept": -0.02391685780544961,
    "comp13_kij_intercept": -0.07679136434180633,
    "comp14_kij_intercept": -0.7073522163072805,
    "comp23_kij_intercept": -0.14841082672007166,
    "comp24_kij_intercept": -1.090375823607204,
    "comp34_kij_intercept": -0.8660356757399423,
}


optimized_params_bahrabadi = {
    "comp14_kij_intercept": -0.49147828002618943,
    "comp24_kij_intercept": -0.6977683189278325,
    "comp34_kij_intercept": -0.8106882487102736,
}


SOURCES = ["bahrabadi"]
SYSTEM_LABELS = ["Jovein"]
OPTIMIZED_PARAMS = {
    "bahrabadi": optimized_params_bahrabadi,
}
OPTIMIZED_PARAMS_ALL_KIJ = {
    "bahrabadi": optimized_params_bahrabadi_all_kij,
}


def build_full_params(_source, opt_dict=None):
    """
    Build full parameter set for a source, optionally applying optimizations.
    """
    params_full = get_parameter_set_from_tables(COMPONENTS_ALL, _source)
    params_full = add_mixing_association(params_full)
    if opt_dict:
        params_full = params_full | opt_dict
    return params_full


TABLE_SOURCE_ORDER = [
    ("bahrabadi", "Jovein"),
]


def system_short_label(components):
    """Create a short label for the system"""
    return "--".join(TERNARY_LABELS.get(comp, comp) for comp in components)


def filter_exp_for_system(_exp_dicts, _permutation_list, _system):
    """Keep only experimental records matching the system source tag."""
    _filtered = []
    for exp, perm in zip(_exp_dicts, _permutation_list[1]):
        _source = exp.get("source", "")
        if _system["which_source"] in _source:
            _filtered.append((exp, perm))

    if not _filtered:
        return [], [_permutation_list[0], []]

    exp_filt, perm_filt = map(list, zip(*_filtered))
    return exp_filt, [_permutation_list[0], perm_filt]


def compute_error_for_case(_calc_dict, _exp_dicts, _permutation_list, _system):
    """Compute the same average distance used in the plotting labels."""
    if not _exp_dicts:
        return np.nan

    if _system["kind"] == "ternary":
        _exp_dicts, _permutation_list = filter_exp_by_temperature(
            _exp_dicts,
            _permutation_list,
            TARGET_TERNARY_TEMP,
            TERNARY_TEMP_TOL,
        )
        if not _exp_dicts:
            return np.nan
        dist = get_minimal_distance(
            _calc_dict,
            _exp_dicts,
            _permutation_list,
            binodal_sampling=8,
        )
    else:
        dist = get_minimal_distance(
            _calc_dict,
            _exp_dicts,
            _permutation_list,
            binodal_sampling=8,
            which_idx_binary=_system["which_idx_binary"],
        )

    return float(dist[0])


def generate_error_table(
    systems=SYSTEMS,
    source_order=TABLE_SOURCE_ORDER,
    optimized_params=OPTIMIZED_PARAMS,
):
    """
    Generate a dataframe with literature and optimized errors for all systems.

    Returns
    -------
    df_display : pd.DataFrame
        Flat table with columns:
        System, Grob Literature, Grob Optimized, ...
    latex_str : str
        LaTeX tabular string with the grouped column style you showed.
    raw_results : dict
        Nested dictionary of all values.
    """
    _raw_results = {}
    rows = []

    for _system in systems:
        _exp_dicts, _permutation_list = load_exp_records(_system["components"])
        _exp_dicts, _permutation_list = filter_exp_for_system(
            _exp_dicts, _permutation_list, _system
        )

        row_label = system_short_label(_system["components"])
        _raw_results[row_label] = {}

        row = {"System": row_label}

        for source_key, source_name in source_order:
            _params_base = build_full_params(source_key)
            _params_opt = build_full_params(
                source_key, optimized_params[source_key]
            )

            if _system["kind"] == "binary":
                _calc_base = trace_binary(_params_base, _system)
                _calc_opt = trace_binary(_params_opt, _system)
            else:
                _calc_base = trace_ternary(_params_base, _system)
                _calc_opt = trace_ternary(_params_opt, _system)

            err_lit = compute_error_for_case(
                _calc_base, _exp_dicts, _permutation_list, _system
            )
            err_opt = compute_error_for_case(
                _calc_opt, _exp_dicts, _permutation_list, _system
            )

            _raw_results[row_label][source_name] = {
                "Literature": err_lit,
                "Optimized": err_opt,
            }

            row[f"{source_name} Literature"] = err_lit
            row[f"{source_name} Optimized"] = err_opt

        rows.append(row)

    df_display = pd.DataFrame(rows)

    numeric_cols = [c for c in df_display.columns if c != "System"]
    total_row = {"System": "Total"}
    for col in numeric_cols:
        total_row[col] = df_display[col].sum(skipna=True)
    df_display = pd.concat(
        [df_display, pd.DataFrame([total_row])], ignore_index=True
    )

    latex_str = dataframe_to_grouped_latex(
        df_display, source_order=source_order
    )
    return df_display, latex_str, _raw_results


def dataframe_to_grouped_latex(
    df, source_order=TABLE_SOURCE_ORDER, decimals=2
):
    """Convert the error dataframe to the grouped LaTeX tabular format."""
    lines = []
    lines.append(r"\begin{tabular}{lcccccc}")
    lines.append(r"\hline")
    lines.append(
        r"System "
        + " & "
        + " & ".join(
            rf"\multicolumn{{2}}{{c}}{{{source_name}}}"
            for _, source_name in source_order
        )
        + r" \\"
    )
    lines.append(r"\cline{2-7}")
    lines.append(
        r"& "
        + " & ".join([r"Literature & Optimized"] * len(source_order))
        + r" \\"
    )
    lines.append(r"\hline")

    ternary_started = False
    for _, row in df.iterrows():
        _system = row["System"]

        if _system == "Total":
            lines.append(r"\hline")
        elif _system.count("--") == 2 and not ternary_started:
            lines.append(r"\hline")
            ternary_started = True

        vals = []
        for _, source_name in source_order:
            for kind in ("Literature", "Optimized"):
                value = row[f"{source_name} {kind}"]
                if pd.isna(value):
                    vals.append("--")
                else:
                    vals.append(f"{value:.{decimals}f}")

        lines.append(f"{_system} & " + " & ".join(vals) + r" \\")

    lines.append(r"\hline")
    lines.append(r"\end{tabular}")
    return "\n".join(lines)


if __name__ == "__main__":
    source_colors = ["red"]

    for system in SYSTEMS:
        exp_dicts, permutation_list = load_exp_records(system["components"])

        filtered = [
            (exp, perm)
            for exp, perm in zip(exp_dicts, permutation_list[1])
            if system["which_source"] in exp.get("source")
        ]

        exp_dicts, permutation_list[1] = (
            map(list, zip(*filtered)) if filtered else ([], [])
        )

        calc_cases = []
        for source, color, source_label in zip(
            SOURCES, source_colors, SYSTEM_LABELS
        ):
            params_base = build_full_params(source)
            params_opt = build_full_params(source, OPTIMIZED_PARAMS[source])
            params_opt_all_kij = build_full_params(
                source, OPTIMIZED_PARAMS_ALL_KIJ[source]
            )

            if system["kind"] == "binary":
                calc_base = trace_binary(params_base, system)
                calc_opt = trace_binary(params_opt, system)
                calc_opt_all_kij = trace_binary(params_opt_all_kij, system)
            else:
                calc_base = trace_ternary(params_base, system)
                calc_opt = trace_ternary(params_opt, system)
                calc_opt_all_kij = trace_ternary(params_opt_all_kij, system)
            calc_cases.append(
                {
                    "label": f"{source_label} Lit.",
                    "calc_dict": calc_base,
                    "color": color,
                    "linestyle": "-",
                }
            )
            calc_cases.append(
                {
                    "label": f"{source_label} Opt. AA",
                    "calc_dict": calc_opt,
                    "color": color,
                    "linestyle": "--",
                }
            )
            calc_cases.append(
                {
                    "label": f"{source_label} Opt. all",
                    "calc_dict": calc_opt_all_kij,
                    "color": color,
                    "linestyle": ":",
                }
            )
        if system["kind"] == "binary":
            fig = plot_binary_system(
                calc_cases,
                exp_dicts,
                permutation_list,
                idx_x_axis=system["which_idx_binary"],
            )
            save_figure(fig, system["components"])
        else:
            fig = plot_ternary_system(
                calc_cases,
                exp_dicts,
                permutation_list,
            )
            save_figure(fig, system["components"])

    df_errors, latex_table, raw_results = generate_error_table()

    path = Path("tables/system_errors_only_kij_aa.tex")
    path.write_text(latex_table, encoding="utf-8")

# %%

PAIR_LABELS = {
    "comp12": "W-B",
    "comp13": "W-BA",
    "comp14": "W-AA",
    "comp23": "B-BA",
    "comp24": "B-AA",
    "comp34": "BA-AA",
}


def _is_close(a, b, rtol=1e-10, atol=1e-10):
    if pd.isna(a) or pd.isna(b):
        return False
    return abs(a - b) <= atol + rtol * abs(b)


def _default_cross_assoc_epsilon(params, pair):
    """
    Return default epsilon_k_ab = 0.5 * (eps_AA + eps_BB) for a compij pair.

    Assumes params contain:
    - comp1_epsilon_k_ab
    - comp2_epsilon_k_ab
    etc.
    """
    i = int(pair[4])
    j = int(pair[5])

    eps_i = params.get(f"comp{i}_epsilon_k_ab", np.nan)
    eps_j = params.get(f"comp{j}_epsilon_k_ab", np.nan)

    if pd.isna(eps_i) or pd.isna(eps_j):
        return np.nan

    return 0.5 * (eps_i + eps_j)


def get_pair_parameter_info(
    source_key, pair, optimized_params=OPTIMIZED_PARAMS
):
    """
    For one source and one pair, determine whether kij_intercept or
    epsilon_k_ab is the optimized quantity, then return literature and
    optimized values for that parameter.

    If literature epsilon_k_ab is just the default mixing-rule value
    0.5 * (pure1 + pure2), it is treated as not explicitly specified
    and shown as '--' in the table.

    Returns
    -------
    uses_epsilon : bool
        True if epsilon_k_ab is used, False if k_ij is used
    lit_val : float or np.nan
    opt_val : float or np.nan
    """
    params_lit = build_full_params(source_key)
    opt_dict = optimized_params[source_key]

    kij_key = f"{pair}_kij_intercept"
    eps_key = f"{pair}_epsilon_k_ab"

    if eps_key in opt_dict:
        uses_epsilon = True

        lit_eps = params_lit.get(eps_key, np.nan)
        default_eps = _default_cross_assoc_epsilon(params_lit, pair)

        if pd.isna(lit_eps) or _is_close(lit_eps, default_eps):
            lit_val = np.nan
        else:
            lit_val = lit_eps

        opt_val = opt_dict[eps_key]

    else:
        uses_epsilon = False
        lit_val = params_lit.get(kij_key, np.nan)

        if kij_key in opt_dict:
            opt_val = opt_dict[kij_key]
        else:
            opt_val = lit_val

    return uses_epsilon, lit_val, opt_val


def generate_interaction_parameter_table(
    source_order=TABLE_SOURCE_ORDER,
    optimized_params=OPTIMIZED_PARAMS,
):
    """
    Create dataframe of literature vs optimized interaction parameters.
    Uses epsilon_k_ab if that is what was optimized, otherwise k_ij.
    """
    all_pairs = ["comp12", "comp13", "comp14", "comp23", "comp24", "comp34"]
    rows = []

    for pair in all_pairs:
        row = {"Pair": PAIR_LABELS.get(pair, pair)}

        for source_key, source_name in source_order:
            uses_epsilon, lit_val, opt_val = get_pair_parameter_info(
                source_key, pair, optimized_params=optimized_params
            )

            row[f"{source_name} UsesEpsilon"] = uses_epsilon
            row[f"{source_name} Literature"] = lit_val
            row[f"{source_name} Optimized"] = opt_val

        rows.append(row)

    _df_params = pd.DataFrame(rows)
    latex_str = interaction_parameter_dataframe_to_latex(
        _df_params, source_order=source_order
    )
    return _df_params, latex_str


def interaction_parameter_dataframe_to_latex(
    df, source_order=TABLE_SOURCE_ORDER, decimals=2
):
    """
    LaTeX table with Literature | Optimized for each source.
    Values marked with * correspond to epsilon^AB instead of k_ij.
    """
    lines = []
    lines.append(r"\begin{tabular}{lcccccc}")
    lines.append(r"\hline")
    lines.append(
        r"Pair "
        + " & "
        + " & ".join(
            rf"\multicolumn{{2}}{{c}}{{{source_name}}}"
            for _, source_name in source_order
        )
        + r" \\"
    )
    lines.append(r"\cline{2-7}")
    lines.append(
        r"& "
        + " & ".join([r"Literature & Optimized"] * len(source_order))
        + r" \\"
    )
    lines.append(r"\hline")

    for _, row in df.iterrows():
        vals = []
        for _, source_name in source_order:
            uses_epsilon = bool(row[f"{source_name} UsesEpsilon"])

            for kind in ("Literature", "Optimized"):
                value = row[f"{source_name} {kind}"]
                if pd.isna(value):
                    vals.append("--")
                else:
                    formatted = f"{value:.{decimals}f}"
                    if uses_epsilon:
                        formatted += r"$^\ast$"
                    vals.append(formatted)

        lines.append(f"{row['Pair']} & " + " & ".join(vals) + r" \\")

    lines.append(r"\hline")
    lines.append(
        r"\multicolumn{7}{l}{$^\ast$ Values marked with an asterisk correspond"
        r" to $\varepsilon^\mathrm{AB}$; entries shown as -- indicate no"
        r" explicitly specified literature value. All other values correspond"
        r"  to $k_{ij}$.} \\"
    )
    lines.append(r"\end{tabular}")
    return "\n".join(lines)


df_params, latex_params = generate_interaction_parameter_table()

Path("tables/interaction_parameter_table_only_kij_aa.tex").write_text(
    latex_params, encoding="utf-8"
)
