import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from scipy.optimize import curve_fit
from scipy.special import voigt_profile
import json
import warnings
import os

warnings.filterwarnings('ignore')

# ─────────────────────────────────────────────
# 1. DATA LOADING
# ─────────────────────────────────────────────
data_path = '/Users/maxim.ziatdinov/Code/SciLink/test_series_annealing_data/out/temp_spectrum_0.npy'
raw = np.load(data_path, allow_pickle=True)

# Handle various array shapes
if raw.ndim == 1:
    # Assume interleaved or need to infer x
    N = len(raw)
    x_data = np.linspace(0, 100, N)
    y_data = raw.astype(float)
elif raw.ndim == 2:
    if raw.shape[0] == 2:
        x_data = raw[0].astype(float)
        y_data = raw[1].astype(float)
    elif raw.shape[1] == 2:
        x_data = raw[:, 0].astype(float)
        y_data = raw[:, 1].astype(float)
    else:
        # Take first two columns
        x_data = raw[:, 0].astype(float)
        y_data = raw[:, 1].astype(float)
else:
    x_data = np.linspace(0, 100, raw.size)
    y_data = raw.flatten().astype(float)

print(f"Loaded data: {len(x_data)} points, x=[{x_data.min():.3f}, {x_data.max():.3f}], y=[{y_data.min():.6f}, {y_data.max():.6f}]")

# ─────────────────────────────────────────────
# 2. MODEL DEFINITION
# ─────────────────────────────────────────────
def voigt_model(x, a0, a1,
                A1, mu1, sigma1, gamma1,
                A2, mu2, sigma2, gamma2,
                A3, mu3, sigma3, gamma3):
    """
    y(x) = a0 + a1*x + sum_{k=1}^{3} A_k * voigt_profile(x - mu_k, sigma_k, gamma_k)
    voigt_profile is normalized to unit area (integral=1).
    A_k is the integrated area of peak k.
    """
    baseline = a0 + a1 * x
    p1 = A1 * voigt_profile(x - mu1, sigma1, gamma1)
    p2 = A2 * voigt_profile(x - mu2, sigma2, gamma2)
    p3 = A3 * voigt_profile(x - mu3, sigma3, gamma3)
    return baseline + p1 + p2 + p3

# Verify model evaluates
_test_x = np.linspace(0, 100, 10)
_test_y = voigt_model(_test_x, 0.1, 0.0, 5.0, 30.0, 1.0, 1.5, 4.0, 55.0, 1.0, 1.5, 3.0, 79.0, 1.0, 1.5)
assert np.all(np.isfinite(_test_y)), "Model test failed"
print("Model verification: PASSED")

# ─────────────────────────────────────────────
# 3. INITIAL GUESS ESTIMATION
# ─────────────────────────────────────────────
# Baseline regions
pre_mask = x_data <= 25
post_mask = x_data >= 85
baseline_mask = pre_mask | post_mask

x_bl = x_data[baseline_mask]
y_bl = y_data[baseline_mask]

if len(x_bl) >= 2:
    bl_coeffs = np.polyfit(x_bl, y_bl, 1)
    a1_init = float(bl_coeffs[0])
    a0_init = float(bl_coeffs[1])
else:
    a0_init = float(np.mean(y_data[:5]))
    a1_init = 0.0

print(f"Baseline init: a0={a0_init:.4f}, a1={a1_init:.6f}")

# Estimate baseline-subtracted signal at peak regions
y_bl_sub = y_data - (a0_init + a1_init * x_data)

# Peak center initial guesses — spectrum 0
mu_init = [29.0, 54.0, 78.0]

def get_amplitude_init(x, y_sub, mu, half_width=8.0):
    mask = (x >= mu - half_width) & (x <= mu + half_width)
    if mask.sum() == 0:
        return max(float(np.max(y_sub)), 1.0)
    local_y = y_sub[mask]
    peak_val = float(np.max(local_y))
    # A_k = area ≈ peak_height / voigt_profile(0, sigma, gamma)
    # For sigma=1, gamma=1.5: voigt_profile(0,1,1.5) ≈ estimated
    vp0 = voigt_profile(0.0, 1.0, 1.5)
    if vp0 > 0 and peak_val > 0:
        return peak_val / vp0
    return max(peak_val * 3.0, 1.0)

