/-
Copyright (c) 2026 Michael R. Douglas. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

# Hermite Polynomials and Wick Ordering

The Wick-ordered monomial `:x^n:_c` (defined by the recursion
`:x^0: = 1`, `:x^1: = x`, `:x^{n+2}: = x · :x^{n+1}: - (n+1)·c · :x^n:`)
equals the scaled probabilist's Hermite polynomial:

  `:x^n:_c = c^{n/2} · He_n(x / √c)`

This file proves this identity by induction using the Hermite three-term
recurrence from `HermiteFunctions.lean`.

## Main results

- `scaledHermite_succ_succ` — the scaled Hermite polynomial satisfies the
  Wick monomial recursion
- `wick_eq_hermiteR` — the Wick monomial equals the scaled Hermite polynomial
  (using `√c ^ n` form)
- `wick_eq_hermiteR_rpow` — same, using `c ^ (n/2)` form

## References

- Simon, *The P(φ)₂ Euclidean QFT*, §I.3
- Glimm-Jaffe, *Quantum Physics*, §8.6
-/

import SchwartzNuclear.HermiteFunctions
import Mathlib.Analysis.SpecialFunctions.Pow.NNReal
import Mathlib.Data.Nat.Choose.Basic
import Mathlib.Data.Nat.Factorial.BigOperators
import Mathlib.Data.Fintype.Pi

noncomputable section

open Polynomial Real

/-! ## Wick monomial recursion -/

/-- The Wick-ordered monomial `:x^n:_c` defined by the three-term recursion.
This is a general definition — not specific to any QFT project. -/
def wickMonomial : ℕ → ℝ → ℝ → ℝ
  | 0, _, _ => 1
  | 1, _, x => x
  | n + 2, c, x => x * wickMonomial (n + 1) c x - (n + 1 : ℝ) * c * wickMonomial n c x

@[simp] theorem wickMonomial_zero (c x : ℝ) : wickMonomial 0 c x = 1 := rfl
@[simp] theorem wickMonomial_one (c x : ℝ) : wickMonomial 1 c x = x := rfl

theorem wickMonomial_succ_succ (n : ℕ) (c x : ℝ) :
    wickMonomial (n + 2) c x =
    x * wickMonomial (n + 1) c x - (n + 1 : ℝ) * c * wickMonomial n c x := rfl

/-! ## Scaled Hermite polynomial satisfies Wick recursion -/

/-- The scaled Hermite polynomial `(√c)^n · He_n(x/√c)`. -/
def scaledHermite (n : ℕ) (c x : ℝ) : ℝ :=
  Real.sqrt c ^ n * (hermiteR n).eval (x / Real.sqrt c)

@[simp] theorem scaledHermite_zero (c x : ℝ) : scaledHermite 0 c x = 1 := by
  simp [scaledHermite, hermiteR, hermite_zero]

theorem scaledHermite_one (c x : ℝ) (hc : 0 < c) : scaledHermite 1 c x = x := by
  simp [scaledHermite, hermiteR, hermite_succ, hermite_zero]
  exact mul_div_cancel₀ x (ne_of_gt (Real.sqrt_pos.mpr hc))

/-- The three-term recurrence for scaled Hermite polynomials:
  `scaledHermite (n+2) c x = x · scaledHermite (n+1) c x - (n+1)·c · scaledHermite n c x`

This is the same recursion as the Wick monomial, which proves they are equal. -/
theorem scaledHermite_succ_succ (n : ℕ) (c x : ℝ) (hc : 0 < c) :
    scaledHermite (n + 2) c x =
    x * scaledHermite (n + 1) c x - (n + 1 : ℝ) * c * scaledHermite n c x := by
  set s := Real.sqrt c
  have hs : s ≠ 0 := ne_of_gt (Real.sqrt_pos.mpr hc)
  set t := x / s
  have hst : s * t = x := mul_div_cancel₀ x hs
  have hs2 : s * s = c := Real.mul_self_sqrt (le_of_lt hc)
  -- Use the Hermite three-term recurrence
  have hrec := hermiteR_recurrence_succ n t
  -- hrec : t * He_{n+1}(t) = He_{n+2}(t) + (n+1) * He_n(t)
  -- So He_{n+2}(t) = t * He_{n+1}(t) - (n+1) * He_n(t)
  -- LHS = s^(n+2) * He_{n+2}(t)
  --      = s^(n+2) * (t * He_{n+1}(t) - (n+1) * He_n(t))
  -- RHS = x * s^(n+1) * He_{n+1}(t) - (n+1) * c * s^n * He_n(t)
  -- These match because s^(n+2) * t = s^(n+1) * (s*t) = s^(n+1) * x
  -- and s^(n+2) = s*s * s^n = c * s^n
  simp only [scaledHermite, show Real.sqrt c = s from rfl]
  -- Replace He_{n+2}(t) using recurrence
  have h_he : (hermiteR (n + 2)).eval t =
      t * (hermiteR (n + 1)).eval t - ↑(n + 1) * (hermiteR n).eval t := by linarith
  rw [h_he]
  -- Goal: s^(n+2) * (t*H1 - (n+1)*H0) = x * (s^(n+1)*H1) - (n+1)*c * (s^n*H0)
  -- Use: s^(n+2) = s*s*s^n = c*s^n, s^(n+1) = s*s^n, c*t = s*x
  set H1 := (hermiteR (n + 1)).eval t
  set H0 := (hermiteR n).eval t
  have key : c * t = s * x := by
    calc c * t = c * (x / s) := rfl
      _ = s * s * x / s := by rw [hs2, mul_div_assoc]
      _ = s * (s * x / s) := by ring
      _ = s * x := by rw [mul_div_cancel_left₀ _ hs]
  calc s ^ (n + 2) * (t * H1 - ↑(n + 1) * H0)
      = s * s * s ^ n * (t * H1 - ↑(n + 1) * H0) := by ring
    _ = c * s ^ n * (t * H1 - ↑(n + 1) * H0) := by rw [hs2]
    _ = c * t * (s ^ n * H1) - ↑(n + 1) * c * (s ^ n * H0) := by ring
    _ = s * x * (s ^ n * H1) - ↑(n + 1) * c * (s ^ n * H0) := by rw [key]
    _ = x * (s ^ (n + 1) * H1) - ↑(n + 1) * c * (s ^ n * H0) := by ring
    _ = x * (s ^ (n + 1) * H1) - (↑n + 1) * c * (s ^ n * H0) := by push_cast; ring

/-! ## Main theorem: Wick monomial = scaled Hermite polynomial -/

/-- **Wick monomials are scaled Hermite polynomials.**

For `c > 0`, the Wick-ordered monomial `:x^n:_c` equals `(√c)^n · He_n(x/√c)`
where `He_n` is the probabilist's Hermite polynomial.

Proof by induction using the shared three-term recurrence. -/
theorem wick_eq_hermiteR : ∀ (n : ℕ) (c : ℝ) (_ : 0 < c) (x : ℝ),
    wickMonomial n c x = scaledHermite n c x
  | 0, _, _, _ => by simp
  | 1, c, hc, x => by rw [wickMonomial_one, scaledHermite_one c x hc]
  | n + 2, c, hc, x => by
    rw [wickMonomial_succ_succ, scaledHermite_succ_succ n c x hc,
        wick_eq_hermiteR (n + 1) c hc x, wick_eq_hermiteR n c hc x]

/-- **Wick monomials are Hermite polynomials** (rpow form).

  `:x^n:_c = c^{n/2} · He_n(x / √c)`

