# BINGO ARIMA: versão Python (statsforecast)
#
# Objetivo: descobrir a ordem (p, d, q) do ARIMA por análise descritiva,
# antes de revelar o gabarito.
#
# Fluxo da brincadeira:
#   1. Rodamos "Sorteio + simulação" para gerar uma série nova.
#   2. Fazemos os passos 1-5 (descritiva, testes, diferenciação) e propomos
#      uma ordem (p, d, q).
#   3. Comparamos com o stepwise e o search do AutoARIMA.
#   4. Rodamos o bloco "Gabarito" no final pra revelar o que foi sorteado.
#
# Equivalente em R: complementar/extras/scripts/bingo_arima.R

# %% imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from statsmodels.tsa.arima_process import ArmaProcess
from statsmodels.tsa.stattools import adfuller, kpss
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.stats.diagnostic import acorr_ljungbox

from statsforecast import StatsForecast
from statsforecast.models import AutoARIMA, ARIMA
from statsforecast.arima import arima_string

import warnings
warnings.filterwarnings("ignore")

# Para um sorteio reprodutível, fixe a seed (ex.: np.random.default_rng(42))
RNG = np.random.default_rng()

# %% 1. sorteio + simulação
# ARIMA(p, d, q) com p,q ∈ {0,1,2} e d ∈ {0,1} e coeficientes uniformes em [0.25, 0.45].
p_true = int(RNG.integers(0, 3))
d_true = int(RNG.integers(0, 2))
q_true = int(RNG.integers(0, 3))

ar_parm = RNG.uniform(0.25, 0.45, size=p_true) if p_true else np.array([])
ma_parm = RNG.uniform(0.25, 0.45, size=q_true) if q_true else np.array([])

# ArmaProcess usa convenção (1 - phi_1 z - ...) Y_t = (1 + theta_1 z + ...) eps_t
# então passamos ar coefs com sinal negativo.
proc = ArmaProcess(np.r_[1.0, -ar_parm], np.r_[1.0, ma_parm])
y_stat = proc.generate_sample(nsample=300, scale=1.0)

# Aplicar d diferenças usando a função inversa: integrais (cumsum d vezes) para gerar I(d)
y = y_stat.copy()
for _ in range(d_true):
    y = np.cumsum(y)

dates = pd.date_range("1995-05-01", periods=len(y), freq="MS")
dados = pd.DataFrame({"unique_id": "vendas", "ds": dates, "y": y})
serie = pd.Series(dados["y"].values, index=dados["ds"], name="vendas")

# %%
dados

# %% 2. helpers de visualização
def gg_tsdisplay(series, lag_max=30):
    s = pd.Series(series).dropna()
    ax = plt.figure(figsize=(10, 6)).subplots(3, 1)
    ax[0].plot(s.index, s.values)
    plot_acf(s, ax=ax[1], lags=lag_max)
    plot_pacf(s, ax=ax[2], lags=lag_max, method="ywm")
    

# %% PASSO 1: descritiva da série original
gg_tsdisplay(serie, title="Série original")

# PASSO 2: testes
# %% adf test
adfuller(serie)
# %% kpss test
kpss(serie.values)

# %% PASSO 3: 1ª e 2ª diferenças
dif1 = serie.diff()
gg_tsdisplay(dif1, title="1ª diferença", lag_max=30)

dif2 = dif1.diff()
gg_tsdisplay(dif2, title="2ª diferença", lag_max=30)


# %% Modelagem com statsforecast
# manual_consenso: ARIMA(p_chute, d_chute, q_chute)
p_chute, d_chute, q_chute = 0, 0, 0

sf = StatsForecast(
    models=[
        ARIMA(order=(p_chute, d_chute, q_chute), include_mean=True, alias="manual_consenso"),
        AutoARIMA(stepwise=True,  alias="stepwise", max_P=0, max_Q=0, max_D=0),
        AutoARIMA(stepwise=False, alias="search", max_P=0, max_Q=0, max_D=0),
    ],
    freq="MS",
    n_jobs=1,
)
sf.fit(df=dados)

# %% AICc por modelo (model_['arma'] = (p, q, P, Q, m, d, D))
fitted_models = {m.alias: m for m in sf.fitted_[0]}
for alias, m in fitted_models.items():
    info = m.model_
    aicc = info.get("aicc")
    arma = info.get("arma")
    ord_str = f"({arma[0]},{arma[5]},{arma[1]})" if arma is not None else "?"
    aicc_str = f"{aicc:.2f}" if isinstance(aicc, (int, float)) else "?"
    print(f"  {alias:18}  ordem = {ord_str:9}  AICc = {aicc_str}")

# %% Resíduos de um dos modelos
res_step = pd.Series(
    fitted_models["stepwise"].model_["residuals"],
    index=dados["ds"],
)
gg_tsdisplay(
    res_step,
    title="Resíduos · stepwise (deveriam parecer white noise)",
    lag_max=30,
)
# %% ljung-box test
acorr_ljungbox(res_step, lags=[10], return_df=True)


# %% GABARITO

print(f"Gabarito: ARIMA({p_true}, {d_true}, {q_true})")
print(f"ar_parm = {np.round(ar_parm, 3).tolist()}")
print(f"ma_parm = {np.round(ma_parm, 3).tolist()}")

# %%
