*! version 1.0.0  12may2026  S. Derya Uysal, Tymon Sloczynski, and Jeffrey M. Wooldridge

* SPDX-License-Identifier: MIT
* Copyright (c) 2026 S. Derya Uysal, Tymon Sloczynski, and Jeffrey M. Wooldridge
* See the LICENSE file in this distribution for full text.

capture program drop drlate_estimate
program define drlate_estimate, eclass
	version 17

	syntax [if] [in] [pw], ///
		yvar(string) tvar(string) zvar(string) ///
		omodel(string) tmodel(string) zmodel(string) ///
		[ ymodelvars(string) tmodelvars(string) zmodelvars(string) ///
		late latt nrm unnrm test(string) ///
		PSTOLerance(real 1e-5) OSample(name) STATNorm(string) METHOD(string) * ]

	// --- 1. Parse options ---
	_get_diopts diopts rest, `options'
	_drlate_gmmopts, `rest'
	local gmmopts `s(gmmopts)'
	local rest `s(rest)'
	local vce `s(vce)'
	local vcetype `s(vcetype)'
	local clustvar `s(clustvar)'
	local iteropt `s(iteropt)'
	local iteroptml `s(iteroptml)'

	_drlate_options, `rest'
	local drlateopts `s(drlateopts)'
	local rest `s(rest)'

	if "`rest'" != "" {
		local wc: word count `rest'
		di as err `"{p}`=plural(`wc',"option")' {bf:`rest'} "' ///
		`"`=plural(`wc',"is","are")' not allowed{p_end}"'
		exit 198
	}

	// --- 2. Option validation ---
	if "`vce'" == "cluster" {
		local vce "cluster `clustvar'"
	}

	if "`pstolerance'" == "" local pstolerance 1E-5
	if `pstolerance'<0 | `pstolerance'>=1 {
		di as err "{p}{bf:pstolerance()} must be >=0 and <1{p_end}"
		exit 198
	}

	if "`osample'" != "" {
		cap confirm variable `osample'
		if !c(rc) {
			di as err "{p}variable {bf:`osample'} already exists{p_end}"
			exit 110
		}
	}

	_drlate_getstats, `late' `latt' `nrm' `unnrm'
	local stat "`s(stat1)'"
	local statnorm "`s(stat2)'"

	if "`omodel'" == "linear" local omodel "regress"
	if "`tmodel'" == "linear" local tmodel "regress"

	if "`method'" == "" local method ipwra
	else if !inlist("`method'", "ipwra", "ipw", "aipw", "ra") {
		di as error "Invalid method {bf:`method'}"
		exit 198
	}

	if "`method'" == "ipw" {
		if "`ymodelvars'" != "" {
			di as error "outcome model not allowed with method(ipw)"
			exit 198
		}
		if "`tmodelvars'" != "" {
			di as error "treatment model not allowed with method(ipw)"
			exit 198
		}
	}

	if "`method'" == "ra" & "`zmodelvars'" != "" {
		di as error "instrument model not allowed with method(ra)"
		exit 198
	}

	if "`zmodel'" == "cbps" & "`stat'" == "latt" {
		di as error "CBPS not possible for LATT"
		exit 198
	}

	// --- 3. Sample and weight setup ---
	marksample touse
	markout `touse' `yvar'

	tempvar samplew
	if "`weight'" != "" qui gen double `samplew'`exp' if `touse'
	else qui gen double `samplew' = 1 if `touse'

	// --- 4. Input validation ---
	tempvar troot zroot
	tempname valuest valuesz

	qui tab `zvar' if `touse', gen(`zroot') matrow(`valuesz')
	if rowsof(`valuesz') != 2 {
		di as error "instrument {bf:`zvar'} must have exactly 2 values"
		exit 450
	}

	qui summarize `zvar' if `touse', meanonly
	if r(min)!=0 | r(max)!=1 {
		di as error "instrument {bf:`zvar'} must be 0/1"
		exit 450
	}

	if "`omodel'" == "logit" {
		tempvar yroot
		tempname valuesy
		qui tab `yvar' if `touse', gen(`yroot') matrow(`valuesy')
		if rowsof(`valuesy') != 2 {
			di as error "outcome must have exactly 2 values for logit"
			exit 450
		}
		qui summarize `yvar' if `touse'
		if r(min)!=0 | r(max)!=1 {
			di as error "outcome must be 0/1 for logit"
			exit 450
		}
	}

	if "`omodel'" == "poisson" {
		qui summarize `yvar' if `touse'
		if r(min) < 0 {
			di as error "outcome must be non-negative for poisson"
			exit 450
		}
	}

	if "`tmodel'" == "logit" {
		tempvar troot
		tempname valuest
		qui tab `tvar' if `touse', gen(`troot') matrow(`valuest')
		if rowsof(`valuest') != 2 {
			di as error "treatment must have exactly 2 values for logit"
			exit 450
		}
		qui summarize `tvar' if `touse'
		if r(min)!=0 | r(max)!=1 {
			di as error "treatment must be 0/1 for logit"
			exit 450
		}
	}

	if "`tmodel'" == "poisson" {
		qui summarize `tvar' if `touse'
		if r(min) < 0 {
			di as error "treatment must be non-negative for poisson"
			exit 450
		}
	}

	// --- 4b. Standardize continuous covariates ---
	// Tempvars are declared here so they stay in scope for all
	// subsequent calls (_drlate_ps, drlate_estimate_late/latt).
	// Logic follows kappalate / teffects2:
	// Pass 1: standardize c.var inside interactions
	// Pass 2: tokenize, skip factor tokens, standardize
	// plain continuous vars (r(r)>2)
	foreach __vl in y t z {
	if "``__vl'modelvars'" != "" {
		local __xvars `"``__vl'modelvars'"'

		// Pass 1: c.var in interactions
		local __testline `"`__xvars'"'
		while regexm(`"`__testline'"', "c\.([a-zA-Z0-9_]+)") {
			local __var = regexs(1)
			local __testline = subinstr(`"`__testline'"', "c.`__var'", "", 1)
			capture confirm variable `__var'
			if _rc == 0 {
				tempvar __stdvar
				qui sum `__var' if `touse'==1 [iw = `samplew']
				qui gen double `__stdvar' = (`__var' - r(mean)) / r(sd) if `touse'==1
				local __xvars : subinstr local __xvars "c.`__var'" "c.`__stdvar'", all
			}
		}

		// Pass 2: plain continuous vars
		local __cleanedvars ""
		tokenize `"`__xvars'"'
		while "`1'" != "" {
			local __token "`1'"
			macro shift
			local __newtoken "`__token'"
			local __skip 0
			if strpos("`__token'", "#") local __skip 1
			if regexm("`__token'", "^[0-9]+[a-z]?\.") local __skip 1
			if regexm("`__token'", "\.") local __skip 1
			if substr("`__token'", 1, 2) == "i." local __skip 1
			if substr("`__token'", 1, 2) == "c." local __skip 1
			if `__skip' == 0 {
				capture confirm variable `__token'
				if _rc == 0 {
					quietly capture tab `__token' if `touse'==1
					if _rc == 0 & r(r) > 2 {
						tempvar __stdvar
						qui sum `__token' if `touse'==1 [iw = `samplew']
						qui gen double `__stdvar' = (`__token' - r(mean)) / r(sd) if `touse'==1
						local __newtoken "`__stdvar'"
					}
				}
			}
			local __cleanedvars "`__cleanedvars' `__newtoken'"
		}

		// Use strtrim not list retokenize -- preserves ## operators
		local __cleanedvars = strtrim(`"`__cleanedvars'"')
		if `"`__cleanedvars'"' != "" local `__vl'modelvars `"`__cleanedvars'"'
		}
	}

	// --- 5. Compliance check ---
	tempname dmeanz1 dmeanz0
	qui summarize `tvar' if `zvar'==1 & `touse'==1
	scalar `dmeanz1' = r(mean)
	qui summarize `tvar' if `zvar'==0 & `touse'==1
	scalar `dmeanz0' = r(mean)

	// --- 6. PS estimation + overlap check ---
	tempvar ips wt1 wt0
	tempname bips bips_1 bips_0

	if "`method'" != "ra" {
		_drlate_ps, ///
			touse(`touse') zvar(`zvar') zmodelvars(`zmodelvars') ///
			zmodel(`zmodel') samplew(`samplew') ///
			pstolerance(`pstolerance') osample(`osample') ///
			ips(`ips') wt1(`wt1') wt0(`wt0') ///
			bips(`bips') bips1(`bips_1') bips0(`bips_0') ///
			iteropt(`iteropt') iteroptml(`iteroptml') ///
			gmmopts(`gmmopts') stat(`stat')
	}

	// --- 7. Normalize check ---
	tempname wt1m wt0m
	if "`method'" != "ra" {
		qui sum `wt1' if `touse'==1
		scalar `wt1m' = round(r(mean), 1e-6)
		qui sum `wt0' if `touse'==1
		scalar `wt0m' = round(r(mean), 1e-6)

		if "`statnorm'"=="nrm" & "`method'"!="ra" & `wt0m'==1 & `wt1m'==1 {
			local statnorm unnrm
		}
	}

	// --- 8. Dispatch to stat-specific estimator ---
	if "`stat'" == "late" {
		drlate_estimate_late, ///
			yvar(`yvar') tvar(`tvar') zvar(`zvar') ///
			omodel(`omodel') tmodel(`tmodel') zmodel(`zmodel') ///
			ymodelvars(`ymodelvars') tmodelvars(`tmodelvars') ///
			zmodelvars(`zmodelvars') ///
			method(`method') statnorm(`statnorm') ///
			touse(`touse') samplew(`samplew') ips(`ips') ///
			wt1(`wt1') wt0(`wt0') ///
			dmeanz1(`dmeanz1') dmeanz0(`dmeanz0') ///
			bips(`bips') bips1(`bips_1') bips0(`bips_0') ///
			vce(`vce') gmmopts(`gmmopts')
	}
	else if "`stat'" == "latt" {
		drlate_estimate_latt, ///
			yvar(`yvar') tvar(`tvar') zvar(`zvar') ///
			omodel(`omodel') tmodel(`tmodel') zmodel(`zmodel') ///
			ymodelvars(`ymodelvars') tmodelvars(`tmodelvars') ///
			zmodelvars(`zmodelvars') ///
			method(`method') statnorm(`statnorm') ///
			touse(`touse') samplew(`samplew') ips(`ips') ///
			wt1(`wt1') wt0(`wt0') ///
			dmeanz1(`dmeanz1') dmeanz0(`dmeanz0') ///
			bips(`bips') bips1(`bips_1') bips0(`bips_0') ///
			vce(`vce') gmmopts(`gmmopts')
	}

	// --- 9. Collect results from e() left by sub-program ---
	// (GMM e() results are still active after sub-program returns)
	local N = e(N)
	local N_clust = e(N_clust)

	tempname late_sc vc_late r_late var_late
	tempname ate_num r_ate_num var_ate_num
	tempname ate_denom r_ate_denom var_ate_denom

	scalar `late_sc' = _b[late:_cons]
	matrix `vc_late' = e(V)
	scalar `r_late' = rownumb(`vc_late', "late:_cons")
	scalar `var_late' = `vc_late'[`r_late', `r_late']

	scalar `ate_num' = _b[num:_cons]
	scalar `r_ate_num' = rownumb(`vc_late', "num:_cons")
	scalar `var_ate_num' = `vc_late'[`r_ate_num', `r_ate_num']

	scalar `ate_denom' = _b[denom:_cons]
	scalar `r_ate_denom' = rownumb(`vc_late', "denom:_cons")
	scalar `var_ate_denom' = `vc_late'[`r_ate_denom', `r_ate_denom']

	// --- 10. Post results ---
	if "`stat'" == "latt" {
		tempname b V
		matrix `b' = (`late_sc', `ate_num', `ate_denom')
		matrix `V' = (`var_late', 0, 0 \ 0, `var_ate_num', 0 \ 0, 0, `var_ate_denom')
		matrix rownames `b' = " "
		matrix colnames `b' = "LATT: D on Y" "ATT: Z on Y" "ATT: Z on D"
		matrix rownames `V' = "LATT: D on Y" "ATT: Z on Y" "ATT: Z on D"
		matrix colnames `V' = "LATT: D on Y" "ATT: Z on Y" "ATT: Z on D"
		ereturn post `b' `V', esample(`touse')
		ereturn local method "`method'"
		ereturn local statnorm "`statnorm'"
		ereturn local stat "`stat'"
		ereturn scalar dmeanz0 = `dmeanz0'
		ereturn scalar dmeanz1 = `dmeanz1'
	}
	else if "`stat'" == "late" {
		tempname b V
		matrix `b' = (`late_sc', `ate_num', `ate_denom')
		matrix `V' = (`var_late', 0, 0 \ 0, `var_ate_num', 0 \ 0, 0, `var_ate_denom')
		matrix rownames `b' = " "
		matrix colnames `b' = "LATE: D on Y" "ATE: Z on Y" "ATE: Z on D"
		matrix rownames `V' = "LATE: D on Y" "ATE: Z on Y" "ATE: Z on D"
		matrix colnames `V' = "LATE: D on Y" "ATE: Z on Y" "ATE: Z on D"
		ereturn post `b' `V', esample(`touse')
		ereturn local method "`method'"
		ereturn local statnorm "`statnorm'"
		ereturn local stat "`stat'"
		ereturn scalar dmeanz0 = `dmeanz0'
		ereturn scalar dmeanz1 = `dmeanz1'
	}

	// --- 11. Display ---
	if "`method'" == "ipw" local methodd "IPW"
	else if "`method'" == "aipw" local methodd "AIPW"
	else if "`method'" == "ipwra" local methodd "IPWRA"
	else if "`method'" == "ra" local methodd "RA"

	if "`zmodel'" == "ipt" local statnormd "normalized"
	else if "`statnorm'" == "nrm" local statnormd "normalized"
	else local statnormd "unnormalized"

	if "`omodel'" == "regress" local omodeld "linear"
	else if "`omodel'" == "poisson" local omodeld "Poisson"
	else local omodeld "`omodel'"

	if "`tmodel'" == "regress" local tmodeld "linear"
	else if "`tmodel'" == "poisson" local tmodeld "Poisson"
	else local tmodeld "`tmodel'"

	if "`method'" == "ipw" {
		local omodeld "weighted mean"
		local tmodeld "weighted mean"
	}

	if "`zmodel'" == "logit" local zmodeld "logit (MLE)"
	else if "`zmodel'" == "cbps" local zmodeld "logit (CBPS)"
	else if "`zmodel'" == "ipt" local zmodeld "logit (IPT)"

	if "`stat'" == "late" di as txt _n "Local average treatment effect" ///
		"{col 51}Number of obs {col 67}= " as res %10.0fc `N'
	else di as txt _n "Local average treatment effect on the treated" ///
		"{col 51}Number of obs {col 67}= " as res %10.0fc `N'
	if "`method'" == "ra" | "`method'" == "ipwra" di "{txt:Estimator}{col 18}:{res: `methodd'}"
	else di "{txt:Estimator}{col 18}:{res: `methodd' (`statnormd')}"
	di "{txt:Outcome model}{col 18}:{res: `omodeld'}"
	di "{txt:Treatment model}{col 18}:{res: `tmodeld'}"
	di "{txt:Instrument model}{col 18}:{res: `zmodeld'}"
end