A_init = [get_amplitude_init(x_data, y_bl_sub, mu) for mu in mu_init]
print(f"Amplitude inits: A1={A_init[0]:.4f}, A2={A_init[1]:.4f}, A3={A_init[2]:.4f}")

p0_base = [
    a0_init, a1_init,
    A_init[0], mu_init[0], 1.0, 1.5,
    A_init[1], mu_init[1], 1.0, 1.5,
    A_init[2], mu_init[2], 1.0, 1.5
]

# ─────────────────────────────────────────────
# 4. PARAMETER BOUNDS
# ─────────────────────────────────────────────
# (lower, upper) for each of 14 parameters:
# a0, a1, A1, mu1, sigma1, gamma1, A2, mu2, sigma2, gamma2, A3, mu3, sigma3, gamma3
lower_bounds = [
    -np.inf, 0.0,          # a0 unconstrained, a1 >= 0
    0.0, 20.0, 0.0, 0.0,  # A1, mu1, sigma1, gamma1
    0.0, 45.0, 0.0, 0.0,  # A2, mu2, sigma2, gamma2
    0.0, 70.0, 0.0, 0.0   # A3, mu3, sigma3, gamma3
]
upper_bounds = [
    np.inf, np.inf,        # a0, a1
    np.inf, 42.0, 5.0, 8.0,  # A1, mu1, sigma1, gamma1
    np.inf, 68.0, 5.0, 8.0,  # A2, mu2, sigma2, gamma2
    np.inf, 92.0, 5.0, 8.0   # A3, mu3, sigma3, gamma3
]
bounds = (lower_bounds, upper_bounds)

# ─────────────────────────────────────────────
# 5. MULTI-START FITTING
# ─────────────────────────────────────────────
sigma_factors = [0.5, 0.8, 1.0, 1.5, 2.0]
gamma_factors = [0.5, 1.0, 1.5, 2.5, 3.5]
ampl_perturbs = [0.8, 1.0, 1.2]
center_perturbs = [-2.0, 0.0, 2.0]

best_rss = np.inf
best_popt = None
best_pcov = None
converged_solutions = []
all_rss_values = []

n_starts = 0
max_starts = 48  # limit for speed

for sf in sigma_factors:
    for gf in gamma_factors:
        if n_starts >= max_starts:
            break
        for ap in ampl_perturbs:
            if n_starts >= max_starts:
                break
            for cp in center_perturbs[:2]:  # limit center perturbs
                if n_starts >= max_starts:
                    break

                p0_try = [
                    a0_init, a1_init,
                    A_init[0] * ap, mu_init[0] + cp, 1.0 * sf, 1.5 * gf,
                    A_init[1] * ap, mu_init[1] + cp, 1.0 * sf, 1.5 * gf,
                    A_init[2] * ap, mu_init[2] + cp, 1.0 * sf, 1.5 * gf
                ]

                # Clip to bounds
                p0_clipped = []
                for i, (v, lo, hi) in enumerate(zip(p0_try, lower_bounds, upper_bounds)):
                    lo_c = lo if np.isfinite(lo) else -1e10
                    hi_c = hi if np.isfinite(hi) else 1e10
                    p0_clipped.append(np.clip(v, lo_c + 1e-8, hi_c - 1e-8))

                try:
                    popt, pcov = curve_fit(
                        voigt_model,
                        x_data, y_data,
                        p0=p0_clipped,
                        bounds=bounds,
                        method='trf',
                        maxfev=20000,
                        ftol=1e-12,
                        xtol=1e-12,
                        gtol=1e-12
                    )

                    # Check covariance validity
                    if not np.all(np.isfinite(pcov)):
                        n_starts += 1
                        continue

                    # Check bounds margin
                    in_bounds = True
                    for i, (v, lo, hi) in enumerate(zip(popt, lower_bounds, upper_bounds)):
                        lo_c = lo if np.isfinite(lo) else -1e38
                        hi_c = hi if np.isfinite(hi) else 1e38
                        if v <= lo_c + 1e-6 or v >= hi_c - 1e-6:
                            in_bounds = False
                            break

                    residuals = y_data - voigt_model(x_data, *popt)
                    rss = float(np.sum(residuals**2))
                    all_rss_values.append(rss)

                    converged_solutions.append({'popt': popt, 'pcov': pcov, 'rss': rss})

                    if rss < best_rss:
                        best_rss = rss
                        best_popt = popt.copy()
                        best_pcov = pcov.copy()

                except Exception as e:
                    pass

                n_starts += 1

