*! did_estimators.mata - Core estimation functions for difference-in-differences
*!
*! Implements point estimation for the standard DID estimator (under the parallel
*! trends assumption) and the sequential DID estimator (under the weaker parallel
*! trends-in-trends assumption). Also provides data structures and equivalence
*! confidence interval computation for assessing pre-treatment trends.

version 16.0

mata:
mata set matastrict on

// ----------------------------------------------------------------------------
// DATA STRUCTURES
// ----------------------------------------------------------------------------

/*---------------------------------------------------------------------------
 * struct did_data - Data container for DID estimation
 *
 * Stores outcome, treatment, and derived variables for difference-in-
 * differences estimation. Supports panel and repeated cross-sectional designs.
 *---------------------------------------------------------------------------*/
struct did_data {
    // Original variables
    real colvector outcome       // Y_it: outcome
    real colvector treatment     // D_it: treatment indicator (0/1)
    real colvector id_unit       // Unit identifier i
    real colvector id_time       // Time period t (normalized)
    real matrix    covariates    // X_it: covariates (optional)
    real colvector cluster_var   // Cluster identifier (optional)

    // Derived variables
    real colvector Gi            // G_i: treatment group (1) vs control (0)
    real colvector It            // I_t: post-treatment indicator (0/1)
    real colvector id_time_std   // Standardized time (0 = treatment period)
    real colvector outcome_delta // ΔY_it: first-differenced outcome

    // Metadata
    real scalar    N             // Number of observations
    real scalar    n_units       // Number of units
    real scalar    n_periods     // Number of time periods
    real scalar    treat_year    // Treatment period (standardized)
    real scalar    is_panel      // 1 = panel, 0 = repeated cross-section
}

/*---------------------------------------------------------------------------
 * struct did_option - Estimation options
 *
 * User-specified options for bootstrap, variance estimation, and display.
 *---------------------------------------------------------------------------*/
struct did_option {
    real scalar    n_boot        // Bootstrap replications
    real scalar    parallel      // 1 = parallel computing enabled
    real scalar    se_boot       // 1 = bootstrap CI, 0 = analytical
    string scalar  id_cluster    // Cluster variable name
    real rowvector lead          // Post-treatment period indices
    real scalar    thres         // Staggered adoption threshold
    real rowvector lag           // Pre-treatment period indices
    real scalar    level         // Confidence level (percent)
    real scalar    seed          // Random seed (. if unset)
    string scalar  var_cluster_pre  // Internal: original cluster variable
    real scalar    quiet         // 1 = suppress progress display
    real scalar    kmax          // Max number of K-DID components (default: 2)
    real scalar    jtest_on      // 1 = enable J-test moment selection
}

// ----------------------------------------------------------------------------
// INITIALIZATION FUNCTIONS
// ----------------------------------------------------------------------------

/*---------------------------------------------------------------------------
 * init_did_option() - Initialize did_option with default values
 *
 * Returns:
 *   struct did_option: initialized with default values
 *---------------------------------------------------------------------------*/
struct did_option scalar init_did_option()
{
    struct did_option scalar opt

    opt.n_boot         = 30
    opt.parallel       = 1
    opt.se_boot        = 0
    opt.id_cluster     = ""
    opt.lead           = 0
    opt.thres          = 2
    opt.lag            = 1
    opt.level          = 95
    opt.seed           = .
    opt.var_cluster_pre = ""
    opt.quiet          = 0
    opt.kmax           = 2
    opt.jtest_on       = 0

    return(opt)
}

// ----------------------------------------------------------------------------
// OLS HELPER FUNCTION
// ----------------------------------------------------------------------------

/*---------------------------------------------------------------------------
 * ols_coef() - Extract OLS coefficient via normal equations
 *
 * Computes β = (X'X)⁻¹X'y and returns the coefficient at specified index.
 *
 * Arguments:
 *   X        : real matrix (n × p), design matrix
 *   y        : real colvector (n × 1), outcome
 *   coef_idx : real scalar, coefficient index (1-based)
 *
 * Returns:
 *   real scalar: coefficient value, or missing if singular/invalid
 *---------------------------------------------------------------------------*/
