package analytics

import (
	"strconv"
)

// NULL provenance tracking for formula control flow (null provenance).
//
// The formula domain distinguishes two things SQL renders identically as NULL:
//
//   - a BLANK operand, which makes a comparison FALSE and therefore selects the
//     else arm;
//   - #Error from undefined arithmetic (x/0), which poisons the entire formula
//     and yields blank for the whole field.
//
// A branch-selecting CASE absorbs both: `CASE WHEN <null> THEN a ELSE b END`
// silently returns b. For a blank operand that correctly selects the else arm.
// For undefined arithmetic it is a silent wrong answer — the defect this file addresses,
// where a 0/0 prepayment ratio fell through to the innermost ELSE literal and
// displayed an unearned discount tier.
//
// This file lets compileIf tell the two apart AT COMPILE TIME.
// Strictness (propagating the NULL as an undefined-arithmetic error) is enabled
// only when the condition's NULL can ONLY have come from undefined arithmetic.
// Anything the compiler cannot prove blank-free counts as blank, so the gate
// fails closed to today's emission, byte for byte.
//
// The invariant this establishes (deliberately narrower than "NULL means
// #Error inside formula space", which is false — other NULL sources exist and
// the sentinel is laundered back to 0 one formula layer up):
//
//	P. At an IF branch-selecting boundary, the compiler either KNOWS that a
//	   NULL condition can only be undefined arithmetic — and then propagates it
//	   as the formula error contract requires — or it does not know, and then
//	   it emits
//	   exactly today's SQL, byte for byte.
//
// Corollary: the only new emission paths sit behind `if strict`, so unchanged
// emission <=> the gate did not fire.

// markArithNullSite records that the subtree currently being compiled emitted
// arithmetic that can be undefined (division by a non-literal divisor, or
// POWER). Nil-safe so callers never have to branch.
//
// Marks describe emitted sites rather than unique AST identities. The gate
// therefore tests `> before` / `== before`, never an exact count; this remains
// correct for shared executable bodies and other context-copying compile paths.
func (ctx *CompileContext) markArithNullSite() {
	if ctx == nil || ctx.arithNullSites == nil {
		return
	}
	*ctx.arithNullSites++
}

// markBlankNullSite records that the subtree currently being compiled read
// something that can carry SQL NULL for a reason OTHER than undefined
// arithmetic — a blank stored value, an unmatched LEFT JOIN, a function whose
// lowering the compiler does not model. Nil-safe.
func (ctx *CompileContext) markBlankNullSite() {
	if ctx == nil || ctx.blankNullSites == nil {
		return
	}
	*ctx.blankNullSites++
}

// nullSiteCounts snapshots both counters. Callers take a delta window around a
// specific subtree (an IF condition / a CASE subject) and compare.
func (ctx *CompileContext) nullSiteCounts() (arith, blank int) {
	if ctx == nil {
		return 0, 0
	}
	if ctx.arithNullSites != nil {
		arith = *ctx.arithNullSites
	}
	if ctx.blankNullSites != nil {
		blank = *ctx.blankNullSites
	}
	return arith, blank
}

// strictNullPropagation reports whether a subtree compiled between the given
// "before" snapshot and now provably NULLs only via undefined arithmetic.
//
// Both halves are load-bearing:
//   - arith > before: there is undefined arithmetic to propagate at all. Without
//     it every condition would become strict and blank operands would start
//     blanking formulas that are correct today.
//   - blank == before: nothing else in the subtree can produce NULL. Without it
//     a blank text/boolean/date leaf sitting next to a division would be
//     misread as #Error and take the formula blank, which is the OPPOSITE of
//     the formula domain (blank comparison is FALSE, select the else arm).
//
// KNOWN RESIDUAL DIVERGENCE, and it is the price of the second half: a
// condition that mixes undefined arithmetic WITH a blank-capable leaf still
// absorbs the #Error, because the gate cannot tell which of the two produced
// the NULL. `IF(IF(Num__c/Den__c > 1, Str__c, "x") = "y", 1, 2)` with Den__c = 0
// returns the else arm here instead of blanking the formula. That is fail-closed to
// today's behavior, never a new wrong answer, and separating the two would need
// a per-value error flag rather than a single SQL NULL.
func strictNullPropagation(arithBefore, blankBefore, arithAfter, blankAfter int) bool {
	return arithAfter > arithBefore && blankAfter == blankBefore
}

// fieldReadSQL is toFormulaDomainSQL plus NULL-provenance tracking.
//
// When the domain conversion did NOT wrap the read, the column still carries
// raw SQL NULL for a blank stored value, so the read is a blank-NULL site.
// When it DID wrap it (#4152 blank-as-zero for numeric/percent), the read can
// no longer be NULL and contributes nothing.
//
// Deriving blank-safety from toFormulaDomainSQL's own OUTPUT — rather than from
// a parallel hand-written type list — is deliberate: there is exactly one
// classifier and it is physically incapable of drifting from the normalizer.
// If #4152's coalesced type set ever widens, this widens with it automatically.
func fieldReadSQL(sqlExpr string, fieldType string, ctx *CompileContext) string {
	out := toFormulaDomainSQL(sqlExpr, fieldType)
	if out == sqlExpr {
		ctx.markBlankNullSite()
	}
	return out
}

// definedDivisor reports whether the divisor node cannot make the quotient
// undefined — i.e. it is a (possibly sign-prefixed) number literal that parses
// to a non-zero value.
//
// Leading unary +/- are unwrapped because the lexer does not fold them: `/-2`
// parses as *UnaryOp{"-", NumberLit{2}}, not *NumberLit{"-2"}. Without the
// unwrap `IF(Num__c/-2 > 0.5, 1, 2)` marked an arith site and acquired a strict
// guard on `COALESCE(x,0) / (-2)`, which can never be NULL — unreachable SQL, a
// duplicated condition and needless golden churn for a divisor that is provably
// safe. Wrong in the "defined" direction is what would matter (it would enable
// strictness where it does not belong), and a sign-prefixed literal is still a
// literal, so this stays exact rather than a guess.
//
// Nothing else is folded. A parenthesised expression, a named constant or an
// arithmetic sub-expression all stay "not defined": guessing wrong there in the
// undefined direction only costs a gate that stays closed, which is fail-closed.
func definedDivisor(node Node) bool {
	for {
		unary, ok := node.(*UnaryOp)
		if !ok || (unary.Op != "-" && unary.Op != "+") {
			break
		}
		node = unary.Operand
	}
	lit, ok := node.(*NumberLit)
	if !ok {
		return false
	}
	value, err := strconv.ParseFloat(lit.Value, 64)
	if err != nil {
		return false
	}
	return value != 0
}