print(f"Multi-start: {n_starts} attempts, {len(converged_solutions)} converged")

if best_popt is None:
    print("WARNING: Multi-start failed, attempting single fit with base p0")
    try:
        best_popt, best_pcov = curve_fit(
            voigt_model, x_data, y_data,
            p0=p0_base, bounds=bounds,
            method='trf', maxfev=50000
        )
        residuals = y_data - voigt_model(x_data, *best_popt)
        best_rss = float(np.sum(residuals**2))
        converged_solutions = [{'popt': best_popt, 'pcov': best_pcov, 'rss': best_rss}]
        all_rss_values = [best_rss]
    except Exception as e:
        raise RuntimeError(f"All fitting attempts failed: {e}")

# Convergence confidence metric
if len(all_rss_values) > 1:
    median_rss = float(np.median(all_rss_values))
    best_vs_median = best_rss / median_rss if median_rss > 0 else 1.0
else:
    median_rss = best_rss
    best_vs_median = 1.0

print(f"Best RSS: {best_rss:.6f}, Median RSS: {median_rss:.6f}, Ratio: {best_vs_median:.4f}")

# ─────────────────────────────────────────────
# 6. EXTRACT PARAMETERS
# ─────────────────────────────────────────────
popt = best_popt
pcov = best_pcov
perr = np.sqrt(np.diag(pcov))

a0, a1 = popt[0], popt[1]
A1, mu1, sigma1, gamma1 = popt[2], popt[3], popt[4], popt[5]
A2, mu2, sigma2, gamma2 = popt[6], popt[7], popt[8], popt[9]
A3, mu3, sigma3, gamma3 = popt[10], popt[11], popt[12], popt[13]

da0, da1 = perr[0], perr[1]
dA1, dmu1, dsigma1, dgamma1 = perr[2], perr[3], perr[4], perr[5]
dA2, dmu2, dsigma2, dgamma2 = perr[6], perr[7], perr[8], perr[9]
dA3, dmu3, dsigma3, dgamma3 = perr[10], perr[11], perr[12], perr[13]

print(f"\nFitted parameters:")
print(f"  Baseline: a0={a0:.4f}±{da0:.4f}, a1={a1:.6f}±{da1:.6f}")
print(f"  Peak 1: A={A1:.4f}±{dA1:.4f}, mu={mu1:.4f}±{dmu1:.4f}, sigma={sigma1:.4f}±{dsigma1:.4f}, gamma={gamma1:.4f}±{dgamma1:.4f}")
print(f"  Peak 2: A={A2:.4f}±{dA2:.4f}, mu={mu2:.4f}±{dmu2:.4f}, sigma={sigma2:.4f}±{dsigma2:.4f}, gamma={gamma2:.4f}±{dgamma2:.4f}")
print(f"  Peak 3: A={A3:.4f}±{dA3:.4f}, mu={mu3:.4f}±{dmu3:.4f}, sigma={sigma3:.4f}±{dsigma3:.4f}, gamma={gamma3:.4f}±{dgamma3:.4f}")

# ─────────────────────────────────────────────
# 7. FIT QUALITY
# ─────────────────────────────────────────────
y_fit = voigt_model(x_data, *popt)
residuals = y_data - y_fit

N = len(x_data)
N_params = 14
rss = float(np.sum(residuals**2))
chi2_red = rss / (N - N_params)

ss_res = rss
ss_tot = float(np.sum((y_data - np.mean(y_data))**2))
r_squared = 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
rmse = float(np.sqrt(np.mean(residuals**2)))

print(f"\nFit quality: R²={r_squared:.6f}, RMSE={rmse:.6f}, chi2_red={chi2_red:.6f}")

# ─────────────────────────────────────────────
# 8. DERIVED QUANTITIES
# ─────────────────────────────────────────────
def voigt_fwhm(sigma, gamma):
    """FWHM_V ≈ 0.5346*2*gamma + sqrt(0.2166*(2*gamma)^2 + (2.3548*sigma)^2)"""
    fL = 2.0 * gamma
    fG = 2.3548 * sigma
    return 0.5346 * fL + np.sqrt(0.2166 * fL**2 + fG**2)

