*! version 1.5  18oct2025
** version 1.0  10sep2025

program flexdid
	version 17.0
	Estimate `0'
	Aggregate
end

program Estimate, eclass sortpreserve
	syntax varlist(min=1 fv) [if] [in]		///
		[fweight pweight iweight],			///
		TX(varname numeric)					///
		TIme(varname numeric)				///
		GRoup(varname numeric)				///
		[									///
			SPECification(string)			///
			TXGRoup(varname numeric)		///
			USERCOhort(varname numeric)			///
			XINTeract(varlist fv)			///
			NOXINTeract						///
			VCE(string)						///
			VERbose							///
		* ]

	// parse Relay options
	_get_diopts diopts, `options'

	// drop permanent variables created and not dropped if the program ends in error
	capture drop _Tx _Grp _Chrt

	if "`specification'" == "" local specification "lagsonly" // specify lagsonly or lagsandleads.
	// If they gave weird strings as the specification
	if "`specification'"!="lagsonly" & "`specification'"!="lagsandleads" {
		display as error `"Value for {bf:specification()} must be "lagsonly" or "lagsandleads"."'
		exit 198
	}

	if "`noxinteract'"=="noxinteract" & "`xinteract'"!="" {
		display as error `"Noxinteract and xinteract cannot be specified together."'
		exit 198
	}

	// Get y and x vars
	gettoken yvar xvars : varlist

	// Weights
	if "`weight'" != "" {
			local wgt [`weight' `exp']
			tempvar wt
			quietly generate double `wt' `exp'
	}

	// VCE
	if "`vce'"=="" local vce "cluster `group'"
	if word("`vce'",1) != "cluster" & word("`vce'",1) != "robust" {
		display as error "vce must be either robust or cluster clustvar"
		exit 198
	}
	if word("`vce'",1) == "cluster" local clustvar `=word("`vce'",2)'

	// Marksample
	marksample touse
	markout `touse' `clustvar' `wt' `tx' `group' `time', strok

	// check that treatment variable is binary
	quietly summarize `tx' if `touse'
	local min = r(min)
	local max = r(max)
	quietly tabulate `tx' if `touse'
	local nvals = r(r)
	if (`min'!=0 | `max'!=1 | `nvals'!=2) {
		display as text "{p 0 6 0 78}"
		display as error "Invalid treatment variable - {bf:tx()} must be binary with 0 for control observations and 1 for treated observations"
		display as text "{p_end}"
		exit 450
	}

	// Check that time is equally spaced
	preserve
	quietly keep if `touse'
	quietly bysort `time': keep if _n==1
	quietly tsset `time'
	local gaps = r(gaps)
	restore
	if (`gaps'!=0 & "`usercohort'"=="") {
		display as text "{p 0 6 0 78}"
		display as error "Time variable has gaps. Specify {bf:usercohort()} to correctly assign cohorts to groups that were first treated at times coincident with gaps in the data."
		display as text "{p_end}"
		exit 451
	}

	if "`noxinteract'" == "" & "`xinteract'" == "" local xinteract "`xvars'"

	quietly local X "`xinteract' `xvars'"
	quietly local xvars: list uniq X

	// Define treatment groups
	if "`txgroup'" == "" local txgroup `group' // treatment group variable
	quietly egen int _Grp = min(`txgroup'/`tx') if `touse', by(`group')
	quietly replace _Grp = 0 if _Grp==. &  `touse'
	label variable _Grp "flexdid treated groups"

	// Define cohorts
	if "`usercohort'"=="" {
		quietly egen int _Chrt = min(`time'/`tx')  if `touse', by(`group')
		quietly replace _Chrt = 0 if _Chrt==. & `touse'
	}
	else quietly generate _Chrt = `usercohort' if `touse'
	label variable _Chrt "flexdid treated cohorts"

	// Check for always treated units
	quietly sum `time' if `touse', meanonly
	local tmin = r(min)

	quietly sum _Chrt if _Chrt>0 & `touse', meanonly
	local cmin = r(min)
	if (`cmin'<=`tmin') {
		display as text "{p 0 6 0 78}"
		display as error "The first cohort is treated in or before the first time period ({it:`tmin'}) observed in the data. This implies there are always-treated units. Remove always-treated units before using {bf:flexdid}."
		display as text "{p_end}"
		exit 498
	}

	// Check for never treated units
	quietly sum _Chrt if `touse', meanonly
	local cmin = r(min)
	local cmax = r(max)
	if (`cmin'>0) {
		display as text "{p 0 6 0 78}"
		display as text "There are no never-treated units. {bf:flexdid} will define the last cohort as the never-treated group after dropping observations in all time periods in which the last cohort was treated ({it:`cmax'})."
		display as text "{p_end}" _n

		tempvar lc
		quietly generate `lc' = (`time'>=`cmax')
		quietly replace `lc' = . if `lc'==1
		markout `touse' `lc'
		quietly replace _Chrt = 0 if _Chrt == `cmax' & `touse'
	}


	// Define pretreatment indicators by cohort (cohort-1 time is base)
	quietly generate byte _Tx = `tx' if `touse'
	quietly levelsof _Chrt if `touse', local(clevels)
	foreach c of local clevels {
		quietly replace _Tx = 1 if _Chrt>0 & _Chrt == `c' & `time'<`=`c'-1' & `touse'
	}
	label variable _Tx "flexdid treatment lags & leads indicator"

	// Define exposure time
	tempvar eventtime ieventtime
	quietly generate int `eventtime' = `time' - _Chrt if _Chrt>0 & `touse'
	quietly replace `eventtime' = -1 if _Chrt==0 & `touse'

	quietly count if _Chrt>0 & `eventtime'>=0 & `touse'
	local cn = r(N)
	quietly count if _Tx==1 & `eventtime'>=0 & `touse'
	local tn = r(N)
	if `cn'!=`tn' {
		display as text "{p 0 6 0 78}"
		display as text "Note: treatment varies at within `group' and `time' Is this expected? Although parameters have been estimated, this is outside the formal scope of the standard model specification. Interpret results appropriately."
		display as text "{p_end}" _n
	}

	// Group-qtr treatment coefficients -- lags and leads
	foreach c of local clevels {
		quietly levelsof _Grp if _Chrt==`c' & `c'>0 & `touse', local(glevels)
		foreach g of local glevels {
			quietly levelsof `time' if `touse', local(tlevels)
			foreach t of local tlevels {
				if `t' >= `c' local TxGlags ///
					`"`TxGlags' `c'._Chrt#`g'._Grp#`t'.`time'#1._Tx"'
			}
		}
	}

	foreach c of local clevels {
		quietly levelsof _Grp if _Chrt==`c' & `c'>0 & `touse', local(glevels)
		foreach g of local glevels {
			quietly levelsof `time' if `touse', local(tlevels)
			foreach t of local tlevels {
				if `t' >= `c' local TxGlagsXX ///
					`"`TxGlagsXX' `c'._Chrt#`g'._Grp#`t'.`time'#1._Tx#(c.(`xinteract')) "'
			}
		}
	}

	foreach c of local clevels {
		quietly levelsof _Grp if _Chrt==`c' & `c'>0 & `touse', local(glevels)
		foreach g of local glevels {
			quietly levelsof `time' if `touse', local(tlevels)
			foreach t of local tlevels {
				if `t' <=`=`c'-2' local TxGleads ///
					`"`TxGleads' `c'._Chrt#`g'._Grp#`t'.`time'#1._Tx "'
			}
		}
	}

	foreach c of local clevels {
		quietly levelsof _Grp if _Chrt==`c' & `c'>0 & `touse', local(glevels)
		foreach g of local glevels {
			quietly levelsof `time' if `touse', local(tlevels)
			foreach t of local tlevels {
				if `t' <=`=`c'-2' local TxGleadsXX ///
					`"`TxGleadsXX' `c'._Chrt#`g'._Grp#`t'.`time'#1._Tx#(c.(`xinteract')) "'
			}
		}
	}

	display as text "{p 0 6 0 78}"
	display as text "Note: Variables {bf:_Grp} containing group identifiers, {bf:_Chrt} containing cohort identifiers, and {bf:_Tx} containing lags and leads treatment indicators, were added to the dataset."
	display as text "{p_end}" _n

	// Lags only specifications
	if "`specification'"=="lagsonly" {
		display as text "Estimating lags only regression parameters"
		if "`verbose'" == "verbose" regress `yvar' `TxGlags' `TxGlagsXX' `xvars' i.`group' i.`time' i.`group'#(c.(`xinteract')) i.`time'#(c.(`xinteract')) `wgt' if `touse', vce(`vce')
		else quietly regress `yvar' `TxGlags' `TxGlagsXX' `xvars' i.`group' i.`time' i.`group'#(c.(`xinteract')) i.`time'#(c.(`xinteract')) `wgt' if `touse', vce(`vce')

	quietly testparm `TxGlags' `TxGlagsXX' `xvars' i.`group' i.`time' i.`group'#(c.(`xinteract')) i.`time'#(c.(`xinteract'))
	local F = r(F)
	}

	// Lags and leads specifications
	if "`specification'"=="lagsandleads" {
		display as text "Estimating lags and leads regression parameters"
		if "`verbose'" == "verbose" regress `yvar' `TxGlags' `TxGleads' `TxGlagsXX' `TxGleadsXX' `xvars' i.`group' i.`time' i.`group'#(c.(`xinteract')) i.`time'#(c.(`xinteract')) `wgt' if `touse', vce(`vce')
		else quietly regress `yvar' `TxGlags' `TxGleads' `TxGlagsXX' `TxGleadsXX' `xvars' i.`group' i.`time' i.`group'#(c.(`xinteract')) i.`time'#(c.(`xinteract')) `wgt' if `touse', vce(`vce')

	quietly testparm `TxGlags' `TxGleads' `TxGlagsXX' `TxGleadsXX' `xvars' i.`group' i.`time' i.`group'#(c.(`xinteract')) i.`time'#(c.(`xinteract'))
	local F = r(F)
	}

	ereturn local cmd flexdid
	ereturn local estat_cmd flexdid_estat
	ereturn local group "`group'"
	ereturn local time "`time'"
	ereturn local tx "`tx'"
	ereturn local txgroup "`txgroup'"
	ereturn local usercohort "`usercohort'"
	ereturn local specification "`specification'"

	ereturn scalar F = `F'

end


program Aggregate, rclass sortpreserve

	display as text "Aggregating estimates"

	flexdid_atet, overall

	tempname beta Var nm

	matrix `beta' = r(b)
	matrix `Var'  = r(V)
	tempname table
	matrix `table' = r(table)
	return hidden local title "ATET over exposure time"
	return matrix table = `table'
	return matrix b     = `beta'
	return matrix V     = `Var'
	return local atettype "overall"
	return add

end