import Mathlib.Data.Matrix.Mul
import Mathlib.Data.ZMod.Basic
import Mathlib.Logic.Equiv.Fin.Rotate
import Mathlib.Tactic

-- ============================================================================
-- McEliece: encryption from an error-correcting code
-- ============================================================================
--
-- McEliece hides a decodable linear code inside a random-looking one.
-- A binary [n, k, t] code encodes k-bit messages as n-bit codewords via a
-- generator matrix G (k×n over 𝔽₂) and corrects up to t errors. The scheme:
--
--   keygen    secret: invertible S (k×k), generator G, permutation P (n×n)
--             public: Ĝ = S·G·P, generator of a scrambled equivalent code
--   encrypt   c = m·Ĝ + e,   e random of Hamming weight ≤ t
--   decrypt   c·P⁻¹ = (m·S)·G + e·P⁻¹;  e·P⁻¹ still has weight ≤ t, so the
--             secret decoder recovers m·S, and multiplying by S⁻¹ gives m
--
-- Two facts make this work, and both are proved here:
--
--   §1  permuting coordinates preserves Hamming weight, so the error stays
--       within the decoder's budget after the P⁻¹ step
--   §3  decryption correctness is pure linear algebra once a decoder for
--       the secret code is given (the decoder enters as a hypothesis)
--
-- What is **not** proved: that Ĝ hides the structure of G. That is the McEliece
-- assumption (a scrambled Goppa code looks like a random code, and decoding
-- a random linear code is hard), not algebra.
--
-- §4 instantiates everything on the [7,4,3] Hamming code; the theory of that
-- code is developed in Algebra/Code/Hamming.lean.

namespace McEliece

open Matrix

-- ============================================================================
-- Section 1: Hamming weight and permutations
-- ============================================================================

/-- Hamming weight of a binary vector: the number of nonzero coordinates. -/
def weight {n : ℕ} (v : Fin n → ZMod 2) : ℕ := ∑ i, (if v i = 0 then 0 else 1)

-- Permuting coordinates does not change the weight.
theorem weight_comp {n : ℕ} (σ : Equiv.Perm (Fin n)) (v : Fin n → ZMod 2) :
    weight (v ∘ σ) = weight v := by
  unfold weight
  exact Equiv.sum_comp σ (fun l => if v l = 0 then (0 : ℕ) else 1)

-- ============================================================================
-- Section 2: Permutation matrices
-- ============================================================================
--
-- A permutation σ of the coordinates acts on row vectors from the right:
-- `v ᵥ* permMatrix σ = v ∘ σ⁻¹`. The inverse of a permutation matrix is the
-- matrix of the inverse permutation.

/-- The permutation matrix of `σ`: entry (i, j) is 1 iff σ i = j. -/
def permMatrix {n : ℕ} (σ : Equiv.Perm (Fin n)) : Matrix (Fin n) (Fin n) (ZMod 2) :=
  (1 : Matrix (Fin n) (Fin n) (ZMod 2)).submatrix σ id

theorem vecMul_permMatrix {n : ℕ} (σ : Equiv.Perm (Fin n)) (v : Fin n → ZMod 2) :
    v ᵥ* permMatrix σ = v ∘ σ.symm := by
  ext j
  simp only [vecMul, dotProduct, permMatrix, submatrix_apply, one_apply, id_eq]
  rw [Finset.sum_eq_single (σ.symm j) _ (fun h => (h (Finset.mem_univ _)).elim)]
  · rw [Equiv.apply_symm_apply, if_pos rfl, mul_one]
    rfl
  · intro i _ hi
    rw [if_neg (mt (Equiv.apply_eq_iff_eq_symm_apply σ).mp hi), mul_zero]

theorem permMatrix_mul {n : ℕ} (σ τ : Equiv.Perm (Fin n)) :
    permMatrix σ * permMatrix τ = permMatrix (σ.trans τ) := by
  ext i j
  simp only [mul_apply, permMatrix, submatrix_apply, one_apply, id_eq]
  rw [Finset.sum_eq_single (σ i) _ (fun h => (h (Finset.mem_univ _)).elim)]
  · rw [if_pos rfl, one_mul, Equiv.trans_apply]
  · intro l _ hl
    rw [if_neg (Ne.symm hl), zero_mul]

theorem permMatrix_refl {n : ℕ} : permMatrix (Equiv.refl (Fin n)) = 1 :=
  Matrix.submatrix_id_id 1

-- The decryption factor P⁻¹ is a permutation matrix again, and P·P⁻¹ = 1.
theorem permMatrix_mul_symm {n : ℕ} (σ : Equiv.Perm (Fin n)) :
    permMatrix σ * permMatrix σ.symm = 1 := by
  rw [permMatrix_mul, Equiv.self_trans_symm, permMatrix_refl]

-- ============================================================================
-- Section 3: The scheme and its correctness
-- ============================================================================

section Abstract

variable {k n t : ℕ}
  (S Sinv : Matrix (Fin k) (Fin k) (ZMod 2))
  (G : Matrix (Fin k) (Fin n) (ZMod 2))
  (σ : Equiv.Perm (Fin n))

/-- Encryption: encode with the public generator `S * G * P`, then add a
random error of weight at most `t`. -/
def encrypt (m : Fin k → ZMod 2) (e : Fin n → ZMod 2) : Fin n → ZMod 2 :=
  m ᵥ* (S * G * permMatrix σ) + e

/-- Decryption: undo the permutation, decode with the secret code's decoder,
then undo the scrambling `S`. -/
def decrypt (decode : (Fin n → ZMod 2) → Fin k → ZMod 2) (c : Fin n → ZMod 2) :
    Fin k → ZMod 2 :=
  decode (c ᵥ* permMatrix σ.symm) ᵥ* Sinv