where `c^{n/2}` is the real power `Real.rpow c (n/2)`. -/
theorem wick_eq_hermiteR_rpow (n : ℕ) (c : ℝ) (hc : 0 < c) (x : ℝ) :
    wickMonomial n c x =
    c ^ ((n : ℝ) / 2) * (hermiteR n).eval (x / Real.sqrt c) := by
  rw [wick_eq_hermiteR n c hc x, scaledHermite]
  congr 1
  -- Show: √c ^ n = c ^ ((n : ℝ) / 2)
  -- √c = c ^ (1/2), so √c ^ n = (c ^ (1/2)) ^ n = c ^ (n/2)
  rw [Real.sqrt_eq_rpow, ← Real.rpow_natCast (c ^ (1/2 : ℝ)) n,
      ← Real.rpow_mul (le_of_lt hc)]
  congr 1
  ring

/-! ## Multiplication-by-x identity (rearranged Wick recursion) -/

/-- **Multiplication-by-`x` identity.**

Multiplying a Wick monomial by its argument shifts the degree up and
introduces a contraction term:

  `x · :x^k:_c = :x^{k+1}:_c + k · c · :x^{k-1}:_c`.

For `k = 0` the contraction term vanishes (`(0 : ℝ) · c · _ = 0`).
For `k ≥ 1` this is the recursion `wickMonomial_succ_succ` rearranged. -/
theorem wickMonomial_x_mul : ∀ (k : ℕ) (c x : ℝ),
    x * wickMonomial k c x =
      wickMonomial (k + 1) c x + (k : ℝ) * c * wickMonomial (k - 1) c x
  | 0, c, x => by simp
  | k + 1, c, x => by
    -- `wickMonomial_succ_succ`: W_{k+2} = x · W_{k+1} - (k+1) c · W_k
    -- Rearrange: x · W_{k+1} = W_{k+2} + (k+1) c · W_k.
    have h := wickMonomial_succ_succ k c x
    -- (k + 1) - 1 = k
    have h0 : ((k + 1 : ℕ) - 1 : ℕ) = k := rfl
    rw [h0]
    push_cast
    linarith

/-! ## Homogeneity

`:γ·x^n:_{γ²·c} = γ^n · :x^n:_c`. Proved by induction using the
three-term recurrence — pure algebra, no Hermite-functional content. -/

/-- **Wick homogeneity**: rescaling `x` by `γ` and `c` by `γ²` rescales
the Wick monomial by `γ^n`. -/
theorem wickMonomial_homogeneity : ∀ (n : ℕ) (γ c x : ℝ),
    wickMonomial n (γ ^ 2 * c) (γ * x) = γ ^ n * wickMonomial n c x
  | 0, _, _, _ => by simp
  | 1, γ, _, x => by simp
  | n + 2, γ, c, x => by
    rw [wickMonomial_succ_succ, wickMonomial_homogeneity (n + 1) γ c x,
        wickMonomial_homogeneity n γ c x, wickMonomial_succ_succ]
    ring

/-! ## Bivariate Wick addition (binomial-type formula)

The Wick monomial in a sum `(c₁ + c₂, x + y)` expands as a binomial-type
sum. This is the polynomial identity equivalent to the generating-function
factorisation
  `exp(t(x+y) − (c₁+c₂)t²/2)
     = exp(tx − c₁ t²/2) · exp(ty − c₂ t²/2)`
expanded coefficient-wise in `t`. -/

/-- Auxiliary "double-indexed" sum used in the proof of bivariate Wick
addition.  Indexes a pair `(a, b) : ℕ × ℕ` with `a + b = n` (encoded
as `b = n - a` and the implicit constraint `a ≤ n` from the `range`). -/
private noncomputable def bivariateSum
    (n : ℕ) (c₁ c₂ x y : ℝ) : ℝ :=
  ∑ k ∈ Finset.range (n + 1),
    (n.choose k : ℝ) * wickMonomial k c₁ x * wickMonomial (n - k) c₂ y

private lemma bivariateSum_zero (c₁ c₂ x y : ℝ) :
    bivariateSum 0 c₁ c₂ x y = 1 := by
  simp [bivariateSum]

private lemma bivariateSum_one (c₁ c₂ x y : ℝ) :
    bivariateSum 1 c₁ c₂ x y = x + y := by
  simp [bivariateSum, Finset.sum_range_succ]
  ring

/-! ### Choose-absorption identities (real-valued versions). -/

/-- `(k + 1) · C(n+1, k+1) = (n+1) · C(n, k)`, real-valued form.
This is `Nat.succ_mul_choose_eq` cast to `ℝ`. -/
private lemma choose_absorb_left_real (n k : ℕ) :
    (((k + 1 : ℕ)) : ℝ) * ((n + 1).choose (k + 1) : ℝ) =
      ((n + 1 : ℕ) : ℝ) * (n.choose k : ℝ) := by
  have h := Nat.add_one_mul_choose_eq n k
  -- h : (n + 1) * n.choose k = (n + 1).choose (k + 1) * (k + 1)
  have h' : (k + 1) * (n + 1).choose (k + 1) = (n + 1) * n.choose k := by linarith [h]
  exact_mod_cast h'

/-- `(n + 1 - k) · C(n+1, k) = (n+1) · C(n, k)` when `k ≤ n`, real-valued form.
Derived by direct factorial manipulation. -/
private lemma choose_absorb_right_real (n k : ℕ) (hk : k ≤ n) :
    (((n + 1 - k : ℕ)) : ℝ) * ((n + 1).choose k : ℝ) =
      ((n + 1 : ℕ) : ℝ) * (n.choose k : ℝ) := by
  have hk' : k ≤ n + 1 := Nat.le_succ_of_le hk
  have hkn : n + 1 - k = (n - k) + 1 := by omega
  have hfac : k.factorial > 0 := Nat.factorial_pos k
  have hfac' : (n - k).factorial > 0 := Nat.factorial_pos (n - k)
  have key : ((n + 1 - k) * (n + 1).choose k) * (k.factorial * (n - k).factorial) =
         ((n + 1) * n.choose k) * (k.factorial * (n - k).factorial) := by
    have lhs :
        ((n + 1 - k) * (n + 1).choose k) * (k.factorial * (n - k).factorial) =
          (n + 1).factorial := by
      rw [hkn]
      have rearr :
          ((n - k + 1) * (n + 1).choose k) * (k.factorial * (n - k).factorial) =
            (n + 1).choose k * k.factorial * ((n - k + 1) * (n - k).factorial) := by ring
      rw [rearr, ← Nat.factorial_succ]
      have hf : (n + 1).choose k * k.factorial * (n + 1 - k).factorial = (n + 1).factorial :=
        Nat.choose_mul_factorial_mul_factorial hk'
      rw [hkn] at hf
      exact hf
    have rhs :
        ((n + 1) * n.choose k) * (k.factorial * (n - k).factorial) = (n + 1).factorial := by
      have :
          ((n + 1) * n.choose k) * (k.factorial * (n - k).factorial) =
            (n + 1) * (n.choose k * k.factorial * (n - k).factorial) := by ring
      rw [this, Nat.choose_mul_factorial_mul_factorial hk, ← Nat.factorial_succ]
    rw [lhs, rhs]
  have hpos : k.factorial * (n - k).factorial > 0 := Nat.mul_pos hfac hfac'
  have hnat : (n + 1 - k) * (n + 1).choose k = (n + 1) * n.choose k :=
    Nat.eq_of_mul_eq_mul_right hpos key
  exact_mod_cast hnat

/-! ### Three-term recursion for `bivariateSum`. -/