def voigt_fwhm_err(sigma, gamma, dsigma, dgamma):
    """Error propagation for FWHM_V"""
    fL = 2.0 * gamma
    fG = 2.3548 * sigma
    denom = np.sqrt(0.2166 * fL**2 + fG**2)
    if denom < 1e-12:
        return 0.0
    dFWHM_dgamma = 0.5346 * 2 + (0.2166 * 2 * fL * 2) / (2 * denom)
    dFWHM_dsigma = (fG * 2.3548) / denom
    return np.sqrt((dFWHM_dgamma * dgamma)**2 + (dFWHM_dsigma * dsigma)**2)

def voigt_area(A_k):
    """Integrated area = A_k (since voigt_profile is normalized to unit area)"""
    return A_k

fwhm1 = voigt_fwhm(sigma1, gamma1)
fwhm2 = voigt_fwhm(sigma2, gamma2)
fwhm3 = voigt_fwhm(sigma3, gamma3)
dfwhm1 = voigt_fwhm_err(sigma1, gamma1, dsigma1, dgamma1)
dfwhm2 = voigt_fwhm_err(sigma2, gamma2, dsigma2, dgamma2)
dfwhm3 = voigt_fwhm_err(sigma3, gamma3, dsigma3, dgamma3)

eta1 = gamma1 / (sigma1 + gamma1) if (sigma1 + gamma1) > 0 else 0.0
eta2 = gamma2 / (sigma2 + gamma2) if (sigma2 + gamma2) > 0 else 0.0
eta3 = gamma3 / (sigma3 + gamma3) if (sigma3 + gamma3) > 0 else 0.0

print(f"\nDerived quantities:")
print(f"  Peak 1: FWHM={fwhm1:.4f}±{dfwhm1:.4f}, area={A1:.4f}±{dA1:.4f}, eta={eta1:.4f}")
print(f"  Peak 2: FWHM={fwhm2:.4f}±{dfwhm2:.4f}, area={A2:.4f}±{dA2:.4f}, eta={eta2:.4f}")
print(f"  Peak 3: FWHM={fwhm3:.4f}±{dfwhm3:.4f}, area={A3:.4f}±{dA3:.4f}, eta={eta3:.4f}")

# ─────────────────────────────────────────────
# 9. RESIDUAL DIAGNOSTICS (5 ZONES)
# ─────────────────────────────────────────────
zones = {
    'pre_baseline (x=0-25)': (x_data <= 25),
    'peak1_region (x=28-33)': (x_data >= 28) & (x_data <= 33),
    'peak2_region (x=52-58)': (x_data >= 52) & (x_data <= 58),
    'peak3_region (x=76-82)': (x_data >= 76) & (x_data <= 82),
    'post_baseline (x=85-100)': (x_data >= 85)
}

residual_means = {}
flags = []
print("\nResidual diagnostics:")
for zone_name, mask in zones.items():
    if mask.sum() > 0:
        mean_res = float(np.mean(residuals[mask]))
        residual_means[zone_name] = mean_res
        flag = ''
        if 'baseline' in zone_name and abs(mean_res) > 0.10:
            flag = ' *** BASELINE MODEL INADEQUACY'
            flags.append(f"{zone_name}: baseline offset {mean_res:.4f}")
        elif 'peak' in zone_name and abs(mean_res) > 0.05:
            flag = ' *** RESIDUAL LINESHAPE MISMATCH'
            flags.append(f"{zone_name}: lineshape offset {mean_res:.4f}")
        print(f"  {zone_name}: mean_residual={mean_res:.6f}{flag}")
    else:
        residual_means[zone_name] = None

# ─────────────────────────────────────────────
# 10. LINESHAPE DIAGNOSIS
# ─────────────────────────────────────────────
eta_all = [eta1, eta2, eta3]
if all(e > 0.8 for e in eta_all):
    lineshape_note = "All peaks Lorentzian-dominated (eta>0.8). Pure Lorentzian model (11 params) would be nearly equivalent."
elif all(e < 0.2 for e in eta_all):
    lineshape_note = "All peaks Gaussian-dominated (eta<0.2). Voigt reduces to near-Gaussian."
