*! _fbnardl_bootstrap — Bootstrap Cointegration Tests for FBNARDL
*! Based on Bertelli, Vacca & Zoia (2022) — Economic Modelling
*! Author: Dr. Merwan Roudane (merwanroudane920@gmail.com)

capture program drop _fbnardl_bootstrap
program define _fbnardl_bootstrap, rclass
    version 17

    // Syntax: _fbnardl_bootstrap D.depvar regvars, options
    syntax varlist [if] [in], ///
        depvar(string)        /// original depvar name
        decnames(string)      /// decomposed variable names (clean)
        ecmcoef(string)       /// ECM coefficient name (L.depvar)
        levelsvars(string)    /// all lagged level variable names (for Fov test)
        indepvars(string)     /// lagged independent variable names (for Find test)
        reps(integer)         /// number of bootstrap replications
        nobs(integer)         /// number of observations used
        [controls(string)]    /// control variable names (optional)

    // Parse the regression specification
    gettoken lhs regressors : varlist

    // =========================================================================
    // STEP 1: Fit unrestricted model, compute test statistics
    // =========================================================================
    qui regress `lhs' `regressors'

    // Store original test statistics
    // Fov: joint test on all lagged levels
    qui test `levelsvars'
    local Fov_orig = r(F)

    // t-test on lagged dependent
    local t_orig = _b[`ecmcoef'] / _se[`ecmcoef']

    // Find: test on lagged independents only
    qui test `indepvars'
    local Find_orig = r(F)

    // Store residuals and fitted values
    tempvar resid_unrestr
    qui predict double `resid_unrestr', residuals

    // Store coefficients for data generation
    tempname b_unrestr
    mat `b_unrestr' = e(b)
    local nobs_model = e(N)
    local nparams = e(df_m) + 1

    // =========================================================================
    // STEP 2-3: Re-estimate under each null, compute restricted residuals
    // =========================================================================

    // --- Fov null: drop all lagged levels ---
    local regressors_fov ""
    foreach v of local regressors {
        local is_level = 0
        foreach lv of local levelsvars {
            if "`v'" == "`lv'" local is_level = 1
        }
        if `is_level' == 0 {
            local regressors_fov "`regressors_fov' `v'"
        }
    }
    qui regress `lhs' `regressors_fov'
    tempvar resid_fov
    qui predict double `resid_fov', residuals

    // --- t null: drop only L.depvar ---
    local regressors_t ""
    foreach v of local regressors {
        if "`v'" != "`ecmcoef'" {
            local regressors_t "`regressors_t' `v'"
        }
    }
    qui regress `lhs' `regressors_t'
    tempvar resid_t
    qui predict double `resid_t', residuals

    // --- Find null: drop lagged independents ---
    local regressors_find ""
    foreach v of local regressors {
        local is_indep = 0
        foreach iv of local indepvars {
            if "`v'" == "`iv'" local is_indep = 1
        }
        if `is_indep' == 0 {
            local regressors_find "`regressors_find' `v'"
        }
    }
    qui regress `lhs' `regressors_find'
    tempvar resid_find
    qui predict double `resid_find', residuals

    // =========================================================================
    // STEP 4-6: Bootstrap via Mata
    // =========================================================================
    // Transfer data to Mata for speed
    // We use residual resampling with recentering

    // Prepare Mata matrices
    tempname bs_Fov bs_t bs_Find
    mata: _fbnardl_do_bootstrap("`resid_fov'", "`resid_t'", "`resid_find'", ///
        "`lhs'", "`regressors'", "`regressors_fov'", "`regressors_t'", "`regressors_find'", ///
        "`levelsvars'", "`ecmcoef'", "`indepvars'", ///
        `reps', "`bs_Fov'", "`bs_t'", "`bs_Find'")

    // =========================================================================
    // Compute critical values and p-values from bootstrap distributions
    // =========================================================================
    // Fov critical values (upper tail — reject if Fov > cv)
    mata: st_local("Fov_cv01", strofreal(_fbnardl_quantile("`bs_Fov'", 0.99)))
    mata: st_local("Fov_cv05", strofreal(_fbnardl_quantile("`bs_Fov'", 0.95)))
    mata: st_local("Fov_cv10", strofreal(_fbnardl_quantile("`bs_Fov'", 0.90)))

    // t critical values (lower tail — reject if t < cv, so use negative quantiles)
    mata: st_local("t_cv01", strofreal(_fbnardl_quantile("`bs_t'", 0.01)))
    mata: st_local("t_cv05", strofreal(_fbnardl_quantile("`bs_t'", 0.05)))
    mata: st_local("t_cv10", strofreal(_fbnardl_quantile("`bs_t'", 0.10)))

    // Find critical values (upper tail)
    mata: st_local("Find_cv01", strofreal(_fbnardl_quantile("`bs_Find'", 0.99)))
    mata: st_local("Find_cv05", strofreal(_fbnardl_quantile("`bs_Find'", 0.95)))
    mata: st_local("Find_cv10", strofreal(_fbnardl_quantile("`bs_Find'", 0.90)))

    // p-values
    mata: st_local("Fov_pval", strofreal(_fbnardl_pvalue_upper("`bs_Fov'", `Fov_orig')))
    mata: st_local("t_pval", strofreal(_fbnardl_pvalue_lower("`bs_t'", `t_orig')))
    mata: st_local("Find_pval", strofreal(_fbnardl_pvalue_upper("`bs_Find'", `Find_orig')))

    // Return results
    return scalar Fov_cv01 = `Fov_cv01'
    return scalar Fov_cv05 = `Fov_cv05'
    return scalar Fov_cv10 = `Fov_cv10'
    return scalar t_cv01 = `t_cv01'
    return scalar t_cv05 = `t_cv05'
    return scalar t_cv10 = `t_cv10'
    return scalar Find_cv01 = `Find_cv01'
    return scalar Find_cv05 = `Find_cv05'
    return scalar Find_cv10 = `Find_cv10'
    return scalar Fov_pval = `Fov_pval'
    return scalar t_pval = `t_pval'
    return scalar Find_pval = `Find_pval'
end


// =============================================================================
// MATA FUNCTIONS FOR BOOTSTRAP
// =============================================================================
mata:
mata set matastrict off

// Bootstrap main routine
void _fbnardl_do_bootstrap(
    string scalar resid_fov_name,
    string scalar resid_t_name,
    string scalar resid_find_name,
    string scalar lhs_name,
    string scalar regressors_name,
    string scalar regressors_fov_name,
    string scalar regressors_t_name,
    string scalar regressors_find_name,
    string scalar levelsvars_name,
    string scalar ecmcoef_name,
    string scalar indepvars_name,
    real scalar B,
    string scalar bs_Fov_matname,
    string scalar bs_t_matname,
    string scalar bs_Find_matname)
{
    // All variable declarations at function scope (Mata requirement)
    real colvector resid_fov, resid_t, resid_find
    real colvector y, y_orig
    real colvector bs_Fov_dist, bs_t_dist, bs_Find_dist
    real colvector idx_fov, idx_t, idx_find
    real colvector bs_resid_fov, bs_resid_t, bs_resid_find
    real colvector y_star, y_full
    real scalar n, b, rc, orig_n
    real scalar pct, last_pct
    real scalar b_ecm, se_ecm, coef_idx
    real scalar valid_fov, valid_t, valid_find

    // Get residuals
    resid_fov  = st_data(., resid_fov_name)
    resid_t    = st_data(., resid_t_name)
    resid_find = st_data(., resid_find_name)

    // Get original y values (full column including missings)
    y_orig = st_data(., lhs_name)
    orig_n = rows(y_orig)

    // Use common non-missing mask: obs must be non-missing in y AND all residuals
    // This ensures all vectors have identical length after selection
    real colvector valid_mask
    valid_mask = (y_orig :!= .) :& (resid_fov :!= .) :& (resid_t :!= .) :& (resid_find :!= .)

    resid_fov  = select(resid_fov, valid_mask)
    resid_t    = select(resid_t, valid_mask)
    resid_find = select(resid_find, valid_mask)
    y          = select(y_orig, valid_mask)

    n = rows(resid_fov)

    // Recenter residuals (Step 5a of Bertelli et al.)
    resid_fov  = resid_fov  :- mean(resid_fov)
    resid_t    = resid_t    :- mean(resid_t)
    resid_find = resid_find :- mean(resid_find)

    // Fitted values under each null (fitted = observed - residual)
    // Note: residuals may be shorter than y due to lags, so use tail of y

    // Initialize bootstrap distributions
    bs_Fov_dist  = J(B, 1, .)
    bs_t_dist    = J(B, 1, .)
    bs_Find_dist = J(B, 1, .)

    // Progress indicator
    printf("  [")
    last_pct = 0

    for (b = 1; b <= B; b++) {
        // Progress bar
        pct = floor(b / B * 50)
        if (pct > last_pct) {
            printf("=")
            displayflush()
            last_pct = pct
        }

        // Resample residuals with replacement
        idx_fov  = ceil(uniform(n, 1) :* n)
        idx_t    = ceil(uniform(n, 1) :* n)
        idx_find = ceil(uniform(n, 1) :* n)

        bs_resid_fov  = resid_fov[idx_fov]
        bs_resid_t    = resid_t[idx_t]
        bs_resid_find = resid_find[idx_find]

        // Recenter bootstrap residuals
        bs_resid_fov  = bs_resid_fov  :- mean(bs_resid_fov)
        bs_resid_t    = bs_resid_t    :- mean(bs_resid_t)
        bs_resid_find = bs_resid_find :- mean(bs_resid_find)

        // --- Fov bootstrap statistic ---
        // y* = fitted_fov + bootstrap_resid_fov
        y_star = (y :- resid_fov) :+ bs_resid_fov

        // Build full column vector with missings in the right places
        y_full = y_orig
        y_full[| orig_n - n + 1, 1 \ orig_n, 1 |] = y_star
        st_store(., lhs_name, y_full)

        rc = _stata("capture qui regress " + lhs_name + " " + regressors_name, 1)
        if (rc == 0) {
            rc = _stata("capture qui test " + levelsvars_name, 1)
            if (rc == 0) {
                bs_Fov_dist[b] = st_numscalar("r(F)")
            }
        }

        // --- t bootstrap statistic ---
        y_star = (y :- resid_t) :+ bs_resid_t
        y_full = y_orig
        y_full[| orig_n - n + 1, 1 \ orig_n, 1 |] = y_star
        st_store(., lhs_name, y_full)

        rc = _stata("capture qui regress " + lhs_name + " " + regressors_name, 1)
        if (rc == 0) {
            coef_idx = _fbnardl_find_coef_idx(ecmcoef_name)
            b_ecm = st_matrix("e(b)")[1, coef_idx]
            se_ecm = sqrt(diagonal(st_matrix("e(V)"))[coef_idx])
            if (se_ecm > 0) {
                bs_t_dist[b] = b_ecm / se_ecm
            }
        }

        // --- Find bootstrap statistic ---
        y_star = (y :- resid_find) :+ bs_resid_find
        y_full = y_orig
        y_full[| orig_n - n + 1, 1 \ orig_n, 1 |] = y_star
        st_store(., lhs_name, y_full)

        rc = _stata("capture qui regress " + lhs_name + " " + regressors_name, 1)
        if (rc == 0) {
            rc = _stata("capture qui test " + indepvars_name, 1)
            if (rc == 0) {
                bs_Find_dist[b] = st_numscalar("r(F)")
            }
        }

        // Restore original y
        st_store(., lhs_name, y_orig)
    }

    printf("]\n")

    // Remove missing bootstrap statistics
    bs_Fov_dist  = select(bs_Fov_dist,  bs_Fov_dist  :!= .)
    bs_t_dist    = select(bs_t_dist,    bs_t_dist    :!= .)
    bs_Find_dist = select(bs_Find_dist, bs_Find_dist :!= .)

    // Store as Stata matrices
    st_matrix(bs_Fov_matname,  bs_Fov_dist)
    st_matrix(bs_t_matname,    bs_t_dist)
    st_matrix(bs_Find_matname, bs_Find_dist)

    valid_fov = rows(bs_Fov_dist)
    valid_t = rows(bs_t_dist)
    valid_find = rows(bs_Find_dist)
    printf("  Valid bootstrap replications: Fov=%g, t=%g, Find=%g\n",
        valid_fov, valid_t, valid_find)
}


// Find coefficient index by name
real scalar _fbnardl_find_coef_idx(string scalar coefname)
{
    string rowvector names
    real scalar i

    names = st_matrixcolstripe("e(b)")[., 2]'
    for (i = 1; i <= cols(names); i++) {
        if (names[i] == coefname) return(i)
    }
    // If not found, try column 1 (equation name : coefname)
    names = st_matrixcolstripe("e(b)")[., 1]' + ":" + st_matrixcolstripe("e(b)")[., 2]'
    for (i = 1; i <= cols(names); i++) {
        if (names[i] == coefname) return(i)
    }
    return(1) // fallback
}


// Quantile function for bootstrap distribution
real scalar _fbnardl_quantile(string scalar matname, real scalar prob)
{
    real colvector v
    real scalar n, idx

    v = st_matrix(matname)
    if (cols(v) > rows(v)) v = v'  // ensure column vector
    v = sort(v, 1)
    n = rows(v)
    idx = ceil(n * prob)
    if (idx < 1) idx = 1
    if (idx > n) idx = n
    return(v[idx])
}


// P-value (upper tail) for F-tests
real scalar _fbnardl_pvalue_upper(string scalar matname, real scalar stat)
{
    real colvector v
    real scalar n

    v = st_matrix(matname)
    if (cols(v) > rows(v)) v = v'
    n = rows(v)
    return(sum(v :>= stat) / n)
}


// P-value (lower tail) for t-test
real scalar _fbnardl_pvalue_lower(string scalar matname, real scalar stat)
{
    real colvector v
    real scalar n

    v = st_matrix(matname)
    if (cols(v) > rows(v)) v = v'
    n = rows(v)
    return(sum(v :<= stat) / n)
}

end