-- Correctness: given a decoder for the secret code that tolerates errors of
-- weight ≤ t, decryption recovers the message. The decoder hypothesis does
-- all the coding theory; what remains is one chain of matrix identities.
--
--   decrypt (encrypt m e)
--   = decode ((m·(S·G·P) + e) · P⁻¹) · S⁻¹        unfold decrypt, encrypt
--   = decode (m·S·G·(P·P⁻¹) + e·P⁻¹) · S⁻¹        add_vecMul, vecMul_vecMul
--   = decode (m·S·G + e·P⁻¹) · S⁻¹                permMatrix_mul_symm
--   = (m·S) · S⁻¹                                 hdecode, weight_comp
--   = m                                           hS, vecMul_one
theorem decrypt_correct
    (decode : (Fin n → ZMod 2) → Fin k → ZMod 2)
    (hS : S * Sinv = 1)
    (hdecode : ∀ x e, weight e ≤ t → decode (x ᵥ* G + e) = x)
    (m : Fin k → ZMod 2)
    (e : Fin n → ZMod 2)
    (he : weight e ≤ t)
  :
    decrypt Sinv σ decode (encrypt S G σ m e) = m := by
  -- The error after undoing P is a permutation of e, so still small.
  have hw : weight (e ∘ σ) ≤ t := by rwa [weight_comp]
  unfold decrypt encrypt
  rw [add_vecMul, vecMul_vecMul, vecMul_permMatrix, Equiv.symm_symm,
    Matrix.mul_assoc (S * G), permMatrix_mul_symm, Matrix.mul_one, ← vecMul_vecMul,
    hdecode _ _ hw, vecMul_vecMul, hS, vecMul_one]

end Abstract

end McEliece

-- ============================================================================
-- Section 4: A concrete instance on the [7,4,3] Hamming code
-- ============================================================================
--
-- The code theory is developed in Algebra/Code/Hamming.lean; here we repeat
-- just what the scheme needs (generator, parity-check matrix, syndrome
-- decoder) to wire it into the McEliece scheme.  Correctness is checked
-- exhaustively by native_decide.

namespace McEliece.Example

open Matrix McEliece

/-- Generator of the [7,4,3] Hamming code in systematic form [I₄ | A]. -/
def G : Matrix (Fin 4) (Fin 7) (ZMod 2) :=
  !![1, 0, 0, 0, 1, 1, 0;
     0, 1, 0, 0, 0, 1, 1;
     0, 0, 1, 0, 1, 1, 1;
     0, 0, 0, 1, 1, 0, 1]

/-- Parity-check matrix H = [Aᵀ | I₃]. -/
def H : Matrix (Fin 3) (Fin 7) (ZMod 2) :=
  !![1, 0, 1, 1, 1, 0, 0;
     1, 1, 1, 0, 0, 1, 0;
     0, 1, 1, 1, 0, 0, 1]

/-- The syndrome of a received word. -/
def syndrome (w : Fin 7 → ZMod 2) : Fin 3 → ZMod 2 := w ᵥ* Hᵀ

/-- Syndrome decoding: if the syndrome matches column j of H, flip bit j;
then read off the first 4 (systematic) coordinates. -/
def decode (w : Fin 7 → ZMod 2) : Fin 4 → ZMod 2 :=
  let w' := match (List.finRange 7).find? (fun j => (fun i => H i j) = syndrome w) with
    | none => w
    | some j => Function.update w j (w j + 1)
  fun i => w' (Fin.castLE (by decide) i)

/-- Scrambling matrix (secret), unitriangular hence invertible over 𝔽₂. -/
def S : Matrix (Fin 4) (Fin 4) (ZMod 2) :=
  !![1, 1, 0, 1;
     0, 1, 1, 0;
     0, 0, 1, 1;
     0, 0, 0, 1]

/-- Its inverse. -/
def Sinv : Matrix (Fin 4) (Fin 4) (ZMod 2) :=
  !![1, 1, 1, 0;
     0, 1, 1, 1;
     0, 0, 1, 1;
     0, 0, 0, 1]

example : S * Sinv = 1 := by native_decide
example : Sinv * S = 1 := by native_decide

/-- Coordinate permutation (secret): the cyclic rotation of the 7 positions. -/
def σ : Equiv.Perm (Fin 7) := finRotate 7

/-- The public key: the scrambled generator Ĝ = S·G·P. -/
def pubG : Matrix (Fin 4) (Fin 7) (ZMod 2) := S * G * permMatrix σ

/-- Encrypt with the public key, as an outsider would. -/
def enc (m : Fin 4 → ZMod 2) (e : Fin 7 → ZMod 2) : Fin 7 → ZMod 2 :=
  m ᵥ* pubG + e

/-- Decrypt with the secret S⁻¹, P⁻¹ and the syndrome decoder. -/
def dec (c : Fin 7 → ZMod 2) : Fin 4 → ZMod 2 :=
  decrypt Sinv σ decode c

-- Round trip: the syndrome decoder corrects any single error, so the full
-- McEliece decryption recovers the original message.
theorem dec_enc (m : Fin 4 → ZMod 2) (e : Fin 7 → ZMod 2) (he : weight e ≤ 1) :
    dec (enc m e) = m := by
  have hdecode : ∀ (x : Fin 4 → ZMod 2) (e : Fin 7 → ZMod 2), weight e ≤ 1 → decode (x ᵥ* G + e) = x := by
    intro x e he
    revert x e
    native_decide
  exact decrypt_correct S Sinv G σ decode (by native_decide) hdecode m e he

end McEliece.Example