else:
    lineshape_note = f"Mixed lineshapes: eta1={eta1:.3f}, eta2={eta2:.3f}, eta3={eta3:.3f}. Voigt is appropriate."
print(f"\nLineshape diagnosis: {lineshape_note}")

# ─────────────────────────────────────────────
# 11. INDIVIDUAL PEAK COMPONENTS
# ─────────────────────────────────────────────
x_fine = np.linspace(x_data.min(), x_data.max(), 2000)
baseline_fine = a0 + a1 * x_fine
peak1_fine = A1 * voigt_profile(x_fine - mu1, sigma1, gamma1)
peak2_fine = A2 * voigt_profile(x_fine - mu2, sigma2, gamma2)
peak3_fine = A3 * voigt_profile(x_fine - mu3, sigma3, gamma3)
y_fit_fine = baseline_fine + peak1_fine + peak2_fine + peak3_fine

# ─────────────────────────────────────────────
# 12. VISUALIZATION
# ─────────────────────────────────────────────
fig = plt.figure(figsize=(14, 10))
gs = gridspec.GridSpec(3, 1, height_ratios=[3, 1, 1], hspace=0.08)

ax_main = fig.add_subplot(gs[0])
ax_res = fig.add_subplot(gs[1], sharex=ax_main)
ax_zones = fig.add_subplot(gs[2], sharex=ax_main)

# Main panel
ax_main.scatter(x_data, y_data, s=4, color='black', alpha=0.6, label='Data', zorder=3)
ax_main.plot(x_fine, y_fit_fine, 'r-', lw=2, label=f'Voigt fit (R²={r_squared:.5f})', zorder=4)
ax_main.plot(x_fine, baseline_fine, 'k--', lw=1.2, alpha=0.7, label='Baseline')
ax_main.fill_between(x_fine, baseline_fine, baseline_fine + peak1_fine,
                      alpha=0.35, color='steelblue',
                      label=f'Peak 1 (μ={mu1:.2f}, FWHM={fwhm1:.2f})')
ax_main.fill_between(x_fine, baseline_fine, baseline_fine + peak2_fine,
                      alpha=0.35, color='darkorange',
                      label=f'Peak 2 (μ={mu2:.2f}, FWHM={fwhm2:.2f})')
ax_main.fill_between(x_fine, baseline_fine, baseline_fine + peak3_fine,
                      alpha=0.35, color='forestgreen',
                      label=f'Peak 3 (μ={mu3:.2f}, FWHM={fwhm3:.2f})')

ax_main.set_ylabel('Intensity', fontsize=12)
ax_main.legend(fontsize=8, loc='upper right')
ax_main.set_title(f'Voigt Profile Fit — 3 Peaks + Linear Baseline\n'
                  f'χ²_red={chi2_red:.4f}, RSS={best_rss:.4f}, '
                  f'Converged: {len(converged_solutions)}/{n_starts} starts',
                  fontsize=11)
ax_main.grid(True, alpha=0.3)
plt.setp(ax_main.get_xticklabels(), visible=False)

# Residuals panel
ax_res.axhline(0, color='red', lw=1.5, ls='--')
ax_res.scatter(x_data, residuals, s=3, color='purple', alpha=0.5)
ax_res.axhline(0.05, color='orange', lw=0.8, ls=':', alpha=0.7)
ax_res.axhline(-0.05, color='orange', lw=0.8, ls=':', alpha=0.7)
ax_res.set_ylabel('Residuals', fontsize=10)
ax_res.grid(True, alpha=0.3)
plt.setp(ax_res.get_xticklabels(), visible=False)

# Zone mean residuals panel
zone_names_short = ['Pre-BL\n(0-25)', 'Peak1\n(28-33)', 'Peak2\n(52-58)', 'Peak3\n(76-82)', 'Post-BL\n(85-100)']
zone_x_centers = [12.5, 30.5, 55.0, 79.0, 92.5]
zone_keys = list(zones.keys())

for zone_key, xc, zn in zip(zone_keys, zone_x_centers, zone_names_short):
    mask = zones[zone_key]
    if mask.sum() > 0:
        mr = residual_means[zone_key]
        color = 'red' if abs(mr) > 0.10 else ('orange' if abs(mr) > 0.05 else 'green')
        ax_zones.axvline(xc, color=color, alpha=0.4, lw=8)
        ax_zones.text(xc, 0, f'{mr:.3f}', ha='center', va='center', fontsize=7,
                      fontweight='bold', color='black')

