"""Fractal indicators: Hurst exponent, fractal dimension, self-similarity."""
from __future__ import annotations

import numpy as np
from scipy.stats import linregress


def hurst_exponent(series: np.ndarray, max_lag: int | None = None) -> float:
    """Compute Hurst exponent via R/S analysis.
    H > 0.5: trending (persistent), H < 0.5: mean-reverting, H ≈ 0.5: random walk.
    """
    n = len(series)
    if n < 20:
        return 0.5

    if max_lag is None:
        max_lag = min(n // 4, 100)

    lags = range(10, max_lag + 1)
    rs_values = []
    lag_values = []

    for lag in lags:
        rs_list = []
        for start in range(0, n - lag, lag):
            chunk = series[start : start + lag]
            if len(chunk) < lag:
                continue
            mean = np.mean(chunk)
            deviations = np.cumsum(chunk - mean)
            r = np.max(deviations) - np.min(deviations)
            s = np.std(chunk, ddof=1)
            if s > 0:
                rs_list.append(r / s)

        if rs_list:
            rs_values.append(np.mean(rs_list))
            lag_values.append(lag)

    if len(lag_values) < 3:
        return 0.5

    log_lags = np.log(lag_values)
    log_rs = np.log(rs_values)
    slope, _, _, _, _ = linregress(log_lags, log_rs)
    return float(np.clip(slope, 0, 1))


def rolling_hurst(close: np.ndarray, window: int = 100, step: int = 1) -> np.ndarray:
    """Compute rolling Hurst exponent."""
    n = len(close)
    result = np.full(n, 0.5)
    returns = np.diff(np.log(np.where(close == 0, 1e-10, close)))

    for i in range(window, n, step):
        h = hurst_exponent(returns[i - window : i])
        result[i] = h
        for j in range(1, min(step, n - i)):
            result[i + j] = h

    return result


def fractal_dimension_higuchi(series: np.ndarray, k_max: int = 10) -> float:
    """Higuchi fractal dimension of a time series."""
    n = len(series)
    if n < k_max * 2:
        return 1.5

    lk = []
    x = np.arange(1, k_max + 1)

    for k in range(1, k_max + 1):
        lm_list = []
        for m in range(1, k + 1):
            indices = np.arange(m - 1, n, k)
            if len(indices) < 2:
                continue
            vals = series[indices]
            length = np.sum(np.abs(np.diff(vals)))
            norm = (n - 1) / (k * ((n - m) // k) * k)
            lm_list.append(length * norm)

        if lm_list:
            lk.append(np.mean(lm_list))

    if len(lk) < 3:
        return 1.5

    log_k = np.log(1.0 / x[: len(lk)])
    log_lk = np.log(np.array(lk))
    slope, _, _, _, _ = linregress(log_k, log_lk)
    return float(np.clip(slope, 1.0, 2.0))


def self_similarity_score(close: np.ndarray, window: int, scales: list[int] | None = None) -> float:
    """Measure self-similarity across multiple time scales.
    Higher score = more fractal (self-similar) structure.
    """
    if scales is None:
        scales = [2, 3, 4, 6]

    if len(close) < window:
        return 0.0

    base = close[-window:]
    base_returns = np.diff(np.log(np.where(base == 0, 1e-10, base)))
    if np.std(base_returns) == 0:
        return 0.0

    similarities = []
    for scale in scales:
        scaled_window = window * scale
        if scaled_window > len(close):
            continue

        scaled = close[-scaled_window:]
        step = scale
        downsampled = scaled[::step][:window]

        if len(downsampled) != len(base):
            downsampled = np.interp(
                np.linspace(0, 1, window),
                np.linspace(0, 1, len(downsampled)),
                downsampled,
            )

        ds_returns = np.diff(np.log(np.where(downsampled == 0, 1e-10, downsampled)))

        if len(ds_returns) != len(base_returns):
            ds_returns = np.interp(
                np.linspace(0, 1, len(base_returns)),
                np.linspace(0, 1, len(ds_returns)),
                ds_returns,
            )

        std_ds = np.std(ds_returns)
        if std_ds == 0:
            continue

        corr = np.corrcoef(base_returns, ds_returns)[0, 1]
        if np.isfinite(corr):
            similarities.append(abs(corr))

    if not similarities:
        return 0.0
    return float(np.mean(similarities))


def volatility_ratio(close: np.ndarray, fast: int = 12, slow: int = 48) -> float:
    """Ratio of short-term to long-term volatility."""
    returns = np.diff(np.log(np.where(close == 0, 1e-10, close)))
    if len(returns) < slow:
        return 1.0
    fast_vol = np.std(returns[-fast:])
    slow_vol = np.std(returns[-slow:])
    if slow_vol == 0:
        return 1.0
    return float(fast_vol / slow_vol)
