#!/usr/bin/env python3
"""
linear_scaling.py — Keijzer Linear Scaling (2003)

Given expression g(x) and target y, solve for optimal scale and offset:
    f(x) = a·g(x) + b

Closed-form solution via least squares:
    a = cov(g, y) / var(g)
    b = mean(y) - a·mean(g)

This reduces the search space: GP only needs to find the SHAPE g(x),
not the exact scale and offset.
"""

from __future__ import annotations

import numpy as np
from typing import Tuple


def linear_scale(
    preds: np.ndarray,
    target: np.ndarray,
) -> Tuple[float, float, np.ndarray]:
    """Solve for optimal a, b in f(x) = a·g(x) + b.

    Args:
        preds: raw expression predictions g(x_i)
        target: target values y_i

    Returns:
        (a, b, scaled_preds) where scaled_preds = a·preds + b
    """
    # Handle degenerate cases
    if len(preds) < 2:
        return 1.0, 0.0, preds

    pred_std = np.std(preds)
    if pred_std < 1e-15:
        # Constant expression: best fit is mean(y)
        return 0.0, float(np.mean(target)), np.full_like(target, np.mean(target))

    # Least squares: a = cov(g,y)/var(g), b = mean(y) - a*mean(g)
    pred_mean = np.mean(preds)
    target_mean = np.mean(target)

    cov_gy = np.mean((preds - pred_mean) * (target - target_mean))
    var_g = np.mean((preds - pred_mean) ** 2)

    if var_g < 1e-15:
        return 0.0, target_mean, np.full_like(target, target_mean)

    a = cov_gy / var_g
    b = target_mean - a * pred_mean

    scaled = a * preds + b
    return float(a), float(b), scaled


def compute_r2(
    preds: np.ndarray,
    target: np.ndarray,
) -> float:
    """Compute R² (coefficient of determination).

    R² = 1 - SS_res / SS_tot
    """
    if len(target) < 2:
        return 0.0

    ss_res = np.sum((target - preds) ** 2)
    ss_tot = np.sum((target - np.mean(target)) ** 2)

    if ss_tot < 1e-15:
        return 1.0 if ss_res < 1e-15 else 0.0

    return float(1.0 - ss_res / ss_tot)


def compute_mse(
    preds: np.ndarray,
    target: np.ndarray,
) -> float:
    """Compute mean squared error."""
    return float(np.mean((target - preds) ** 2))