ax_zones.set_ylim(-0.5, 0.5)
ax_zones.set_ylabel('Zone\nmean res.', fontsize=9)
ax_zones.set_xlabel('x', fontsize=12)
ax_zones.grid(True, alpha=0.3)
ax_zones.axhline(0, color='k', lw=0.8)

plt.tight_layout()
save_path = 'fit_visualization.png'
fig.savefig(save_path, dpi=150, bbox_inches='tight')
plt.close(fig)
print(f"\nSaved: {save_path}")

# ─────────────────────────────────────────────
# 13. RESULTS JSON
# ─────────────────────────────────────────────
summary_parts = [
    f"3-peak Voigt fit with linear baseline ({N} points, 14 free parameters).",
    f"R²={r_squared:.5f}, RMSE={rmse:.5f}, chi2_red={chi2_red:.4f}.",
    f"Converged: {len(converged_solutions)}/{n_starts} starts, best_vs_median_RSS={best_vs_median:.4f}.",
    lineshape_note,
]
if flags:
    summary_parts.append("FLAGS: " + '; '.join(flags))

results = {
    "model_type": "3-peak Voigt profile + linear baseline: y(x) = a0 + a1*x + sum_k A_k * V(x-mu_k, sigma_k, gamma_k)",
    "parameters": {
        "baseline": {
            "a0": round(float(a0), 6), "a0_err": round(float(da0), 6),
            "a1": round(float(a1), 8), "a1_err": round(float(da1), 8)
        },
        "peak_1": {
            "center": round(float(mu1), 4), "center_err": round(float(dmu1), 4),
            "amplitude_area": round(float(A1), 4), "amplitude_err": round(float(dA1), 4),
            "sigma_gaussian": round(float(sigma1), 4), "sigma_err": round(float(dsigma1), 4),
            "gamma_lorentzian": round(float(gamma1), 4), "gamma_err": round(float(dgamma1), 4),
            "fwhm": round(float(fwhm1), 4), "fwhm_err": round(float(dfwhm1), 4),
            "eta_effective": round(float(eta1), 4)
        },
        "peak_2": {
            "center": round(float(mu2), 4), "center_err": round(float(dmu2), 4),
            "amplitude_area": round(float(A2), 4), "amplitude_err": round(float(dA2), 4),
            "sigma_gaussian": round(float(sigma2), 4), "sigma_err": round(float(dsigma2), 4),
            "gamma_lorentzian": round(float(gamma2), 4), "gamma_err": round(float(dgamma2), 4),
            "fwhm": round(float(fwhm2), 4), "fwhm_err": round(float(dfwhm2), 4),
            "eta_effective": round(float(eta2), 4)
        },
        "peak_3": {
            "center": round(float(mu3), 4), "center_err": round(float(dmu3), 4),
            "amplitude_area": round(float(A3), 4), "amplitude_err": round(float(dA3), 4),
            "sigma_gaussian": round(float(sigma3), 4), "sigma_err": round(float(dsigma3), 4),
            "gamma_lorentzian": round(float(gamma3), 4), "gamma_err": round(float(dgamma3), 4),
            "fwhm": round(float(fwhm3), 4), "fwhm_err": round(float(dfwhm3), 4),
            "eta_effective": round(float(eta3), 4)
        }
    },
    "fit_quality": {
        "r_squared": round(float(r_squared), 6),
        "rmse": round(float(rmse), 6),
        "rss": round(float(best_rss), 6),
        "chi2_reduced": round(float(chi2_red), 6),
        "n_points": int(N),
        "n_params": int(N_params),
        "converged_starts": int(len(converged_solutions)),
        "total_starts": int(n_starts),
        "best_vs_median_rss": round(float(best_vs_median), 4)
    },
    "residual_diagnostics": {
        zone: (round(v, 6) if v is not None else None)
        for zone, v in residual_means.items()
    },
    "lineshape_diagnosis": lineshape_note,
    "flags": flags,
    "summary": ' '.join(summary_parts)
}

print(f"FIT_RESULTS_JSON:{json.dumps(results)}")