real scalar ols_coef(real matrix X, real colvector y, real scalar coef_idx)
{
    real matrix X_valid, X_basis
    real colvector beta, valid_idx, coef_pos
    real rowvector basis_idx, other_basis_idx, trial_idx
    real scalar n, p, n_valid, current_rank, j

    n = rows(X)
    p = cols(X)

    // Check minimum observations
    if (n < p) {
        return(.)
    }

    // Listwise deletion for missing values
    valid_idx = selectindex(rowmissing(X) :== 0 :& y :< .)
    n_valid = length(valid_idx)

    if (n_valid == 0) {
        return(.)
    }
    X_valid = X[valid_idx, .]

    // Validate coefficient index
    if (coef_idx < 1 | coef_idx > p) {
        return(.)
    }

    // Build a full-rank basis for all non-target columns.
    other_basis_idx = J(1, 0, .)
    current_rank = 0
    for (j = 1; j <= p; j++) {
        if (j == coef_idx) {
            continue
        }
        if (length(other_basis_idx) == 0) {
            trial_idx = j
        }
        else {
            trial_idx = other_basis_idx, j
        }
        if (rank(X_valid[., trial_idx]) > current_rank) {
            other_basis_idx = trial_idx
            current_rank = rank(X_valid[., other_basis_idx])
        }
    }

    if (length(other_basis_idx) == 0) {
        basis_idx = coef_idx
    }
    else {
        basis_idx = other_basis_idx, coef_idx
    }

    // The target coefficient is not identified if it adds no independent variation.
    if (rank(X_valid[., basis_idx]) == current_rank) {
        return(.)
    }

    basis_idx = sort(basis_idx, 1)
    X_basis = X_valid[., basis_idx]

    if (n_valid < cols(X_basis)) {
        return(.)
    }

    beta = qrsolve(X_basis, y[valid_idx])
    if (rows(beta) != cols(X_basis)) {
        return(.)
    }
    if (any(beta :>= .)) {
        return(.)
    }

    coef_pos = selectindex(basis_idx' :== coef_idx)
    if (length(coef_pos) != 1) {
        return(.)
    }

    return(beta[coef_pos[1]])
}

/*---------------------------------------------------------------------------
 * _sdid_outcome_by_lead() - Lead-aware transformed outcome for k = 2 DID
 *
 * For lead = 0, the sequential DID estimator uses the original transformed
 * outcome_delta = Y_t - E[Y_{g,t-1}]. For lead > 0, Appendix E defines
 * tau_2(s) around the target post-treatment time T* + s:
 *
 *   [Y_{T*+s} - Y_{T*-1}] - (s+1)[Y_{T*-1} - Y_{T*-2}]
 *
 * This helper encodes the same estimand inside the 2x2 regression skeleton by
 * keeping post-period observations unchanged and replacing the t = -1 outcome
 * with (s+2)Y_{-1} - (s+1)E[Y_{g,-2}].
 *---------------------------------------------------------------------------*/
real colvector _sdid_outcome_by_lead(real colvector Y,
                                     real colvector Y_delta,
                                     real colvector Gi,
                                     real colvector time_std,
                                     real colvector id_unit,
                                     real colvector support_mask,
                                     real scalar lead)
{
    real colvector result, idx_pre1, idx_post, idx_pre1_valid, idx_target
    real colvector valid_units, idx_pre2, idx_pre2_valid
    real scalar g, mean_pre2, mean_lag, i, iter, target_ts, lag_ts
    transmorphic scalar unit_set

    result = J(rows(Y), 1, .)

    for (g = 0; g <= 1; g++) {
        idx_pre1_valid = selectindex((Gi :== g) :& (time_std :== -1) :& (support_mask :> 0) :& (Y :< .))
        if (length(idx_pre1_valid) == 0) {
            continue
        }

        valid_units = uniqrows(id_unit[idx_pre1_valid])
        unit_set = asarray_create("real", 1)
        for (i = 1; i <= rows(valid_units); i++) {
            asarray(unit_set, valid_units[i], 1)
        }

        if (lead <= 0) {
            for (target_ts = -1; target_ts <= 0; target_ts++) {
                idx_target = selectindex((Gi :== g) :& (time_std :== target_ts) :& (support_mask :> 0) :& (Y :< .))
                if (length(idx_target) == 0) {
                    continue
                }

                lag_ts = target_ts - 1
                idx_pre2 = selectindex((Gi :== g) :& (time_std :== lag_ts) :& (Y :< .))
                if (length(idx_pre2) == 0) {
                    continue
                }

                idx_pre2_valid = J(length(idx_pre2), 1, .)
                iter = 1
                for (i = 1; i <= length(idx_pre2); i++) {
                    if (asarray_contains(unit_set, id_unit[idx_pre2[i]])) {
                        idx_pre2_valid[iter] = idx_pre2[i]
                        iter++
                    }
                }

                mean_lag = .
                if (iter > 1) {
                    idx_pre2_valid = idx_pre2_valid[1::(iter - 1)]
                    if (all(Y[idx_pre2_valid] :< .)) {
                        mean_lag = mean(Y[idx_pre2_valid])
                    }
                }

                if (missing(mean_lag)) {
                    continue
                }

                result[idx_target] = Y[idx_target] :- mean_lag
            }

            continue
        }

        idx_post = selectindex((Gi :== g) :& (time_std :== lead) :& (support_mask :> 0) :& (Y :< .))
        if (length(idx_post) > 0) {
            result[idx_post] = Y[idx_post]
        }

        idx_pre2 = selectindex((Gi :== g) :& (time_std :== -2) :& (Y :< .))
        if (length(idx_pre2) == 0) {
            continue
        }

        idx_pre2_valid = J(length(idx_pre2), 1, .)
        iter = 1
        for (i = 1; i <= length(idx_pre2); i++) {
            if (asarray_contains(unit_set, id_unit[idx_pre2[i]])) {
                idx_pre2_valid[iter] = idx_pre2[i]
                iter++
            }
        }

        mean_pre2 = .
        if (iter > 1) {
            idx_pre2_valid = idx_pre2_valid[1::(iter - 1)]
            if (all(Y[idx_pre2_valid] :< .)) {
                mean_pre2 = mean(Y[idx_pre2_valid])
            }
        }

        if (missing(mean_pre2)) {
            continue
        }

        idx_pre1 = selectindex((Gi :== g) :& (time_std :== -1) :& (support_mask :> 0) :& (Y :< .))
        if (length(idx_pre1) > 0) {
            result[idx_pre1] = (lead + 2) :* Y[idx_pre1] :- (lead + 1) * mean_pre2
        }
    }

    return(result)
}


// ----------------------------------------------------------------------------
// MAIN DID ESTIMATION FUNCTION
// ----------------------------------------------------------------------------

/*---------------------------------------------------------------------------
 * did_fit() - Compute DID and sequential DID point estimates
 *
 * The standard DID estimator τ̂_DID is consistent under the parallel trends
 * assumption. The sequential DID estimator τ̂_sDID requires only the weaker
 * parallel trends-in-trends assumption for lead = 0 and uses the Appendix E
 * lead-aware k = 2 transformation when lead > 0.
 *
 * Arguments:
 *   Y        : real colvector, outcome Y_it
 *   Y_delta  : real colvector, baseline transformed outcome for lead = 0
 *   Gi       : real colvector, group indicator G_i (1 = treated)
 *   It       : real colvector, post-treatment indicator I_t
 *   id_unit   : real colvector, unit identifiers used for lag joins
 *   X        : real matrix, covariates (or empty)
 *   time_std : real colvector, standardized time index
 *   lead     : real scalar, post-treatment period (0 = treatment year)
 *
 * Returns:
 *   real rowvector (1 × 2): (τ̂_DID, τ̂_sDID)
 *
 * Regression models:
 *   DID:  Y_it = α + β₁G_i + β₂I_t + τG_i×I_t + X'γ + ε
 *   sDID (lead = 0):  ΔY_it = α + β₁G_i + β₂I_t + τG_i×I_t + X'γ + ε
 *   sDID (lead > 0): lead-aware Appendix E k = 2 transform on {-1, lead}
 *---------------------------------------------------------------------------*/
real rowvector did_fit(real colvector Y, real colvector Y_delta,
                       real colvector Gi, real colvector It,
                       real colvector id_unit, real matrix X,
                       real colvector time_std, real scalar lead,
                       | real scalar is_panel)
{
    real colvector idx, idx_did, idx_sdid, reg_valid, support_mask_full
    real colvector Y_sub, Yd_sub, Yd_lead, Gi_sub, It_sub
    real colvector Gi_did, It_did, Gi_sdid, It_sdid
    real colvector valid_did, valid_sdid, cov_valid, gi_it_valid
    real colvector id_unit_sub, cov_unit_complete, cov_counts, cov_unit_idx
    real colvector support_units, support_unit_idx
    real matrix X_sub, X_did, X_sdid, design_did, design_sdid
    real rowvector result
    real rowvector distinct_times
    real scalar tau_did, tau_sdid, n_sub, k_cov, i, required_periods, _is_panel
    transmorphic scalar cov_count_map

    // -------------------------------------------------------------------------
    // Input validation
    // -------------------------------------------------------------------------
    _is_panel = (args() >= 9 ? is_panel : 1)

    if (lead < 0) {
        errprintf("{err}Error: lead must be >= 0 (got %g)\n", lead)
        return((., .))
    }

    // Subset data to relevant time periods: t ∈ {-1, lead}
    idx = selectindex((time_std :== -1) :| (time_std :== lead))

    // Return missing if no observations in specified periods
    if (length(idx) == 0) {
        return((., .))
    }

    // Extract observations for the selected time periods
    Y_sub = Y[idx]
    Gi_sub = Gi[idx]
    It_sub = It[idx]
    id_unit_sub = id_unit[idx]
    n_sub = length(idx)

    // Handle covariates
    k_cov = cols(X)
    reg_valid = (Y :< .) :& (Gi :< .) :& (It :< .)
    if (k_cov > 0) {
        X_sub = X[idx, .]
        reg_valid = reg_valid :& (rowmissing(X) :== 0)
    }
    // Listwise deletion for missing values
    valid_did = (Y_sub :< .)

    // Exclude observations with missing covariates
    if (k_cov > 0) {
        cov_valid = (rowmissing(X_sub) :== 0)
        distinct_times = uniqrows(time_std[idx])
        required_periods = rows(distinct_times)

        cov_counts = J(n_sub, 1, 0)
        cov_count_map = asarray_create("real", 1)
        cov_unit_idx = selectindex(cov_valid)
        for (i = 1; i <= length(cov_unit_idx); i++) {
            if (asarray_contains(cov_count_map, id_unit_sub[cov_unit_idx[i]])) {
                asarray(
                    cov_count_map,
                    id_unit_sub[cov_unit_idx[i]],
                    asarray(cov_count_map, id_unit_sub[cov_unit_idx[i]]) + 1
                )
            }
            else {
                asarray(cov_count_map, id_unit_sub[cov_unit_idx[i]], 1)
            }
        }

        for (i = 1; i <= n_sub; i++) {
            if (asarray_contains(cov_count_map, id_unit_sub[i])) {
                cov_counts[i] = asarray(cov_count_map, id_unit_sub[i])
            }
        }

        if (_is_panel) {
            cov_unit_complete = cov_valid :& (cov_counts :== required_periods)
        }
        else {
            cov_unit_complete = cov_valid
        }
        valid_did = valid_did :& cov_unit_complete
    }

    // Exclude observations with missing group or time indicators
    gi_it_valid = (Gi_sub :< .) :& (It_sub :< .)
    valid_did = valid_did :& gi_it_valid

    support_mask_full = reg_valid
    if (k_cov > 0) {
        if (_is_panel) {
            support_units = uniqrows(id_unit_sub[selectindex(cov_unit_complete :& gi_it_valid)])
            support_mask_full = J(rows(Y), 1, 0)
            if (rows(support_units) > 0) {
                for (i = 1; i <= rows(Y); i++) {
                    if (anyof(support_units, id_unit[i])) {
                        support_mask_full[i] = 1
                    }
                }
            }
        }
        else {
            support_mask_full = reg_valid :& (rowmissing(X) :== 0)
        }
    }

    Yd_lead = _sdid_outcome_by_lead(Y, Y_delta, Gi, time_std, id_unit, support_mask_full, lead)
    Yd_sub = Yd_lead[idx]
    valid_sdid = (Yd_sub :< .)
    valid_sdid = valid_sdid :& gi_it_valid

    // Obtain indices of valid observations
    idx_did = selectindex(valid_did)
    idx_sdid = selectindex(valid_sdid)

    // Standard DID estimation (requires at least 4 observations)
    if (length(idx_did) < 4) {
        tau_did = .
    }
    else {
        Gi_did = Gi_sub[idx_did]
        It_did = It_sub[idx_did]

        // Check for sufficient variation
        if (min(Gi_did) == max(Gi_did)) {
            tau_did = .
        }
        else if (min(It_did) == max(It_did)) {
            tau_did = .
        }
        else if (sum((Gi_did :== 0) :& (It_did :== 0)) == 0 |
                 sum((Gi_did :== 0) :& (It_did :== 1)) == 0 |
                 sum((Gi_did :== 1) :& (It_did :== 0)) == 0 |
                 sum((Gi_did :== 1) :& (It_did :== 1)) == 0) {
            // DID requires support in every 2x2 group-period cell.
            tau_did = .
        }
        else {
            // Construct design matrix: [1, G_i, I_t, G_i×I_t, X]
            design_did = J(length(idx_did), 1, 1), Gi_did, It_did, Gi_did :* It_did

            if (k_cov > 0) {
                X_did = X_sub[idx_did, .]
                design_did = design_did, X_did
            }

            // Extract coefficient on interaction term (position 4)
            tau_did = ols_coef(design_did, Y_sub[idx_did], 4)
        }
    }

    // Sequential DID estimation
    if (length(idx_sdid) < 4) {
        tau_sdid = .
    }
    else {
        Gi_sdid = Gi_sub[idx_sdid]
        It_sdid = It_sub[idx_sdid]

        // Check for sufficient variation
        if (min(Gi_sdid) == max(Gi_sdid)) {
            tau_sdid = .
        }
        else if (min(It_sdid) == max(It_sdid)) {
            tau_sdid = .
        }
        else if (sum((Gi_sdid :== 0) :& (It_sdid :== 0)) == 0 |
                 sum((Gi_sdid :== 0) :& (It_sdid :== 1)) == 0 |
                 sum((Gi_sdid :== 1) :& (It_sdid :== 0)) == 0 |
                 sum((Gi_sdid :== 1) :& (It_sdid :== 1)) == 0) {
            // Sequential DID is unidentified if any 2x2 cell disappears
            // after outcome_delta listwise deletion.
            tau_sdid = .
        }
        else {
            // Construct design matrix: [1, G_i, I_t, G_i×I_t, X]
            design_sdid = J(length(idx_sdid), 1, 1), Gi_sdid, It_sdid, Gi_sdid :* It_sdid

            if (k_cov > 0) {
                X_sdid = X_sub[idx_sdid, .]
                design_sdid = design_sdid, X_sdid
            }

            // Extract coefficient on interaction term (position 4)
            tau_sdid = ols_coef(design_sdid, Yd_sub[idx_sdid], 4)
        }
    }

    // Return point estimates
    result = (tau_did, tau_sdid)
    return(result)
}

real rowvector did_fit_treated_support(real colvector Y, real colvector Y_delta,
                                       real colvector Gi, real colvector It,
                                       real colvector id_unit, real matrix X,
                                       real colvector time_std, real scalar lead,
                                       | real scalar is_panel)
{
    real colvector idx, Y_sub, Yd_sub, Gi_sub, It_sub, id_unit_sub, support_mask_full
    real colvector valid_did, valid_sdid, cov_valid, gi_it_valid
    real colvector reg_valid, cov_unit_complete, cov_counts, cov_unit_idx
    real colvector treated_idx_did, treated_idx_sdid, treated_idx_common, support_units
    real matrix X_sub
    real rowvector distinct_times, result
    real scalar n_sub, k_cov, i, required_periods, _is_panel
    transmorphic scalar cov_count_map

    _is_panel = (args() >= 9 ? is_panel : 1)

    idx = selectindex((time_std :== -1) :| (time_std :== lead))
    if (length(idx) == 0) {
        return((0, 0, 0))
    }

    Y_sub = Y[idx]
    Gi_sub = Gi[idx]
    It_sub = It[idx]
    id_unit_sub = id_unit[idx]
    n_sub = length(idx)
    k_cov = cols(X)

    reg_valid = (Y :< .) :& (Gi :< .) :& (It :< .)
    if (k_cov > 0) {
        X_sub = X[idx, .]
        reg_valid = reg_valid :& (rowmissing(X) :== 0)
    }
    else {
        X_sub = J(n_sub, 0, .)
    }

    valid_did = (Y_sub :< .)

    if (k_cov > 0) {
        cov_valid = (rowmissing(X_sub) :== 0)
        distinct_times = uniqrows(time_std[idx])
        required_periods = rows(distinct_times)
        cov_counts = J(n_sub, 1, 0)
        cov_count_map = asarray_create("real", 1)
        cov_unit_idx = selectindex(cov_valid)

        for (i = 1; i <= length(cov_unit_idx); i++) {
            if (asarray_contains(cov_count_map, id_unit_sub[cov_unit_idx[i]])) {
                asarray(
                    cov_count_map,
                    id_unit_sub[cov_unit_idx[i]],
                    asarray(cov_count_map, id_unit_sub[cov_unit_idx[i]]) + 1
                )
            }
            else {
                asarray(cov_count_map, id_unit_sub[cov_unit_idx[i]], 1)
            }
        }

        for (i = 1; i <= n_sub; i++) {
            if (asarray_contains(cov_count_map, id_unit_sub[i])) {
                cov_counts[i] = asarray(cov_count_map, id_unit_sub[i])
            }
        }

        if (_is_panel) {
            cov_unit_complete = cov_valid :& (cov_counts :== required_periods)
        }
        else {
            cov_unit_complete = cov_valid
        }
        valid_did = valid_did :& cov_unit_complete
    }

    gi_it_valid = (Gi_sub :< .) :& (It_sub :< .)
    valid_did = valid_did :& gi_it_valid

    support_mask_full = reg_valid
    if (k_cov > 0) {
        if (_is_panel) {
            support_units = uniqrows(id_unit_sub[selectindex(cov_unit_complete :& gi_it_valid)])
            support_mask_full = J(rows(Y), 1, 0)
            if (rows(support_units) > 0) {
                for (i = 1; i <= rows(Y); i++) {
                    if (anyof(support_units, id_unit[i])) {
                        support_mask_full[i] = 1
                    }
                }
            }
        }
        else {
            support_mask_full = reg_valid :& (rowmissing(X) :== 0)
        }
    }

    Yd_sub = _sdid_outcome_by_lead(Y, Y_delta, Gi, time_std, id_unit, support_mask_full, lead)[idx]
    valid_sdid = (Yd_sub :< .)
    valid_sdid = valid_sdid :& gi_it_valid

    treated_idx_did = selectindex(valid_did :& (Gi_sub :== 1))
    treated_idx_sdid = selectindex(valid_sdid :& (Gi_sub :== 1))
    treated_idx_common = selectindex(valid_did :& valid_sdid :& (Gi_sub :== 1))

    result = (
        rows(uniqrows(id_unit_sub[treated_idx_did])),
        rows(uniqrows(id_unit_sub[treated_idx_sdid])),
        rows(uniqrows(id_unit_sub[treated_idx_common]))
    )

    return(result)
}


/*---------------------------------------------------------------------------
 * did_fit_k() - Compute K-dimensional component point estimates
 *
 * Returns τ̂_1(s), τ̂_2(s), ..., τ̂_K(s) where K = min(kmax, K_init(s)).
 * Each τ̂_k(s) is computed via the k-th order transformed outcome DID
 * regression on {-1, lead}.
 *
 * k=1: standard DID (parallel trends)
 * k=2: sequential DID (parallel trends-in-trends)
 * k≥3: higher-order DID ((k-1)-th degree polynomial confounding)
 *
 * Arguments:
 *   Y        : real colvector, outcome Y_it
 *   Gi       : real colvector, group indicator G_i (1 = treated)
 *   It       : real colvector, post-treatment indicator I_t
 *   id_unit  : real colvector, unit identifiers
 *   X        : real matrix, covariates (or empty)
 *   time_std : real colvector, standardized time index
 *   lead     : real scalar, post-treatment period (0 = treatment year)
 *   kmax     : real scalar, maximum number of components
 *   is_panel : real scalar, 1 = panel, 0 = RCS
 *
 * Returns:
 *   real rowvector (1 × kmax): (τ̂_1, τ̂_2, ..., τ̂_kmax)
 *     Components beyond K_init are set to missing.
 *---------------------------------------------------------------------------*/
real rowvector did_fit_k(real colvector Y,
                         real colvector Gi, real colvector It,
                         real colvector id_unit, real matrix X,
                         real colvector time_std, real scalar lead,
                         real scalar kmax,
                         | real scalar is_panel)
{
    real rowvector result, point_est_k1
    real colvector idx, Y_sub, Gi_sub, It_sub, id_unit_sub
    real colvector Yd_k, Yd_k_sub
    real colvector valid_k, gi_it_valid, cov_valid, cov_unit_complete, cov_counts, cov_unit_idx
    real colvector idx_k, Gi_k, It_k, reg_valid, support_mask_full, support_units
    real matrix X_sub, X_k, design_k
    real rowvector distinct_times
    real scalar _is_panel, k_comp, n_sub, k_cov, i, required_periods, tau_k
    transmorphic scalar cov_count_map

    _is_panel = (args() >= 9 ? is_panel : 1)

    result = J(1, kmax, .)

    // Subset data to relevant time periods: t ∈ {-1, lead}
    idx = selectindex((time_std :== -1) :| (time_std :== lead))
    if (length(idx) == 0) {
        return(result)
    }

    Y_sub = Y[idx]
    Gi_sub = Gi[idx]
    It_sub = It[idx]
    id_unit_sub = id_unit[idx]
    n_sub = length(idx)
    k_cov = cols(X)

    // Build validity masks (same logic as did_fit)
    reg_valid = (Y :< .) :& (Gi :< .) :& (It :< .)
    if (k_cov > 0) {
        X_sub = X[idx, .]
        reg_valid = reg_valid :& (rowmissing(X) :== 0)
    }

    valid_k = (Y_sub :< .)
    if (k_cov > 0) {
        cov_valid = (rowmissing(X_sub) :== 0)
        distinct_times = uniqrows(time_std[idx])
        required_periods = rows(distinct_times)

        cov_counts = J(n_sub, 1, 0)
        cov_count_map = asarray_create("real", 1)
        cov_unit_idx = selectindex(cov_valid)
        for (i = 1; i <= length(cov_unit_idx); i++) {
            if (asarray_contains(cov_count_map, id_unit_sub[cov_unit_idx[i]])) {
                asarray(cov_count_map, id_unit_sub[cov_unit_idx[i]],
                    asarray(cov_count_map, id_unit_sub[cov_unit_idx[i]]) + 1)
            }
            else {
                asarray(cov_count_map, id_unit_sub[cov_unit_idx[i]], 1)
            }
        }
        for (i = 1; i <= n_sub; i++) {
            if (asarray_contains(cov_count_map, id_unit_sub[i])) {
                cov_counts[i] = asarray(cov_count_map, id_unit_sub[i])
            }
        }
        if (_is_panel) {
            cov_unit_complete = cov_valid :& (cov_counts :== required_periods)
        }
        else {
            cov_unit_complete = cov_valid
        }
        valid_k = valid_k :& cov_unit_complete
    }

    gi_it_valid = (Gi_sub :< .) :& (It_sub :< .)
    valid_k = valid_k :& gi_it_valid

    // Build support mask for full data (needed by _kdid_outcome_by_lead)
    support_mask_full = reg_valid
    if (k_cov > 0) {
        if (_is_panel) {
            support_units = uniqrows(id_unit_sub[selectindex(valid_k)])
            support_mask_full = J(rows(Y), 1, 0)
            if (rows(support_units) > 0) {
                for (i = 1; i <= rows(Y); i++) {
                    if (anyof(support_units, id_unit[i])) {
                        support_mask_full[i] = 1
                    }
                }
            }
        }
        else {
            support_mask_full = reg_valid :& (rowmissing(X) :== 0)
        }
    }

    // Loop over k = 1..kmax
    for (k_comp = 1; k_comp <= kmax; k_comp++) {

        // Compute k-th order transformed outcome
        Yd_k = _kdid_outcome_by_lead(Y, Gi, time_std, id_unit, support_mask_full, lead, k_comp)
        Yd_k_sub = Yd_k[idx]

        // Build validity mask for this component
        valid_k = (Yd_k_sub :< .) :& gi_it_valid
        if (k_cov > 0) {
            if (_is_panel) {
                valid_k = valid_k :& cov_unit_complete
            }
            else {
                valid_k = valid_k :& (rowmissing(X_sub) :== 0)
            }
        }

        idx_k = selectindex(valid_k)

        if (length(idx_k) < 4) {
            // Not enough obs; this and higher k are infeasible
            break
        }

        Gi_k = Gi_sub[idx_k]
        It_k = It_sub[idx_k]

        // Check sufficient variation in all 2×2 cells
        if (min(Gi_k) == max(Gi_k) || min(It_k) == max(It_k)) {
            break
        }
        if (sum((Gi_k :== 0) :& (It_k :== 0)) == 0 |
            sum((Gi_k :== 0) :& (It_k :== 1)) == 0 |
            sum((Gi_k :== 1) :& (It_k :== 0)) == 0 |
            sum((Gi_k :== 1) :& (It_k :== 1)) == 0) {
            break
        }

        // Construct design matrix: [1, G_i, I_t, G_i×I_t, X]
        design_k = J(length(idx_k), 1, 1), Gi_k, It_k, Gi_k :* It_k
        if (k_cov > 0) {
            X_k = X_sub[idx_k, .]
            design_k = design_k, X_k
        }

        // Extract coefficient on interaction term (position 4)
        tau_k = ols_coef(design_k, Yd_k_sub[idx_k], 4)
        result[k_comp] = tau_k
    }

    return(result)
}


/*---------------------------------------------------------------------------
 * did_fit_struct() - Wrapper for did_fit() using did_data structure
 *
 * Arguments:
 *   data : struct did_data, prepared data
 *   lead : real scalar, post-treatment period (default: 0)
 *
 * Returns:
 *   real rowvector (1 × 2): (τ̂_DID, τ̂_sDID)
 *---------------------------------------------------------------------------*/
real rowvector did_fit_struct(struct did_data scalar data, | real scalar lead)
{
    // Default lead = 0
    if (args() < 2) lead = 0

    // Extract data from structure and call did_fit()
    return(did_fit(
        data.outcome,
        data.outcome_delta,
        data.Gi,
        data.It,
        data.id_unit,
        data.covariates,
        data.id_time_std,
        lead,
        data.is_panel
    ))
}

// ----------------------------------------------------------------------------
// OPTION POPULATION FUNCTION
// ----------------------------------------------------------------------------

/*---------------------------------------------------------------------------
 * _diddesign_populate_option() - Populate global did_option structure
 *
 * Internal function called from _diddesign_parse.ado to transfer parsed
 * command-line options to the global did_opt structure.
 *
 * Returns:
 *   0 on success
 *---------------------------------------------------------------------------*/
real scalar _diddesign_populate_option(
    real scalar n_boot,
    real scalar parallel,
    real scalar se_boot,
    string scalar id_cluster,
    real rowvector lead,
    real scalar thres,
    real rowvector lag,
    real scalar level,
    real scalar seed
)
{
    external struct did_option scalar did_opt

    did_opt = init_did_option()
    did_opt.n_boot         = n_boot
    did_opt.parallel       = parallel
    did_opt.se_boot        = se_boot
    did_opt.id_cluster     = id_cluster
    did_opt.lead           = lead
    did_opt.thres          = thres
    did_opt.lag            = lag
    did_opt.level          = level
    did_opt.seed           = seed
    did_opt.var_cluster_pre = ""

    return(0)
}

// ----------------------------------------------------------------------------
// DATA POPULATION FUNCTION
// ----------------------------------------------------------------------------

/*---------------------------------------------------------------------------
 * _diddesign_populate_data() - Populate global did_data structure
 *
 * Internal function called from _diddesign_prep.ado to transfer prepared
 * data from Stata variables to the global did_dat structure.
 *
 * Returns:
 *   0 on success
 *---------------------------------------------------------------------------*/
real scalar _diddesign_populate_data(
    string scalar outcome_var,
    string scalar treatment_var,
    string scalar id_var,
    string scalar id_time_var,
    string scalar covar_vars,
    string scalar cluster_var,
    string scalar Gi_var,
    string scalar It_var,
    string scalar id_time_std_var,
    string scalar outcome_delta_var,
    real scalar N,
    real scalar n_units,
    real scalar n_periods,
    real scalar treat_year,
    real scalar is_panel,
    string scalar touse_var
)
{
    external struct did_data scalar did_dat
    string rowvector covar_list
    real scalar k

    did_dat = did_data()

    // Original variables
    did_dat.outcome = st_data(., outcome_var, touse_var)
    did_dat.treatment = st_data(., treatment_var, touse_var)

    if (id_var != "") {
        did_dat.id_unit = st_data(., id_var, touse_var)
    }
    else {
        did_dat.id_unit = J(N, 1, .)
    }

    did_dat.id_time = st_data(., id_time_var, touse_var)

    // Covariates
    if (covar_vars != "") {
        covar_list = tokens(covar_vars)
        did_dat.covariates = st_data(., covar_list, touse_var)
    }
    else {
        did_dat.covariates = J(0, 0, .)
    }

    // Cluster variable
    if (cluster_var != "") {
        did_dat.cluster_var = st_data(., cluster_var, touse_var)
    }
    else {
        did_dat.cluster_var = J(0, 1, .)
    }

    // Derived variables
    did_dat.Gi = st_data(., Gi_var, touse_var)
    did_dat.It = st_data(., It_var, touse_var)
    did_dat.id_time_std = st_data(., id_time_std_var, touse_var)
    did_dat.outcome_delta = st_data(., outcome_delta_var, touse_var)

    // Metadata
    did_dat.N = N
    did_dat.n_units = n_units
    did_dat.n_periods = n_periods
    did_dat.treat_year = treat_year
    did_dat.is_panel = is_panel

    return(0)
}

// ----------------------------------------------------------------------------
// EQUIVALENCE CONFIDENCE INTERVAL FUNCTIONS
// ----------------------------------------------------------------------------

/*---------------------------------------------------------------------------
 * compute_eq_ci() - Compute 95% equivalence confidence interval
 *
 * Computes the equivalence CI for assessing parallel trends using the
 * Two One-Sided Tests (TOST) methodology.
 *
 * Arguments:
 *   estimate  : real scalar, point estimate (e.g., placebo DID)
 *   std_error : real scalar, standard error
 *
 * Returns:
 *   real rowvector (1 × 2): symmetric equivalence interval (-ν, ν)
 *
 * Method:
 *   1. Construct 90% CI: estimate ± z_{0.95} × SE
 *   2. Compute ν = max(|CI90_UB|, |CI90_LB|)
 *   3. Return (-ν, ν)
 *---------------------------------------------------------------------------*/
real rowvector compute_eq_ci(real scalar estimate, real scalar std_error)
{
    real scalar z_95, CI90_UB, CI90_LB, CI90_UB_ab, CI90_LB_ab, nu

    // Input validation: return missing for invalid inputs
    if (missing(estimate) | missing(std_error) | std_error <= 0) {
        return((., .))
    }

    // z critical value for 90% CI (corresponds to 95% equivalence test)
    // invnormal(0.95) ≈ 1.64485362695147
    z_95 = invnormal(0.95)

    // Compute 90% CI bounds
    CI90_UB = estimate + z_95 * std_error
    CI90_LB = estimate - z_95 * std_error

    // Take absolute values
    CI90_UB_ab = abs(CI90_UB)
    CI90_LB_ab = abs(CI90_LB)

    // Symmetric equivalence bound: max of absolute values
    nu = max((CI90_UB_ab, CI90_LB_ab))

    // Return symmetric equivalence CI: (-nu, nu)
    return((-nu, nu))
}


/*---------------------------------------------------------------------------
 * compute_eq_ci_vec() - Vectorized equivalence CI computation
 *
 * Batch computation of equivalence confidence intervals.
 *
 * Arguments:
 *   estimates  : real colvector (n × 1), point estimates
 *   std_errors : real colvector (n × 1), standard errors
 *
 * Returns:
 *   real matrix (n × 2): each row is (-ν, ν) equivalence interval
 *---------------------------------------------------------------------------*/
real matrix compute_eq_ci_vec(real colvector estimates, real colvector std_errors)
{
    real scalar n, z_95, i
    real colvector CI90_UB, CI90_LB, CI90_UB_ab, CI90_LB_ab, nu
    real colvector invalid_mask
    real matrix result

    n = rows(estimates)

    // Dimension check
    if (rows(std_errors) != n) {
        _error("estimates and std_errors must have the same number of rows")
    }

    if (n == 0) {
        return(J(0, 2, .))
    }

    // 90% CI corresponds to 95% equivalence test
    z_95 = invnormal(0.95)

    // Vectorized computation
    CI90_UB = estimates :+ z_95 :* std_errors
    CI90_LB = estimates :- z_95 :* std_errors
    CI90_UB_ab = abs(CI90_UB)
    CI90_LB_ab = abs(CI90_LB)
    nu = rowmax((CI90_UB_ab, CI90_LB_ab))

    // Mark invalid entries
    invalid_mask = (estimates :>= .) :| (std_errors :>= .) :| (std_errors :<= 0)

    // Set nu to missing for invalid rows
    for (i = 1; i <= n; i++) {
        if (invalid_mask[i]) {
            nu[i] = .
        }
    }

    // Build result matrix: (-nu, nu)
    result = (-nu, nu)

    return(result)
}


// ============================================================================
// GENERALIZED K-DID: MATHEMATICAL HELPER FUNCTIONS
// ============================================================================
// Implements the k-th order difference operator from Appendix E.2 of
// Egami & Yamauchi (2022). The closed-form expansion is:
//
//   Δ^k_s(Ȳ_{g,T*+s}) = Ȳ_{g,T*+s} - Ȳ_{g,T*-1}
//                        - Σ_{j=1}^{k-1} M^{j+1}_s · Δ^j(Ȳ_{g,T*-1})
//
// where M^ℓ_s = C(s+ℓ-1, ℓ-1) is the binomial coefficient, and Δ^j is the
// standard j-th order finite difference on pre-treatment outcomes.
//
// Each k-th component estimator τ̂_k(s) can be computed via a standard 2×2
// DID regression on a "transformed outcome" that absorbs the pre-treatment
// polynomial trend, preserving the existing regression infrastructure.
// ============================================================================

/*---------------------------------------------------------------------------
 * compute_M_coeff() - Compute M^ℓ_s coefficient from paper Assumption E.1
 *
 * M^ℓ_s = Π_{j=1}^{ℓ-1} (s+j) / Π_{j=1}^{ℓ-1} j = C(s+ℓ-1, ℓ-1)
 *
 * Arguments:
 *   ell : real scalar, order ℓ (≥ 2)
 *   s   : real scalar, lead (≥ 0)
 *
 * Returns:
 *   real scalar: M^ℓ_s value
 *---------------------------------------------------------------------------*/
real scalar compute_M_coeff(real scalar ell, real scalar s)
{
    real scalar result, j

    if (ell < 2) {
        return(1)
    }

    result = 1
    for (j = 1; j <= ell - 1; j++) {
        result = result * (s + j) / j
    }

    return(result)
}

/*---------------------------------------------------------------------------
 * compute_kdid_pre_coefficients() - Compute pre-period transformation coeffs
 *
 * For the k-th component estimator with lead s, the Δ^k_s operator applied
 * to group means yields:
 *
 *   Δ^k_s(Ȳ_{g,T*+s}) = Ȳ_{g,T*+s} - α₀·Ȳ_{g,T*-1}
 *                        - α₁·Ȳ_{g,T*-2} - ... - α_{k-1}·Ȳ_{g,T*-k}
 *
 * This function returns the coefficient vector (α₀, α₁, ..., α_{k-1}).
 *
 * Derivation:
 *   α₀ = 1 + Σ_{j=1}^{k-1} M^{j+1}_s
 *   αₚ = Σ_{j=p}^{k-1} M^{j+1}_s · (-1)^p · C(j,p)   for p = 1..k-1
 *
 * These arise from expanding the closed-form Δ^k_s using the standard
 * finite difference Δ^j(Ȳ_{g,T*-1}) = Σ_{m=0}^{j} (-1)^m C(j,m) Ȳ_{g,T*-1-m}.
 *
 * Arguments:
 *   k : real scalar, component order (≥ 1)
 *   s : real scalar, lead (≥ 0)
 *
 * Returns:
 *   real rowvector (1 × k): (α₀, α₁, ..., α_{k-1})
 *
 * Special cases:
 *   k=1: (1)                   -- standard DID
 *   k=2, s=0: (2, -1)          -- sequential DID (lead=0)
 *   k=2, s>0: (s+2, -(s+1))   -- sequential DID (lead>0)
 *---------------------------------------------------------------------------*/
real rowvector compute_kdid_pre_coefficients(real scalar k, real scalar s)
{
    real rowvector alpha
    real scalar j, p, M_jp1, sign_p, comb_jp

    if (k < 1) {
        _error("compute_kdid_pre_coefficients: k must be >= 1")
    }

    alpha = J(1, k, 0)

    // α₀ = 1 + Σ_{j=1}^{k-1} M^{j+1}_s
    alpha[1] = 1
    for (j = 1; j <= k - 1; j++) {
        alpha[1] = alpha[1] + compute_M_coeff(j + 1, s)
    }

    // αₚ = Σ_{j=p}^{k-1} M^{j+1}_s · (-1)^p · C(j,p) for p = 1..k-1
    for (p = 1; p <= k - 1; p++) {
        sign_p = (mod(p, 2) == 0 ? 1 : -1)
        alpha[p + 1] = 0
        for (j = p; j <= k - 1; j++) {
            M_jp1 = compute_M_coeff(j + 1, s)
            comb_jp = comb(j, p)
            alpha[p + 1] = alpha[p + 1] + M_jp1 * sign_p * comb_jp
        }
    }

    return(alpha)
}

/*---------------------------------------------------------------------------
 * _kdid_outcome_by_lead() - Generalized k-th order transformed outcome
 *
 * For the k-th component estimator, constructs a transformed outcome such
 * that the standard 2×2 DID regression on {-1, lead} recovers τ̂_k(s).
 *
 * Post-period (time_std == lead): keeps Y unchanged.
 * Pre-period (time_std == -1):    replaces with
 *   Ỹ^{(k)}_{i,-1} = α₀ · Y_{i,T*-1} + Σ_{p=1}^{k-1} αₚ · Ȳ_{g_i,T*-1-p}
 *
 * For k=1 this is identity (standard DID).
 * For k=2 this matches _sdid_outcome_by_lead() exactly.
 *
 * Arguments:
 *   Y            : real colvector, outcome Y_it
 *   Gi           : real colvector, group indicator G_i (0/1)
 *   time_std     : real colvector, standardized time (0 = treatment period)
 *   id_unit      : real colvector, unit identifier
 *   support_mask : real colvector, valid observation mask
 *   lead         : real scalar, post-treatment period
 *   k            : real scalar, component order (≥ 1)
 *
 * Returns:
 *   real colvector: transformed outcome (same length as Y, missing where
 *                   the transformation cannot be computed)
 *---------------------------------------------------------------------------*/
real colvector _kdid_outcome_by_lead(real colvector Y,
                                     real colvector Gi,
                                     real colvector time_std,
                                     real colvector id_unit,
                                     real colvector support_mask,
                                     real scalar lead,
                                     real scalar k)
{
    real colvector result, idx_pre1, idx_post, idx_period, idx_valid
    real rowvector alpha
    real scalar g, p, mean_period, target_ts, n_Y
    real scalar alpha_0

    n_Y = rows(Y)
    result = J(n_Y, 1, .)

    // k=1: standard DID, the pre-period outcome is just Y itself
    if (k == 1) {
        // Post-period
        if (lead <= 0) {
            // lead=0: periods are {-1, 0}; post is time_std==0
            idx_post = selectindex((time_std :== 0) :& (support_mask :> 0) :& (Y :< .))
            if (length(idx_post) > 0) {
                result[idx_post] = Y[idx_post]
            }
            idx_pre1 = selectindex((time_std :== -1) :& (support_mask :> 0) :& (Y :< .))
            if (length(idx_pre1) > 0) {
                result[idx_pre1] = Y[idx_pre1]
            }
        }
        else {
            idx_post = selectindex((time_std :== lead) :& (support_mask :> 0) :& (Y :< .))
            if (length(idx_post) > 0) {
                result[idx_post] = Y[idx_post]
            }
            idx_pre1 = selectindex((time_std :== -1) :& (support_mask :> 0) :& (Y :< .))
            if (length(idx_pre1) > 0) {
                result[idx_pre1] = Y[idx_pre1]
            }
        }
        return(result)
    }

    // k >= 2: compute coefficients and construct transformed outcome
    alpha = compute_kdid_pre_coefficients(k, (lead <= 0 ? 0 : lead))
    alpha_0 = alpha[1]

    for (g = 0; g <= 1; g++) {

        // Post-period: keep Y unchanged
        if (lead <= 0) {
            target_ts = 0
        }
        else {
            target_ts = lead
        }
        idx_post = selectindex((Gi :== g) :& (time_std :== target_ts) :& (support_mask :> 0) :& (Y :< .))
        if (length(idx_post) > 0) {
            result[idx_post] = Y[idx_post]
        }

        // Pre-period (time_std == -1): build transformed outcome
        idx_pre1 = selectindex((Gi :== g) :& (time_std :== -1) :& (support_mask :> 0) :& (Y :< .))
        if (length(idx_pre1) == 0) {
            continue
        }

        // Start with α₀ · Y_{i,T*-1}
        result[idx_pre1] = alpha_0 :* Y[idx_pre1]

        // Add αₚ · Ȳ_{g,T*-1-p} for p = 1..k-1
        for (p = 1; p <= k - 1; p++) {
            // Find observations at time T*-1-p (i.e., time_std == -(p+1))
            idx_period = selectindex((Gi :== g) :& (time_std :== -(p + 1)) :& (Y :< .))
            if (length(idx_period) == 0) {
                // Cannot compute: need period T*-1-p but it's missing
                result[idx_pre1] = J(length(idx_pre1), 1, .)
                break
            }
            mean_period = mean(Y[idx_period])
            if (missing(mean_period)) {
                result[idx_pre1] = J(length(idx_pre1), 1, .)
                break
            }
            result[idx_pre1] = result[idx_pre1] :+ alpha[p + 1] * mean_period
        }
    }

    return(result)
}

/*---------------------------------------------------------------------------
 * kdid_max_k_for_lead() - Determine maximum feasible k for a given lead
 *
 * The k-th component requires pre-treatment periods T*-1, T*-2, ..., T*-k
 * with non-empty observations for both treatment and control groups.
 *
 * Arguments:
 *   Gi           : real colvector, group indicator (0/1)
 *   time_std     : real colvector, standardized time
 *   Y            : real colvector, outcome
 *   support_mask : real colvector, valid observation mask
 *   kmax_req     : real scalar, user-requested kmax
 *
 * Returns:
 *   real scalar: K_init = min(kmax_req, max feasible k)
 *---------------------------------------------------------------------------*/
real scalar kdid_max_k_for_lead(real colvector Gi,
                                real colvector time_std,
                                real colvector Y,
                                real colvector support_mask,
                                real scalar kmax_req)
{
    real scalar k, p, n_treat, n_control
    real colvector idx

    for (k = 1; k <= kmax_req; k++) {
        // k-th component needs period T*-1-p for p = 0..k-1
        // i.e., time_std == -1, -2, ..., -k
        for (p = 0; p <= k - 1; p++) {
            // Check treated group
            idx = selectindex((Gi :== 1) :& (time_std :== -(p + 1)) :& (support_mask :> 0) :& (Y :< .))
            n_treat = length(idx)
            // Check control group
            idx = selectindex((Gi :== 0) :& (time_std :== -(p + 1)) :& (support_mask :> 0) :& (Y :< .))
            n_control = length(idx)

            if (n_treat == 0 || n_control == 0) {
                return(k - 1)
            }
        }
    }

    return(kmax_req)
}


// ----------------------------------------------------------------------------
// MODULE VERIFICATION FUNCTION
// ----------------------------------------------------------------------------

/*---------------------------------------------------------------------------
 * _did_estimators_loaded() - Verify module is loaded
 *---------------------------------------------------------------------------*/
void _did_estimators_loaded()
{
    printf("{txt}did_estimators.mata loaded successfully\n")
}

end