/-- The bivariate sum satisfies the same three-term Wick recursion as
`wickMonomial n (c₁ + c₂) (x + y)`.  Combined with matching base cases
this yields the bivariate addition formula. -/
private lemma bivariateSum_recursion (n : ℕ) (c₁ c₂ x y : ℝ) :
    bivariateSum (n + 2) c₁ c₂ x y =
      (x + y) * bivariateSum (n + 1) c₁ c₂ x y -
        ((n : ℝ) + 1) * (c₁ + c₂) * bivariateSum n c₁ c₂ x y := by
  -- Abbreviations.
  set W₁ : ℕ → ℝ := fun k => wickMonomial k c₁ x with hW₁
  set W₂ : ℕ → ℝ := fun k => wickMonomial k c₂ y with hW₂
  -- The multiplication-by-argument identity.
  have hx : ∀ k, x * W₁ k = W₁ (k + 1) + (k : ℝ) * c₁ * W₁ (k - 1) := fun k => by
    simpa [hW₁] using wickMonomial_x_mul k c₁ x
  have hy : ∀ k, y * W₂ k = W₂ (k + 1) + (k : ℝ) * c₂ * W₂ (k - 1) := fun k => by
    simpa [hW₂] using wickMonomial_x_mul k c₂ y
  -- Unfold bivariateSum.
  simp only [bivariateSum]
  -- The "top" piece: combine the W₁-shifted (via x) and W₂-shifted (via y) sums.
  -- Define the "top-shift-x" sum:  A = ∑_{k=0}^{n+1} C(n+1, k) W₁(k+1) W₂(n+1-k).
  -- Re-indexed (k' = k+1): A = ∑_{k'=1}^{n+2} C(n+1, k'-1) W₁(k') W₂(n+2-k').
  -- And the "top-shift-y" sum: C = ∑_{k=0}^{n+1} C(n+1, k) W₁(k) W₂(n+2-k).
  -- A + C should give bivariateSum(n+2) by Pascal.
  -- The "low" pieces: B = ∑ k·c₁·C(n+1,k) W₁(k-1) W₂(n+1-k),
  --                   D = ∑ (n+1-k)·c₂·C(n+1,k) W₁(k) W₂(n-k),
  -- and these should equal (n+1)c₁·bivariateSum(n) and (n+1)c₂·bivariateSum(n)
  -- respectively, by the absorption identities.
  --
  -- We prove the recursion by computing
  --   (x+y)·bivariateSum(n+1) - bivariateSum(n+2)
  --     = (n+1)(c₁+c₂)·bivariateSum(n).
  -- Equivalently:  bivariateSum(n+2) + (n+1)(c₁+c₂)·bivariateSum(n)
  --             = (x+y)·bivariateSum(n+1).
  -- We organise the proof as: RHS - LHS = 0.
  -- Move everything to one side.
  have key :
      (x + y) * (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 1 - k)) =
      (∑ k ∈ Finset.range (n + 3),
          ((n + 2).choose k : ℝ) * W₁ k * W₂ (n + 2 - k)) +
      ((n : ℝ) + 1) * (c₁ + c₂) *
        (∑ k ∈ Finset.range (n + 1),
          (n.choose k : ℝ) * W₁ k * W₂ (n - k)) := by
    -- Expand (x+y) and apply hx, hy.
    have expand_xy :
        (x + y) * (∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 1 - k)) =
        (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) * (W₁ (k + 1) + (k : ℝ) * c₁ * W₁ (k - 1)) *
            W₂ (n + 1 - k)) +
        (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) * W₁ k *
            (W₂ (n + 1 - k + 1) + ((n + 1 - k : ℕ) : ℝ) * c₂ * W₂ (n + 1 - k - 1))) := by
      rw [add_mul, Finset.mul_sum, Finset.mul_sum]
      congr 1
      · refine Finset.sum_congr rfl (fun k _ => ?_)
        rw [show x * (((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 1 - k)) =
              ((n + 1).choose k : ℝ) * (x * W₁ k) * W₂ (n + 1 - k) by ring,
            hx k]
      · refine Finset.sum_congr rfl (fun k _ => ?_)
        rw [show y * (((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 1 - k)) =
              ((n + 1).choose k : ℝ) * W₁ k * (y * W₂ (n + 1 - k)) by ring,
            hy (n + 1 - k)]
    rw [expand_xy]
    -- Split the four sums.
    rw [show
      (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) *
            (W₁ (k + 1) + (k : ℝ) * c₁ * W₁ (k - 1)) * W₂ (n + 1 - k)) =
        (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) * W₁ (k + 1) * W₂ (n + 1 - k)) +
        (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) * ((k : ℝ) * c₁ * W₁ (k - 1)) * W₂ (n + 1 - k)) by
      rw [← Finset.sum_add_distrib]; refine Finset.sum_congr rfl (fun k _ => ?_); ring]
    rw [show
      (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) * W₁ k *
            (W₂ (n + 1 - k + 1) + ((n + 1 - k : ℕ) : ℝ) * c₂ * W₂ (n + 1 - k - 1))) =
        (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 1 - k + 1)) +
        (∑ k ∈ Finset.range (n + 2),
          ((n + 1).choose k : ℝ) * W₁ k *
            (((n + 1 - k : ℕ) : ℝ) * c₂ * W₂ (n + 1 - k - 1))) by
      rw [← Finset.sum_add_distrib]; refine Finset.sum_congr rfl (fun k _ => ?_); ring]
    -- Now we have 4 sums on the LHS. Call them A_top, A_low, B_top, B_low.
    -- Claim:
    --   A_top + B_top = ∑_{j=0}^{n+2} C(n+2, j) W₁(j) W₂(n+2-j)   [by Pascal]
    --   A_low + B_low = (n+1)(c₁+c₂) · ∑_k C(n,k) W₁(k) W₂(n-k)   [by absorption]
    -- Add the two and rearrange.
    --
    -- A_top: reindex k → k+1.  Original: ∑_{k=0}^{n+1} C(n+1, k) W₁(k+1) W₂(n+1-k).
    -- After k → k-1 (i.e. j = k+1): ∑_{j=1}^{n+2} C(n+1, j-1) W₁(j) W₂(n+1-(j-1)) =
    -- ∑_{j=1}^{n+2} C(n+1, j-1) W₁(j) W₂(n+2-j).
    have h_A_top :
        (∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * W₁ (k + 1) * W₂ (n + 1 - k)) =
          ∑ j ∈ Finset.range (n + 3),
            (if j = 0 then (0 : ℝ) else ((n + 1).choose (j - 1) : ℝ)) *
              W₁ j * W₂ (n + 2 - j) := by
      -- Use Finset.sum_range_succ' on the RHS only.
      conv_rhs => rw [show (n + 3) = (n + 2) + 1 from rfl, Finset.sum_range_succ']
      -- The j = 0 term on the RHS vanishes.
      have hj0 : (if (0 : ℕ) = 0 then (0 : ℝ) else ((n + 1).choose (0 - 1) : ℝ)) *
            W₁ 0 * W₂ (n + 2 - 0) = 0 := by simp
      rw [hj0, add_zero]
      refine Finset.sum_congr rfl (fun k hk => ?_)
      have hk' : k < n + 2 := Finset.mem_range.mp hk
      have hkne : k + 1 ≠ 0 := Nat.succ_ne_zero _
      have hksub : (k + 1 : ℕ) - 1 = k := by omega
      have hk2 : n + 2 - (k + 1) = n + 1 - k := by omega
      rw [if_neg hkne, hksub, hk2]
    -- B_top is essentially ∑_{k=0}^{n+1} C(n+1, k) W₁(k) W₂(n+2-k), but the exponent
    -- on W₂ is `n + 1 - k + 1` which we need to simplify to `n + 2 - k` for k ≤ n+1.
    have h_B_top :
        (∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 1 - k + 1)) =
          ∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 2 - k) := by
      refine Finset.sum_congr rfl (fun k hk => ?_)
      have hk' : k ≤ n + 1 := by rw [Finset.mem_range] at hk; omega
      have : n + 1 - k + 1 = n + 2 - k := by omega
      rw [this]
    -- Reformulate B_top using the same "if j = 0 then 0 else …" form for compatibility,
    -- but extended over range(n+3) so that we can combine with A_top via Pascal.
    have h_B_top' :
        (∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 2 - k)) =
          ∑ j ∈ Finset.range (n + 3),
            (if j = n + 2 then (0 : ℝ) else ((n + 1).choose j : ℝ)) *
              W₁ j * W₂ (n + 2 - j) := by
      -- Rewrite RHS via Finset.sum_range_succ.
      conv_rhs => rw [show (n + 3) = (n + 2) + 1 from rfl, Finset.sum_range_succ]
      -- The k = n+2 term on the RHS vanishes because (if n+2 = n+2 then 0 else …) = 0.
      have hzero : (if (n + 2 : ℕ) = n + 2 then (0 : ℝ) else ((n + 1).choose (n + 2) : ℝ)) *
            W₁ (n + 2) * W₂ (n + 2 - (n + 2)) = 0 := by simp
      rw [hzero, add_zero]
      refine Finset.sum_congr rfl (fun k hk => ?_)
      have hk' : k < n + 2 := Finset.mem_range.mp hk
      have hne : k ≠ n + 2 := Nat.ne_of_lt hk'
      rw [if_neg hne]
    -- Combine: A_top + B_top = bivariateSum(n+2) by Pascal.
    have h_top :
        (∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * W₁ (k + 1) * W₂ (n + 1 - k)) +
        (∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * W₁ k * W₂ (n + 1 - k + 1)) =
          ∑ j ∈ Finset.range (n + 3),
            ((n + 2).choose j : ℝ) * W₁ j * W₂ (n + 2 - j) := by
      rw [h_A_top, h_B_top, h_B_top', ← Finset.sum_add_distrib]
      refine Finset.sum_congr rfl (fun j _ => ?_)
      -- Coefficient identity: (if j=0 then 0 else C(n+1, j-1)) + (if j=n+2 then 0 else C(n+1, j))
      -- = C(n+2, j).
      have hcoeff :
          (if j = 0 then (0 : ℝ) else ((n + 1).choose (j - 1) : ℝ)) +
              (if j = n + 2 then (0 : ℝ) else ((n + 1).choose j : ℝ)) =
            ((n + 2).choose j : ℝ) := by
        rcases Nat.eq_zero_or_pos j with hj0 | hj1
        · subst hj0
          simp [Nat.choose]
        · -- j ≥ 1
          rcases lt_or_ge j (n + 2) with hjlt | hjge
          · -- 1 ≤ j ≤ n + 1
            have hne0 : j ≠ 0 := Nat.ne_of_gt hj1
            have hne_n2 : j ≠ n + 2 := Nat.ne_of_lt hjlt
            simp [hne0, hne_n2]
            -- (n+1).choose (j-1) + (n+1).choose j = (n+2).choose j
            have hjsub : j - 1 + 1 = j := Nat.sub_add_cancel hj1
            -- Use: (n+1).choose ((j-1) + 1) = (n+1).choose (j-1) + (n+1).choose j ... wait.
            -- Pascal: (n+2).choose j = (n+1).choose (j-1) + (n+1).choose j when j ≥ 1.
            -- mathlib: Nat.choose_succ_succ : (n+1).choose (k+1) = n.choose k + n.choose (k+1).
            have hpas : (n + 1 + 1).choose (j - 1 + 1) =
                          (n + 1).choose (j - 1) + (n + 1).choose (j - 1 + 1) :=
              Nat.choose_succ_succ (n + 1) (j - 1)
            rw [hjsub] at hpas
            -- hpas : (n + 2).choose j = (n + 1).choose (j - 1) + (n + 1).choose j
            -- So coefficient sum = C(n+1, j-1) + C(n+1, j) = C(n+2, j).
            have : ((n + 2).choose j : ℝ) =
                ((n + 1).choose (j - 1) : ℝ) + ((n + 1).choose j : ℝ) := by
              exact_mod_cast hpas
            linarith
          · -- j ≥ n + 2.  We don't have an upper bound from the hypotheses
            -- here, but the equation should hold anyway for any such j ≥ n+2.
            -- For j = n+2: LHS = C(n+1, n+1) + 0 = 1 = C(n+2, n+2).
            -- For j > n+2: LHS = 0 + C(n+1, j) which equals 0 = C(n+2, j) since j > n+2.
            -- Easier: case on j = n+2 vs j ≥ n+3.
            rcases hjge.lt_or_eq with hj_gt | hj_eq
            · -- j > n + 2
              have hne0 : j ≠ 0 := by omega
              have hne_n2 : j ≠ n + 2 := by omega
              rw [if_neg hne0, if_neg hne_n2]
              have h1 : (n + 1).choose (j - 1) = 0 :=
                Nat.choose_eq_zero_of_lt (by omega)
              have h2 : (n + 1).choose j = 0 :=
                Nat.choose_eq_zero_of_lt (by omega)
              have h3 : (n + 2).choose j = 0 :=
                Nat.choose_eq_zero_of_lt (by omega)
              rw [h1, h2, h3]
              push_cast; ring
            · -- j = n + 2 (hj_eq : n + 2 = j)
              have hjeq : j = n + 2 := hj_eq.symm
              have hjsub : j - 1 = n + 1 := by omega
              have hne0 : j ≠ 0 := by omega
              rw [if_neg hne0, hjsub, if_pos hjeq]
              rw [hjeq, Nat.choose_self, Nat.choose_self]
              push_cast; ring
      -- Apply.
      have : (if j = 0 then (0 : ℝ) else ((n + 1).choose (j - 1) : ℝ)) * W₁ j * W₂ (n + 2 - j) +
              (if j = n + 2 then (0 : ℝ) else ((n + 1).choose j : ℝ)) * W₁ j * W₂ (n + 2 - j) =
            ((if j = 0 then (0 : ℝ) else ((n + 1).choose (j - 1) : ℝ)) +
              (if j = n + 2 then (0 : ℝ) else ((n + 1).choose j : ℝ))) *
              W₁ j * W₂ (n + 2 - j) := by ring
      rw [this, hcoeff]
    -- A_low: ∑_{k=0}^{n+1} C(n+1, k) (k:ℝ) c₁ W₁(k-1) W₂(n+1-k).
    -- The k=0 term vanishes.  Reindex k → k+1.
    have h_A_low :
        (∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * ((k : ℝ) * c₁ * W₁ (k - 1)) * W₂ (n + 1 - k)) =
          ((n : ℝ) + 1) * c₁ *
            (∑ k ∈ Finset.range (n + 1),
              (n.choose k : ℝ) * W₁ k * W₂ (n - k)) := by
      rw [show (n + 2) = (n + 1) + 1 from rfl, Finset.sum_range_succ']
      simp
      rw [Finset.mul_sum]
      refine Finset.sum_congr rfl (fun k hk => ?_)
      -- After the simp: each summand looks like C(n+1, k+1) * ((k+1:ℝ) * c₁ * W₁ k) * W₂ (n - k)
      -- and we want it equal to (n+1) * c₁ * (C(n, k) * W₁ k * W₂ (n - k)).
      have habs := choose_absorb_left_real n k
      -- habs : ((k + 1 : ℕ) : ℝ) * ((n + 1).choose (k + 1) : ℝ) = ((n + 1 : ℕ) : ℝ) * (n.choose k : ℝ)
      have habs' : ((k : ℝ) + 1) * ((n + 1).choose (k + 1) : ℝ) =
                    ((n : ℝ) + 1) * (n.choose k : ℝ) := by
        have := habs
        push_cast at this
        linarith
      have :
          ((n + 1).choose (k + 1) : ℝ) * (((k : ℝ) + 1) * c₁ * W₁ k) * W₂ (n - k) =
            (((k : ℝ) + 1) * ((n + 1).choose (k + 1) : ℝ)) * c₁ * W₁ k * W₂ (n - k) := by
        ring
      rw [this, habs']
      ring
    -- B_low: ∑_{k=0}^{n+1} C(n+1, k) W₁(k) (n+1-k) c₂ W₂(n+1-k-1).
    -- For k = n + 1, the factor (n+1-k) = 0 vanishes.  So restrict to k ∈ range (n+1).
    have h_B_low :
        (∑ k ∈ Finset.range (n + 2),
            ((n + 1).choose k : ℝ) * W₁ k *
              (((n + 1 - k : ℕ) : ℝ) * c₂ * W₂ (n + 1 - k - 1))) =
          ((n : ℝ) + 1) * c₂ *
            (∑ k ∈ Finset.range (n + 1),
              (n.choose k : ℝ) * W₁ k * W₂ (n - k)) := by
      -- Drop last (k = n+1) term: (n+1 - (n+1) : ℕ) = 0.
      rw [show (n + 2) = (n + 1) + 1 from rfl, Finset.sum_range_succ]
      have hzero :
          ((n + 1).choose (n + 1) : ℝ) * W₁ (n + 1) *
            (((n + 1 - (n + 1) : ℕ) : ℝ) * c₂ * W₂ (n + 1 - (n + 1) - 1)) = 0 := by
        have : (n + 1 - (n + 1) : ℕ) = 0 := Nat.sub_self _
        rw [this]
        simp
      rw [hzero, add_zero, Finset.mul_sum]
      refine Finset.sum_congr rfl (fun k hk => ?_)
      have hk' : k ≤ n := by rw [Finset.mem_range] at hk; omega
      have habs := choose_absorb_right_real n k hk'
      -- habs : ((n + 1 - k : ℕ) : ℝ) * ((n + 1).choose k : ℝ) = ((n + 1 : ℕ) : ℝ) * (n.choose k : ℝ)
      have hkn1 : n + 1 - k - 1 = n - k := by omega
      rw [hkn1]
      have :
          ((n + 1).choose k : ℝ) * W₁ k * (((n + 1 - k : ℕ) : ℝ) * c₂ * W₂ (n - k)) =
            (((n + 1 - k : ℕ) : ℝ) * ((n + 1).choose k : ℝ)) * c₂ * W₁ k * W₂ (n - k) := by
        ring
      rw [this, habs]
      push_cast
      ring
    -- Now combine everything.  We have four sums; group into (top + top) and (low + low).
    linarith [h_top, h_A_low, h_B_low]
  linarith [key]

/-- The Wick monomial vanishes for a zero argument and zero variance,
except at degree 0. -/
private lemma wickMonomial_zero_zero : ∀ k : ℕ, wickMonomial k (0 : ℝ) (0 : ℝ) =
    if k = 0 then 1 else 0
  | 0 => by simp
  | 1 => by simp
  | k + 2 => by
    rw [wickMonomial_succ_succ]
    simp [wickMonomial_zero_zero (k + 1), wickMonomial_zero_zero k]

/-- **Bivariate Wick addition.**

  `:_{(x+y)}^n:_{c₁+c₂} = ∑_k C(n, k) :x^k:_{c₁} · :y^{n-k}:_{c₂}`.

Proved by two-step induction on `n`: the LHS and the RHS (`bivariateSum`)
satisfy the same three-term recursion (`bivariateSum_recursion`) and the
same base cases. -/
theorem wickMonomial_add_add : ∀ (n : ℕ) (c₁ c₂ x y : ℝ),
    wickMonomial n (c₁ + c₂) (x + y) =
    ∑ k ∈ Finset.range (n + 1),
      (n.choose k : ℝ) * wickMonomial k c₁ x * wickMonomial (n - k) c₂ y := by
  -- It suffices to show wickMonomial n (c₁+c₂) (x+y) = bivariateSum n c₁ c₂ x y.
  suffices h : ∀ n c₁ c₂ x y, wickMonomial n (c₁ + c₂) (x + y) = bivariateSum n c₁ c₂ x y by
    intro n c₁ c₂ x y
    exact h n c₁ c₂ x y
  intro n
  induction n using Nat.twoStepInduction with
  | zero =>
    intro c₁ c₂ x y; simp [bivariateSum]
  | one =>
    intro c₁ c₂ x y
    rw [bivariateSum_one]; simp
  | more n ih1 ih2 =>
    intro c₁ c₂ x y
    rw [wickMonomial_succ_succ, ih2 c₁ c₂ x y, ih1 c₁ c₂ x y,
        bivariateSum_recursion n c₁ c₂ x y]

/-! ## Multivariate Wick multinomial expansion

The multivariate generalisation of the bivariate addition formula.
For any finite index set `ι`, and any `γ ξ : ι → ℝ`,

  `wickMonomial k (∑_j γ_j²) (∑_j γ_j · ξ_j)
     = ∑_{|α|=k} (k! / ∏_j α_j!) · (∏_j γ_j^{α_j}) · ∏_j wickMonomial α_j 1 ξ_j`.

Proof by `Finset.induction_on s` on the support set `s : Finset ι`,
using `wickMonomial_add_add` to peel off one summand at a time and
`wickMonomial_homogeneity` to absorb the resulting `γ_j^k` factors.
-/

/-- Multi-indices supported in a Finset `s ⊆ ι` with total degree `k`. -/
private noncomputable def multiIndicesSupportedIn
    {ι : Type*} [Fintype ι] [DecidableEq ι] (s : Finset ι) (k : ℕ) :
    Finset (ι → ℕ) :=
  (Fintype.piFinset (fun _ : ι => Finset.range (k + 1))).filter
    (fun α => (∀ j ∉ s, α j = 0) ∧ ∑ j, α j = k)

/-- The full set `multiIndicesOfTotalDegree` corresponds to the support
being all of `ι`. -/
private lemma multiIndicesSupportedIn_univ
    {ι : Type*} [Fintype ι] [DecidableEq ι] (k : ℕ) :
    multiIndicesSupportedIn (Finset.univ : Finset ι) k =
    (Fintype.piFinset (fun _ : ι => Finset.range (k + 1))).filter
      (fun α => ∑ j, α j = k) := by
  unfold multiIndicesSupportedIn
  ext α
  simp [Finset.mem_filter, Fintype.mem_piFinset, Finset.mem_range]

/-- Empty case: only the zero multi-index is supported in ∅, and only
at total degree 0. -/
private lemma multiIndicesSupportedIn_empty
    {ι : Type*} [Fintype ι] [DecidableEq ι] (k : ℕ) :
    multiIndicesSupportedIn (∅ : Finset ι) k =
      if k = 0 then {(fun _ => 0 : ι → ℕ)} else ∅ := by
  unfold multiIndicesSupportedIn
  ext α
  simp only [Finset.mem_filter, Fintype.mem_piFinset, Finset.mem_range,
    Finset.notMem_empty]
  rcases Nat.eq_zero_or_pos k with hk | hk
  · subst hk
    simp only [if_true, Finset.mem_singleton, funext_iff]
    constructor
    · rintro ⟨_, hα0, _⟩; intro j; exact hα0 j (by simp)
    · intro hα0
      refine ⟨fun i => ?_, fun j _ => hα0 j, ?_⟩
      · rw [hα0 i]; omega
      · simp_rw [hα0]; simp
  · have hk_ne : k ≠ 0 := Nat.ne_of_gt hk
    simp only [hk_ne, if_false, Finset.notMem_empty, iff_false, not_and]
    intro hbound hα0
    -- α j = 0 for all j (since α has empty support), so ∑ α = 0 ≠ k.
    have hzero : ∀ j, α j = 0 := fun j => hα0 j (by simp)
    simp_rw [hzero]
    simp
    omega

/-- **Multivariate Wick multinomial expansion** (over a Finset).

For any Finset `s : Finset ι`, any `γ ξ : ι → ℝ`, and any `k : ℕ`:

  `wickMonomial k (∑_{j ∈ s} γ_j²) (∑_{j ∈ s} γ_j · ξ_j) =
     ∑_{α ∈ multiIndicesSupportedIn s k}
       (k! / ∏_j α_j!) · (∏_j γ_j^{α_j}) · ∏_j wickMonomial α_j 1 ξ_j`.

The products on the RHS are over all of `ι`, but factors with
`α j = 0` (which holds for `j ∉ s`) contribute 1.

Proved by `Finset.induction_on s`, peeling off one element at a time
via `wickMonomial_add_add` and `wickMonomial_homogeneity`. -/
private theorem wickMonomial_pow_sum_expansion_aux
    {ι : Type*} [Fintype ι] [DecidableEq ι] (γ ξ : ι → ℝ)
    (s : Finset ι) :
    ∀ (k : ℕ),
    wickMonomial k (∑ j ∈ s, (γ j) ^ 2) (∑ j ∈ s, γ j * ξ j) =
    ∑ α ∈ multiIndicesSupportedIn s k,
      ((k.factorial : ℝ) / ∏ j, ((α j).factorial : ℝ)) *
      (∏ j, γ j ^ (α j)) *
      (∏ j, wickMonomial (α j) 1 (ξ j)) := by
  induction s using Finset.induction_on with
  | empty =>
    intro k
    -- LHS: wickMonomial k 0 0 = if k = 0 then 1 else 0.
    -- RHS: sum over multiIndicesSupportedIn ∅ k = if k = 0 then {0_index} else ∅.
    simp only [Finset.sum_empty, wickMonomial_zero_zero, multiIndicesSupportedIn_empty]
    rcases Nat.eq_zero_or_pos k with hk | hk
    · subst hk
      simp
    · have hk_ne : k ≠ 0 := Nat.ne_of_gt hk
      simp [hk_ne]
  | insert j₀ s hj₀ ih =>
    intro k
    -- Split off j₀.
    have hsum_var :
        (∑ j ∈ insert j₀ s, (γ j) ^ 2) = (γ j₀) ^ 2 + ∑ j ∈ s, (γ j) ^ 2 :=
      Finset.sum_insert hj₀
    have hsum_pt :
        (∑ j ∈ insert j₀ s, γ j * ξ j) = γ j₀ * ξ j₀ + ∑ j ∈ s, γ j * ξ j :=
      Finset.sum_insert hj₀
    rw [hsum_var, hsum_pt]
    -- Apply bivariate addition.
    rw [wickMonomial_add_add]
    -- Each summand: C(k, m) · wickMonomial m (γ_{j₀}²) (γ_{j₀} ξ_{j₀})
    --              · wickMonomial (k - m) (∑_s γ²) (∑_s γ ξ).
    -- Apply homogeneity to the j₀ factor: wickMonomial m (γ_{j₀}²) (γ_{j₀} ξ_{j₀}) =
    --     γ_{j₀}^m · wickMonomial m 1 ξ_{j₀}.
    -- And IH to the s factor.
    have hhom : ∀ m, wickMonomial m ((γ j₀) ^ 2) (γ j₀ * ξ j₀) =
                  (γ j₀) ^ m * wickMonomial m 1 (ξ j₀) := fun m => by
      have := wickMonomial_homogeneity m (γ j₀) 1 (ξ j₀)
      simp at this
      exact this
    -- Substitute homogeneity for the j₀ factor and the IH for the s factor.
    have step1 :
        (∑ m ∈ Finset.range (k + 1),
            (k.choose m : ℝ) * wickMonomial m ((γ j₀) ^ 2) (γ j₀ * ξ j₀) *
              wickMonomial (k - m) (∑ j ∈ s, (γ j) ^ 2) (∑ j ∈ s, γ j * ξ j)) =
        (∑ m ∈ Finset.range (k + 1),
            (k.choose m : ℝ) * ((γ j₀) ^ m * wickMonomial m 1 (ξ j₀)) *
              ∑ α ∈ multiIndicesSupportedIn s (k - m),
                (((k - m).factorial : ℝ) / ∏ j, ((α j).factorial : ℝ)) *
                (∏ j, γ j ^ (α j)) *
                (∏ j, wickMonomial (α j) 1 (ξ j))) := by
      refine Finset.sum_congr rfl (fun m _ => ?_)
      rw [hhom, ih (k - m)]
    rw [step1]
    -- Now we need to prove the LHS = RHS where the RHS is the sum over
    -- multiIndicesSupportedIn (insert j₀ s) k.
    -- Convert the double sum (m, α) into a single sum over α' = update α j₀ m.
    -- Use Finset.sum_bij.
    -- First, push the inner sum outside.
    rw [show ∑ m ∈ Finset.range (k + 1),
              ((k.choose m : ℝ) * ((γ j₀) ^ m * wickMonomial m 1 (ξ j₀))) *
                ∑ α ∈ multiIndicesSupportedIn s (k - m),
                  (((k - m).factorial : ℝ) / ∏ j, ((α j).factorial : ℝ)) *
                  (∏ j, γ j ^ (α j)) *
                  (∏ j, wickMonomial (α j) 1 (ξ j))
            = ∑ m ∈ Finset.range (k + 1),
                ∑ α ∈ multiIndicesSupportedIn s (k - m),
                  ((k.choose m : ℝ) * ((γ j₀) ^ m * wickMonomial m 1 (ξ j₀))) *
                  ((((k - m).factorial : ℝ) / ∏ j, ((α j).factorial : ℝ)) *
                  (∏ j, γ j ^ (α j)) *
                  (∏ j, wickMonomial (α j) 1 (ξ j))) from
      Finset.sum_congr rfl (fun m _ => Finset.mul_sum _ _ _)]
    -- Use Finset.sum_sigma' to convert nested sums to a Σ-sum.
    rw [Finset.sum_sigma' (Finset.range (k + 1))
        (fun m => multiIndicesSupportedIn s (k - m))]
    -- Now define the bijection (m, α) ↦ Function.update α j₀ m.
    refine Finset.sum_bij (fun (mα : Σ _ : ℕ, ι → ℕ) _ => Function.update mα.2 j₀ mα.1)
      ?hi ?h_inj ?h_surj ?h_eq
    · -- hi: image is in multiIndicesSupportedIn (insert j₀ s) k.
      rintro ⟨m, α⟩ hmα
      simp only [Finset.mem_sigma, Finset.mem_range] at hmα
      obtain ⟨hm_lt, hα⟩ := hmα
      simp only [multiIndicesSupportedIn, Finset.mem_filter, Fintype.mem_piFinset,
        Finset.mem_range] at hα ⊢
      obtain ⟨hα_bd, hα_supp, hα_sum⟩ := hα
      refine ⟨?_, ?_, ?_⟩
      · intro j
        rcases eq_or_ne j j₀ with hj | hj
        · subst hj
          rw [Function.update_self]
          omega
        · rw [Function.update_of_ne hj]
          have := hα_bd j
          omega
      · -- support: for j ∉ insert j₀ s, update α j₀ m at j = α j = 0.
        intro j hj
        rw [Finset.mem_insert, not_or] at hj
        obtain ⟨hj_ne, hj_ns⟩ := hj
        rw [Function.update_of_ne hj_ne]
        exact hα_supp j hj_ns
      · -- sum: ∑ (Function.update α j₀ m) = m + ∑ α (with α j₀ replaced) = m + (k - m) = k.
        -- Function.update α j₀ m: at j₀ value is m; elsewhere value is α j.
        -- So ∑_{j ∈ univ} (Function.update α j₀ m) j = m + ∑_{j ∈ univ \ {j₀}} α j
        --   = m + (∑_{j ∈ univ} α j - α j₀) = m + (k - m) - α j₀
        -- We have α j₀ = 0 because j₀ ∉ s (so by hα_supp).
        have hα_j₀ : α j₀ = 0 := hα_supp j₀ hj₀
        have hsum_eq : ∑ j, Function.update α j₀ m j = m + (k - m) := by
          rw [Finset.sum_update_of_mem (Finset.mem_univ j₀)]
          -- Goal: m + ∑ x ∈ univ \ {j₀}, α x = m + (k - m)
          have hsplit_sum :
              ∑ j, α j = α j₀ + ∑ x ∈ Finset.univ \ {j₀}, α x := by
            conv_lhs => rw [show (Finset.univ : Finset ι) = insert j₀ (Finset.univ \ {j₀}) by
              ext x
              simp only [Finset.mem_insert, Finset.mem_sdiff, Finset.mem_univ,
                Finset.mem_singleton, true_and]
              tauto]
            rw [Finset.sum_insert (by simp)]
          have : ∑ x ∈ Finset.univ \ {j₀}, α x = k - m := by
            omega
          rw [this]
        rw [hsum_eq]
        omega
    · -- h_inj: injective.
      rintro ⟨m, α⟩ hmα ⟨m', α'⟩ hmα' heq
      simp only at heq
      -- Function.update α j₀ m = Function.update α' j₀ m' implies m = m' (eval at j₀)
      -- and α j = α' j for j ≠ j₀.
      have hj₀_eq : (Function.update α j₀ m) j₀ = (Function.update α' j₀ m') j₀ := by
        rw [heq]
      simp at hj₀_eq
      have hα_eq : α = α' := by
        funext j
        rcases eq_or_ne j j₀ with hj | hj
        · -- j = j₀: both α j₀ and α' j₀ are 0 (since j₀ ∉ s).
          simp only [Finset.mem_sigma] at hmα hmα'
          have h1 : α j₀ = 0 := by
            have hmem := hmα.2
            simp only [multiIndicesSupportedIn, Finset.mem_filter] at hmem
            exact hmem.2.1 j₀ hj₀
          have h2 : α' j₀ = 0 := by
            have hmem := hmα'.2
            simp only [multiIndicesSupportedIn, Finset.mem_filter] at hmem
            exact hmem.2.1 j₀ hj₀
          rw [hj, h1, h2]
        · have hcongr := congr_fun heq j
          rw [Function.update_of_ne hj, Function.update_of_ne hj] at hcongr
          exact hcongr
      simp [hj₀_eq, hα_eq]
    · -- h_surj: every α' in multiIndicesSupportedIn (insert j₀ s) k arises.
      intro α' hα'
      simp only [multiIndicesSupportedIn, Finset.mem_filter, Fintype.mem_piFinset,
        Finset.mem_range] at hα'
      obtain ⟨hbd, hsupp, hsum⟩ := hα'
      -- Take m = α' j₀ and α = Function.update α' j₀ 0.
      refine ⟨⟨α' j₀, Function.update α' j₀ 0⟩, ?_, ?_⟩
      · simp only [Finset.mem_sigma, Finset.mem_range]
        refine ⟨?_, ?_⟩
        · -- α' j₀ < k + 1
          have := hbd j₀
          omega
        · simp only [multiIndicesSupportedIn, Finset.mem_filter, Fintype.mem_piFinset,
            Finset.mem_range]
          -- Helper: for j ≠ j₀, α' j ≤ ∑_{x ≠ j₀} α' x = k - α' j₀.
          have hsplit_sum :
              ∑ j, α' j = α' j₀ + ∑ x ∈ Finset.univ \ {j₀}, α' x := by
            conv_lhs => rw [show (Finset.univ : Finset ι) = insert j₀ (Finset.univ \ {j₀}) by
              ext x
              simp only [Finset.mem_insert, Finset.mem_sdiff, Finset.mem_univ,
                Finset.mem_singleton, true_and]
              tauto]
            rw [Finset.sum_insert (by simp)]
          have hsum_rest : ∑ x ∈ Finset.univ \ {j₀}, α' x = k - α' j₀ := by omega
          have hbd_rest : ∀ j ≠ j₀, α' j ≤ k - α' j₀ := by
            intro j hj
            have hmem : j ∈ Finset.univ \ {j₀} := by
              simp [hj]
            calc α' j ≤ ∑ x ∈ Finset.univ \ {j₀}, α' x :=
                    Finset.single_le_sum (f := α') (s := Finset.univ \ {j₀})
                      (fun _ _ => Nat.zero_le _) hmem
              _ = k - α' j₀ := hsum_rest
          refine ⟨?_, ?_, ?_⟩
          · intro j
            rcases eq_or_ne j j₀ with hj | hj
            · subst hj; rw [Function.update_self]; omega
            · rw [Function.update_of_ne hj]
              have := hbd_rest j hj
              omega
          · intro j hj
            rcases eq_or_ne j j₀ with hj' | hj'
            · subst hj'; rw [Function.update_self]
            · rw [Function.update_of_ne hj']
              apply hsupp
              rw [Finset.mem_insert, not_or]
              exact ⟨hj', hj⟩
          · -- ∑ (Function.update α' j₀ 0) = ∑ α' - α' j₀ = k - α' j₀.
            rw [Finset.sum_update_of_mem (Finset.mem_univ j₀)]
            rw [hsum_rest]
            ring
      · -- Function.update (Function.update α' j₀ 0) j₀ (α' j₀) = α'.
        dsimp only
        funext j
        rcases eq_or_ne j j₀ with hj | hj
        · subst hj; rw [Function.update_self]
        · rw [Function.update_of_ne hj, Function.update_of_ne hj]
    · -- h_eq: the values agree.
      rintro ⟨m, α⟩ hmα
      simp only at *
      simp only [Finset.mem_sigma, Finset.mem_range] at hmα
      obtain ⟨hm_lt, hα⟩ := hmα
      simp only [multiIndicesSupportedIn, Finset.mem_filter] at hα
      obtain ⟨_, hα_supp, hα_sum⟩ := hα
      have hα_j₀ : α j₀ = 0 := hα_supp j₀ hj₀
      -- Show: C(k, m) · γ_{j₀}^m · W_m 1 ξ_{j₀} · (k-m)!/∏α_j! · ∏ γ^α · ∏ W α 1 ξ
      --     = k!/∏ (update α j₀ m)_j! · ∏ γ^(update α j₀ m) · ∏ W (update α j₀ m) 1 ξ.
      -- Use:
      -- (1) (update α j₀ m) j₀ = m, (update α j₀ m) j = α j for j ≠ j₀.
      -- (2) ∏_j (update α j₀ m)_j! = m! · ∏_{j ≠ j₀} α j!
      --                              = m! · (∏ α_j!) / α_{j₀}! = m! · ∏ α_j!  (since α_{j₀} = 0, factorial = 1).
      -- (3) C(k, m) = k! / (m! · (k - m)!), so C(k, m) · (k-m)!/∏α_j! = k!/(m! ∏α_j!) = k!/∏(update)_j!.
      -- (4) Similarly for the γ and W products: split off j₀ and combine.
      -- Helper: split ∏ over `univ` into the j₀ term times ∏ over `univ \ {j₀}`.
      -- We use this for the three update-quantities below.  Specialised to ℝ
      -- to avoid universe-polymorphism issues.
      have hsplit : ∀ (f : ι → ℝ),
          ∏ j, f j = f j₀ * ∏ j ∈ Finset.univ \ {j₀}, f j := by
        intro f
        conv_lhs => rw [show (Finset.univ : Finset ι) = insert j₀ (Finset.univ \ {j₀}) by
          ext x
          simp only [Finset.mem_insert, Finset.mem_sdiff, Finset.mem_univ,
            Finset.mem_singleton, true_and]
          tauto]
        rw [Finset.prod_insert (by simp)]
      -- Now rewrite the three products.
      have hupdate_factorial_R :
          (∏ j, ((Function.update α j₀ m j).factorial : ℝ)) =
            (m.factorial : ℝ) * ∏ j, ((α j).factorial : ℝ) := by
        rw [hsplit (fun j => ((Function.update α j₀ m j).factorial : ℝ))]
        rw [hsplit (fun j => ((α j).factorial : ℝ))]
        rw [Function.update_self, hα_j₀, Nat.factorial_zero, Nat.cast_one]
        rw [show ∏ j ∈ Finset.univ \ {j₀},
              ((Function.update α j₀ m j).factorial : ℝ) =
            ∏ j ∈ Finset.univ \ {j₀}, ((α j).factorial : ℝ) by
          refine Finset.prod_congr rfl (fun j hj => ?_)
          rw [Finset.mem_sdiff, Finset.mem_singleton] at hj
          rw [Function.update_of_ne hj.2]]
        ring
      have hupdate_gamma :
          (∏ j, γ j ^ (Function.update α j₀ m j)) = γ j₀ ^ m * ∏ j, γ j ^ α j := by
        rw [hsplit (fun j => γ j ^ (Function.update α j₀ m j))]
        rw [hsplit (fun j => γ j ^ α j)]
        rw [Function.update_self, hα_j₀, pow_zero]
        rw [show γ j₀ ^ m * ∏ j ∈ Finset.univ \ {j₀}, γ j ^ Function.update α j₀ m j =
              γ j₀ ^ m * ∏ j ∈ Finset.univ \ {j₀}, γ j ^ α j by
          congr 1
          refine Finset.prod_congr rfl (fun j hj => ?_)
          rw [Finset.mem_sdiff, Finset.mem_singleton] at hj
          rw [Function.update_of_ne hj.2]]
        ring
      have hupdate_W :
          (∏ j, wickMonomial (Function.update α j₀ m j) 1 (ξ j)) =
            wickMonomial m 1 (ξ j₀) * ∏ j, wickMonomial (α j) 1 (ξ j) := by
        rw [hsplit (fun j => wickMonomial (Function.update α j₀ m j) 1 (ξ j))]
        rw [hsplit (fun j => wickMonomial (α j) 1 (ξ j))]
        rw [Function.update_self, hα_j₀, wickMonomial_zero, one_mul]
        rw [show wickMonomial m 1 (ξ j₀) *
              ∏ j ∈ Finset.univ \ {j₀}, wickMonomial (Function.update α j₀ m j) 1 (ξ j) =
            wickMonomial m 1 (ξ j₀) *
              ∏ j ∈ Finset.univ \ {j₀}, wickMonomial (α j) 1 (ξ j) by
          congr 1
          refine Finset.prod_congr rfl (fun j hj => ?_)
          rw [Finset.mem_sdiff, Finset.mem_singleton] at hj
          rw [Function.update_of_ne hj.2]]
      rw [hupdate_factorial_R, hupdate_gamma, hupdate_W]
      -- After substitutions, both sides factor through γ_{j₀}^m · W_m 1 ξ_{j₀} · ∏γ^α · ∏ W α 1 ξ.
      -- The only remaining task is the coefficient identity:
      --   C(k, m) · (k-m)!/∏α! = k! / (m! · ∏α!).
      have hm_le : m ≤ k := Nat.lt_succ_iff.mp hm_lt
      have hpos_m : (0 : ℝ) < (m.factorial : ℝ) := by exact_mod_cast Nat.factorial_pos m
      have hpos_prod : (0 : ℝ) < (∏ j, ((α j).factorial : ℝ)) := by
        apply Finset.prod_pos
        intro j _
        exact_mod_cast Nat.factorial_pos _
      have hpos_km : (0 : ℝ) < ((k - m).factorial : ℝ) := by exact_mod_cast Nat.factorial_pos _
      have hpos_prod_ne : (∏ j, ((α j).factorial : ℝ)) ≠ 0 := ne_of_gt hpos_prod
      have hpos_m_ne : (m.factorial : ℝ) ≠ 0 := ne_of_gt hpos_m
      have hcoef' : (k.choose m : ℝ) * ((k - m).factorial : ℝ) * (m.factorial : ℝ) =
                    (k.factorial : ℝ) := by
        have hkm : k.choose m * (k - m).factorial * m.factorial = k.factorial := by
          have h := Nat.choose_mul_factorial_mul_factorial hm_le
          linarith
        exact_mod_cast hkm
      field_simp
      linear_combination (γ j₀ ^ m * wickMonomial m 1 (ξ j₀) * (∏ j, γ j ^ α j) *
        (∏ j, wickMonomial (α j) 1 (ξ j))) * hcoef'

/-- **Multivariate Wick multinomial expansion** (textbook lemma).

For any finite index set `ι`, any `γ ξ : ι → ℝ` and any `k : ℕ`,

  `wickMonomial k (∑ j, (γ j)²) (∑ j, γ j · ξ j) =
     ∑_{|α|=k} (k! / ∏ α_j!) · (∏ γ_j^{α_j}) · ∏_j wickMonomial α_j 1 ξ_j`,

where the sum is over multi-indices `α : ι → ℕ` of total degree `k`.

Specialisation of `wickMonomial_pow_sum_expansion_aux` to `s = univ`. -/
theorem wickMonomial_pow_sum_expansion
    {ι : Type*} [Fintype ι] [DecidableEq ι]
    (γ ξ : ι → ℝ) (k : ℕ) :
    wickMonomial k (∑ j, (γ j) ^ 2) (∑ j, γ j * ξ j) =
    ∑ α ∈ (Fintype.piFinset (fun _ : ι => Finset.range (k + 1))).filter
        (fun α => ∑ j, α j = k),
      ((k.factorial : ℝ) / ∏ j, ((α j).factorial : ℝ)) *
      (∏ j, γ j ^ (α j)) *
      (∏ j, wickMonomial (α j) 1 (ξ j)) := by
  have h := wickMonomial_pow_sum_expansion_aux γ ξ (Finset.univ : Finset ι) k
  rw [multiIndicesSupportedIn_univ] at h
  exact h

end
