import Init.Data.Nat.Lemmas
import Init.Prelude
import Mathlib.Data.Nat.Basic
import Mathlib.Data.Nat.Dist
import Mathlib.Data.Set.Basic
import Mathlib.Data.Set.Defs
import Mathlib.Data.Set.Image
import Mathlib.Data.Set.Insert
import Mathlib.Data.Set.Lattice
import Mathlib.Order.Closure
import Mathlib.Order.Lattice
import Mathlib.Tactic.ApplyAt
import Mathlib.Tactic.SimpIntro
import Mathlib.Tactic.NthRewrite

-- Keys are integers
abbrev Key := Nat

-- Define the inverse of a symmetric key
class InvKey where
  invKey : Key → Key
  all_symmetric : Bool
  invKey_spec : ∀ K : Key,  invKey (invKey K) = K
  invKey_symmetric : all_symmetric → invKey = id

open InvKey

@[grind]
def symKeys [InvKey] : Set Key := { K | invKey K = K }

-- Define the datatype for agents
@[grind]
inductive Agent
  | Server : Agent
  | Friend : Nat → Agent
  | Spy : Agent

@[grind]
inductive Msg
  | Agent : Agent → Msg -- Agent names
  | Number : Nat → Msg -- Ordinary integers, timestamps, ...
  | Nonce : Nat → Msg -- Unguessable nonces
  | Key : Key → Msg -- Crypto keys
  | Hash : Msg → Msg -- Hashing
  | MPair : Msg → Msg → Msg -- Compound messages
  | Crypt : Key → Msg → Msg -- Encryption, public- or shared-key

namespace Msg
-- Define concrete syntax for message tuples
notation "⦃" x ", " y "⦄" => MPair x y
notation "⦃" x ", " y ", " z "⦄" => MPair x (MPair y z)
end Msg

open Msg
open Agent

-- Define HPair
def HPair (X Y : Msg) : Msg :=
  ⦃Hash ⦃X, Y⦄, Y⦄

notation "⦃" x ", " y "⦄ₕ" => HPair x y

-- Define keysFor
def keysFor [InvKey] (H : Set Msg) : Set Key :=
  invKey '' { K | ∃ X, Crypt K X ∈ H }

-- Define the inductive set `parts`
@[grind]
inductive parts (H : Set Msg) : Set Msg
  | inj {X : Msg} : X ∈ H → parts H X
  | fst {X Y : Msg} : ⦃X, Y⦄ ∈ parts H → parts H X
  | snd {X Y : Msg} : ⦃X, Y⦄ ∈ parts H → parts H Y
  | body {K : Key} {X : Msg} : Crypt K X ∈ parts H → parts H X

-- Monotonicity lemmas

/--
An `apply` tactic that also calls the assumptions
-/
syntax "aapply" term  : tactic
macro_rules
  |`(tactic| aapply $l:term) => `(tactic| apply $l <;> try assumption)

@[simp]
lemma parts_mono: Monotone parts := by
  intro _ _ h _ a
  induction a
  · apply parts.inj; aapply h
  · aapply parts.fst
  · aapply parts.snd
  · aapply parts.body

-- Equations for injective constructors
@[simp]
lemma Friend_image_eq {A : Set Nat} {x : Nat} :
  ((Friend x) ∈ Friend '' A) = (x ∈ A) := by
  simp

@[simp]
lemma Key_image_eq {A : Set Key} {x : Key} :
  (Key x ∈ Key '' A) = (x ∈ A) := by
  simp

@[simp]
lemma Nonce_Key_image_eq {A : Set Key} {x : Nat} :
  (Nonce x ∉ Key '' A) := by
  simp

-- Lemma: Inverse of keys
@[simp]
lemma invKey_eq (K K' : Key) [InvKey] : (invKey K = invKey K') ↔ (K = K') := by
  apply Iff.intro
  case mp =>
    intro h
    rw [←invKey_spec K, ←invKey_spec K', h]
  case mpr =>
    intro h
    rw [h]

-- Lemmas for the `keysFor` operator
@[simp]
lemma keysFor_empty [InvKey] : keysFor ∅ = ∅ := by
  simp[keysFor]

@[simp]
lemma keysFor_union (H H' : Set Msg) [InvKey] : keysFor (H ∪ H') = keysFor H ∪ keysFor H' := by
  simp[keysFor]
  ext
  constructor
  · intro h; simp_all; grind
  · intro h; simp_all; grind

-- Monotonicity
lemma keysFor_mono [InvKey] : Monotone keysFor := by
  simp_intro _ _ sub _ h
  rcases h with ⟨K, ⟨⟨X, _⟩, _⟩⟩ ; exists K; apply And.intro
  · exists X; aapply sub
  · assumption

-- Lemmas for `keysFor` with specific message types
@[simp]
lemma keysFor_insert_Agent (A : Agent) (H : Set Msg) [InvKey] :
    keysFor (insert (Agent A) H) = keysFor H := by
  simp[keysFor]

@[simp]
lemma keysFor_insert_Nonce (N : Nat) (H : Set Msg) [InvKey] :
    keysFor (insert (Nonce N) H) = keysFor H := by
  simp[keysFor]

@[simp]
lemma keysFor_insert_Number (N : Nat) (H : Set Msg) [InvKey] :
    keysFor (insert (Msg.Hash (Nonce N)) H) = keysFor H := by
  simp[keysFor]

@[simp]
lemma keysFor_insert_Key (K : Key) (H : Set Msg) [InvKey] :
    keysFor (insert (Key K) H) = keysFor H := by
  simp[keysFor]

@[simp]
lemma keysFor_insert_Hash (X : Msg) (H : Set Msg) [InvKey] :
    keysFor (insert (Hash X) H) = keysFor H := by
  simp[keysFor]

@[simp]
lemma keysFor_insert_MPair (X Y : Msg) (H : Set Msg) [InvKey] :
    keysFor (insert ⦃X, Y⦄ H) = keysFor H := by
  simp[keysFor]

@[simp]
lemma keysFor_insert_Crypt (K : Key) (X : Msg) (H : Set Msg) [InvKey] :
    keysFor (insert (Crypt K X) H) = insert (invKey K) (keysFor H) := by
  simp[insert,Set.insert,keysFor]
  ext
  grind

@[simp]
lemma keysFor_image_Key (E : Set Key) [InvKey] : keysFor (Key '' E) = ∅ := by
  simp[keysFor]

lemma Crypt_imp_invKey_keysFor {K : Key} {X : Msg} {H : Set Msg} [InvKey] :
    Crypt K X ∈ H → invKey K ∈ keysFor H := by
  intro h
  simp[invKey_eq,keysFor]
  exact ⟨_, h⟩
  
-- MPair_parts lemma
lemma MPair_parts {H : Set Msg} {X Y : Msg} { P : Prop} :
  ⦃X, Y⦄ ∈ parts H → (parts H X  → parts H Y → P) → P :=
by
  grind

-- parts_increasing lemma
lemma parts_increasing {H : Set Msg} : H ⊆ parts H :=
  λ _ hx => parts.inj hx

-- parts_empty_aux lemma
lemma parts_empty_aux {X : Msg} : parts ∅ X → False :=
by
  intro h
  induction h <;> assumption

-- parts_empty lemma
@[simp]
lemma parts_empty : parts ∅ = ∅ :=
  Set.eq_empty_of_forall_notMem @parts_empty_aux

-- parts_emptyE lemma
lemma parts_emptyE {X : Msg} {P: Prop} : X ∈ parts ∅ → P :=
  λ h => False.elim (parts_empty_aux h)

-- parts_singleton lemma
lemma parts_singleton {H : Set Msg} {X : Msg} :
  X ∈ parts H → ∃ Y ∈ H, X ∈ parts {Y} :=
by
  intro h
  induction h with
  | inj h => exact ⟨_, h, parts.inj (Set.mem_singleton _)⟩
  | fst _ ih => rcases ih with ⟨Z, ⟨l, r⟩⟩; exists Z; and_intros; apply l; apply parts.fst; exact r
  | snd _ ih => rcases ih with ⟨Z, ⟨l, r⟩⟩; exists Z; and_intros; apply l; apply parts.snd; exact r
  | body _ ih => rcases ih with ⟨Z, ⟨l, r⟩⟩; exists Z; and_intros; apply l; apply parts.body; exact r
  
-- parts_union lemma
@[simp]
lemma parts_union {G H : Set Msg} : parts (G ∪ H) = parts G ∪ parts H :=
by
  apply Set.ext
  intro X
  constructor
  · intro h
    induction h with
    | inj h => cases h with
      | inl hG => exact Or.inl (parts.inj hG)
      | inr hH => exact Or.inr (parts.inj hH)
    | fst h ih => cases ih with
      | inl hG => exact Or.inl (parts.fst hG)
      | inr hH => exact Or.inr (parts.fst hH)
    | snd h ih => cases ih with
      | inl hG => exact Or.inl (parts.snd hG)
      | inr hH => exact Or.inr (parts.snd hH)
    | body h ih => cases ih with
      | inl hG => exact Or.inl (parts.body hG)
      | inr hH => exact Or.inr (parts.body hH)
  · intro h
    cases h with
    | inl hG => induction hG with
      | inj hG => exact parts.inj (Or.inl hG)
      | fst h ih => exact parts.fst ih
      | snd h ih => exact parts.snd ih
      | body h ih => exact parts.body ih
    | inr hH => induction hH with
      | inj hH => exact parts.inj (Or.inr hH)
      | fst h ih => exact parts.fst ih
      | snd h ih => exact parts.snd ih
      | body h ih => exact parts.body ih

-- parts_insert lemma
@[simp]
lemma parts_insert {X : Msg} {H : Set Msg} :
  parts (insert X H) = parts {X} ∪ parts H :=
by
  rw [Set.insert_eq, parts_union]

-- parts_insert2 lemma
@[simp]
lemma parts_insert2 {X Y : Msg} {H : Set Msg} :
  parts (insert X (insert Y H)) = parts {X} ∪ parts {Y} ∪ parts H :=
by
rw [Set.insert_eq, Set.insert_eq, parts_union, parts_union]
grind

-- parts_image lemma
@[simp]
lemma parts_image {f : Msg → Msg} {A : Set Msg} :
  parts (f '' A) = ⋃ x ∈ A, parts {f x} :=
by
  apply Set.ext
  intro X
  constructor
  · intro h
    induction h with
    | inj h => rcases h with ⟨a, ha, rfl⟩; exact Set.mem_biUnion ha (parts.inj (Set.mem_singleton _))
    | fst _ ih => simp_all; rcases ih with ⟨W, ⟨_, r⟩⟩; apply parts.fst at r; exists W;
    | snd _ ih => simp_all; rcases ih with ⟨W, ⟨_, r⟩⟩; apply parts.snd at r; exists W;
    | body _ ih => simp_all; rcases ih with ⟨K, ⟨_, r⟩⟩; apply parts.body at r; exists K;
  · intro h
    simp_all
    rcases h with ⟨W, ⟨_, r⟩⟩; induction r with
      | inj a => simp_all; apply parts.inj; exists W;
      | fst _ ih => exact parts.fst ih
      | snd _ ih => exact parts.snd ih
      | body _ ih => exact parts.body ih

-- parts_insert_subset lemma
lemma parts_insert_subset {X : Msg} {H : Set Msg} :
  insert X (parts H) ⊆ parts (insert X H) :=
by
  intro x hx
  cases hx with
  | inl h => simp_all; left; apply parts.inj; simp;
  | inr h => induction h with
    | inj h => exact parts.inj (Set.mem_insert_of_mem _ h)
    | fst h ih => exact parts.fst ih
    | snd h ih => exact parts.snd ih
    | body h ih => exact parts.body ih


-- Idempotence and transitivity lemmas for `parts`
lemma parts_partsD {H : Set Msg} : parts (parts H) ⊆ parts H :=
by
  intro x h
  induction h
  · assumption
  · aapply parts.fst
  · aapply parts.snd
  · aapply parts.body

abbrev partsClosureOperator : ClosureOperator (Set Msg) :=
  ClosureOperator.mk' parts parts_mono @parts_increasing @parts_partsD

@[simp]
lemma parts_idem {H : Set Msg} : parts (parts H) = parts H :=
  by apply partsClosureOperator.idempotent

lemma parts_subset_iff {G H : Set Msg} : (G ⊆ parts H) ↔ (parts G ⊆ parts H) :=
by apply partsClosureOperator.le_closure_iff

lemma parts_trans {G H : Set Msg} {X : Msg} :
  X ∈ parts G → G ⊆ parts H → X ∈ parts H :=
by intro a b; apply parts_mono at b; rw[parts_idem] at b; apply b; apply a;

-- Cut lemma
lemma parts_cut {G H : Set Msg} {X Y : Msg} :
  Y ∈ parts (insert X G) → X ∈ parts H → Y ∈ parts (G ∪ H) :=
by
  intro a b; rw[parts_union]; rw[parts_insert] at a; cases a <;> grind[parts_trans]
  
@[simp]
lemma parts_cut_eq {h : X ∈ parts H}:
  parts (insert X H) = parts H :=
by
  simp[parts_insert];
  apply_rules [parts_subset_iff.mp, Set.singleton_subset_iff.mpr]
  
-- parts_element lemma
lemma parts_element:
  X ∈ parts H ↔ parts {X} ⊆ parts H
:= by
  constructor
  · intro h; apply_rules [ parts_subset_iff.mp, Set.singleton_subset_iff.mpr ]
  · intro h; aapply parts_subset_iff.mpr; simp

@[simp]
lemma parts_insert_Agent {H : Set Msg} {agt : Agent} :
  parts (insert (Agent agt) H) = insert (Agent agt) (parts H) :=
by
  ext
  constructor
  · intro h; rw[parts_insert] at h; cases h with
    | inl h => induction h with
      | inj => simp_all
      | fst _ a_ih => cases a_ih; contradiction; right; aapply parts.fst
      | snd _ a_ih => cases a_ih; contradiction; right; aapply parts.snd
      | body _ a_ih => cases a_ih; contradiction; right; aapply parts.body
    | inr h => right; assumption
  · apply parts_insert_subset
  
@[simp]
lemma parts_singleton_Agent :
  parts {Agent agt} = {Agent agt} := by
  rw[Set.singleton_def, parts_insert_Agent, parts_empty]

@[simp]
lemma parts_insert_Nonce {H : Set Msg} {N : Nat} :
  parts (insert (Nonce N) H) = insert (Nonce N) (parts H) :=
by
  ext
  constructor
  · intro h; rw[parts_insert] at h; cases h with
    | inl h => induction h with
      | inj => simp_all
      | fst _ a_ih => cases a_ih; contradiction; right; aapply parts.fst
      | snd _ a_ih => cases a_ih; contradiction; right; aapply parts.snd
      | body _ a_ih => cases a_ih; contradiction; right; aapply parts.body
    | inr h => right; assumption
  · apply parts_insert_subset

@[simp]
lemma parts_singleton_Nonce :
  parts {Nonce N} = {Nonce N} := by
  rw[Set.singleton_def, parts_insert_Nonce, parts_empty]
  
@[simp]
lemma parts_insert_Number {H : Set Msg} {N : Nat} :
  parts (insert (Number N) H) = insert (Number N) (parts H) :=
by
  ext
  constructor
  · intro h; rw[parts_insert] at h; cases h with
    | inl h => induction h with
      | inj => simp_all
      | fst _ a_ih => cases a_ih; contradiction; right; aapply parts.fst
      | snd _ a_ih => cases a_ih; contradiction; right; aapply parts.snd
      | body _ a_ih => cases a_ih; contradiction; right; aapply parts.body
    | inr h => right; assumption
  · apply parts_insert_subset

@[simp]
lemma parts_singleton_Number :
  parts {Number N} = {Number N} := by
  rw[Set.singleton_def, parts_insert_Number, parts_empty]

@[simp]
lemma parts_insert_Key {H : Set Msg} {K : Key} :
  parts (insert (Key K) H) = insert (Key K) (parts H) :=
by
  ext
  constructor
  · intro h; rw[parts_insert] at h; cases h with
    | inl h => induction h with
      | inj => simp_all
      | fst _ a_ih => cases a_ih; contradiction; right; aapply parts.fst
      | snd _ a_ih => cases a_ih; contradiction; right; aapply parts.snd
      | body _ a_ih => cases a_ih; contradiction; right; aapply parts.body
    | inr h => right; assumption
  · apply parts_insert_subset

@[simp]
lemma parts_singleton_Key :
  parts {Key K} = {Key K} := by
  rw[Set.singleton_def, parts_insert_Key, parts_empty]

@[simp]
lemma parts_insert_Hash {H : Set Msg} {X : Msg} :
  parts (insert (Hash X) H) = insert (Hash X) (parts H) :=
by
  ext
  constructor
  · intro h; rw[parts_insert] at h; cases h with
    | inl h => induction h with
      | inj => simp_all
      | fst _ a_ih => cases a_ih; contradiction; right; aapply parts.fst
      | snd _ a_ih => cases a_ih; contradiction; right; aapply parts.snd
      | body _ a_ih => cases a_ih; contradiction; right; aapply parts.body
    | inr h => right; assumption
  · apply parts_insert_subset

@[simp]
lemma parts_singleton_Hash :
  parts {Hash H} = {Hash H} := by
  rw[Set.singleton_def, parts_insert_Hash, parts_empty]

@[simp]
lemma parts_insert_Crypt {H : Set Msg} {K : Key} {X : Msg} :
  parts (insert (Crypt K X) H) = insert (Crypt K X) (parts (insert X H)) :=
by
  ext x
  constructor
  · intro h; rw[parts_insert] at h; cases h with
    | inl h => induction h with
      | inj => left; assumption;
      | fst _ a_ih => cases a_ih; contradiction; right; aapply parts.fst
      | snd _ a_ih => cases a_ih; contradiction; right; aapply parts.snd
      | body _ a_ih => cases a_ih with
        | inl => simp_all; right; left; apply parts_increasing; trivial;
        | inr h => apply parts.body at h; right; exact h;
    | inr => right; simp; right; assumption;
  · intro h; cases h with
    | inl => apply parts.inj; left; assumption
    | inr h => 
      apply (parts_cut (G := H) (H := {Crypt K X}) (X := X) (Y := x)) at h 
      rw[←Set.singleton_union, Set.union_comm]
      apply h
      apply parts.body (K := K) (X := X)
      apply parts.inj
      trivial
      
@[simp]
lemma parts_singleton_Crypt :
  parts {Crypt K X} = {Crypt K X} ∪ parts {X} := by
  rw[
    Set.singleton_def,
    parts_insert_Crypt,
    ←Set.singleton_def,
    Set.insert_union,
    Set.empty_union
  ]

@[simp]
lemma parts_insert_MPair {H: Set Msg} {X Y : Msg} :
  parts (insert ⦃X, Y⦄ H) = insert ⦃X, Y⦄ (parts (insert X (insert Y H))) :=
by 
  ext x
  constructor;
  · intro h; rw[parts_insert] at h; cases h with
    | inl h => induction h with
      | inj => simp_all;
      | @fst A B _ a_ih => cases a_ih with
        | inl h => cases h; right; apply parts.inj; left; trivial
        | inr h => right; aapply parts.fst;
      | @snd A B _ a_ih => cases a_ih with
        | inl h => cases h; right; apply parts.inj; right; left; trivial;
        | inr h => right; aapply parts.snd;
      | body _ a_ih => cases a_ih; contradiction; right; aapply parts.body
    | inr h => simp; right; right; right; assumption;
  · intro h; cases h with
    | inl => rw[parts_insert]; left; apply parts.inj; assumption;
    | inr h => simp_all; cases h with
      | inl h => 
        left;
        apply parts_trans ( H := {⦃X, Y⦄}) at h;
        simp at h;
        apply h;
        apply parts.fst;
        apply parts.inj;
        trivial;
      | inr h => cases h with
        | inl h => 
            left;
            apply parts_trans ( H := {⦃X, Y⦄}) at h;
            simp at h;
            apply h;
            apply parts.snd;
            apply parts.inj;
            trivial;
        | inr => right; assumption;

@[simp]
lemma parts_singleton_MPair :
  parts {⦃X, Y⦄} = {⦃X, Y⦄} ∪ parts (insert X {Y}) := by
  rw[
    Set.singleton_def,
    parts_insert_MPair,
    ←Set.singleton_def,
    Set.insert_union,
    Set.empty_union
  ]

@[simp]
lemma parts_image_Key {N : Set Key} : parts (Key '' N) = Key '' N :=
by 
  ext
  constructor
  · intro a; simp; induction a <;> simp_all;
  · simp; intro y _ _; apply parts.inj; simp; exists y;

-- In any message, there is an upper bound N on its greatest nonce.
lemma msg_Nonce_supply {msg : Msg} : ∃ N, ∀ n, N ≤ n → Nonce n ∉ parts {msg} :=
by
  induction msg with
  | Agent a => exists 0;
               have ins_agt := parts_insert_Agent (H := {}) (agt := a);
               simp_all
  | Number a => exists 0;
                have ins_number := parts_insert_Number (H := {}) (N := a);
                simp_all;
  | Nonce n => exists n.succ;
               intro _ _ _;
               have ins_nonce := parts_insert_Nonce (H := {}) (N := n);
               simp_all;
  | Key k => exists 0;
             have ins_key := parts_insert_Key (H := {}) (K := k);
             simp_all;
  | Hash X ih => exists 0;
                 have ins_hash := parts_insert_Hash (H := {}) (X := X);
                 simp_all;
  | MPair X Y ihX ihY =>
    rcases ihX with ⟨wX, hH⟩;
    cases ihY with
    | intro wY hY => exists Nat.max wX wY; intro n h₁ h₂;
                     have ins_mpair := parts_insert_MPair (H := {}) (X := X) (Y := Y);
                     simp_all;
  | Crypt K X ih => rcases ih with ⟨N, h⟩; exists N;
                    have ins_crypt := parts_insert_Crypt (H := {}) (K := K) (X := X);
                    simp_all;

-- Inductive relation "analz"
inductive analz [InvKey] (H : Set Msg) : Set Msg
  | inj {X : Msg} : X ∈ H → analz H X
  | fst {X Y : Msg} : ⦃X, Y⦄ ∈ analz H → analz H X
  | snd {X Y : Msg} : ⦃X, Y⦄ ∈ analz H → analz H Y
  | decrypt {K : Key} {X : Msg} :
      Crypt K X ∈ analz H → Key (invKey K) ∈ analz H → analz H X

-- Monotonicity
lemma analz_mono [InvKey] : Monotone analz :=
by 
  intro _ _ h₁ _ h₂
  induction h₂ with
  | inj h => exact analz.inj (h₁ h)
  | fst h ih => exact analz.fst ih
  | snd h ih => exact analz.snd ih
  | decrypt h₁ h₂ ih₁ ih₂ => exact analz.decrypt ih₁ ih₂

-- Making it safe speeds up proofs
-- @[simp]
lemma MPair_analz {H : Set Msg} {X Y : Msg} {P : Prop} [InvKey] :
  ⦃X, Y⦄ ∈ analz H → (analz H X → analz H Y → P) → P :=
by
  intro h ih
  apply ih
  · apply analz.fst h
  · apply analz.snd h

@[simp]
lemma analz_increasing [InvKey] {H : Set Msg} : H ⊆ analz H :=
  λ _ hx => analz.inj hx

lemma analz_into_parts {H : Set Msg} {X : Msg} [InvKey] : X ∈ analz H → X ∈ parts H :=
by
  intro h
  induction h with
  | inj h => aapply parts.inj
  | fst _ ih => aapply parts.fst
  | snd _ ih => aapply parts.snd
  | decrypt _ _ ih₁ => aapply parts.body

lemma analz_subset_parts {H : Set Msg} [InvKey] : analz H ⊆ parts H :=
  λ _ hx => analz_into_parts hx

@[simp]
lemma analz_parts {H : Set Msg} [InvKey] : analz (parts H) = parts H :=
by
  ext X; constructor
  · intro h; induction h with
    | inj => assumption;
    | fst => aapply parts.fst;
    | snd => aapply parts.snd;
    | decrypt => aapply parts.body;
  · apply analz_increasing;

lemma not_parts_not_analz {H : Set Msg} {X : Msg} [InvKey] :
  X ∉ parts H → X ∉ analz H :=
  λ h₁ h₂ => h₁ (analz_into_parts h₂)

@[simp]
lemma parts_analz {H : Set Msg} [InvKey] : parts (analz H) = parts H :=
  by 
    ext; constructor;
    · intro h; induction h with
      | inj => aapply analz_subset_parts
      | fst => aapply parts.fst
      | snd => aapply parts.snd
      | body => aapply parts.body
    · apply parts_mono; apply analz_increasing;

@[simp]
lemma analz_insertI {X : Msg} {H : Set Msg} [InvKey] :
  insert X (analz H) ⊆ analz (insert X H) :=
by
  intro x hx
  cases hx with
  | inl h => apply analz.inj; left; assumption;
  | inr h => aapply analz_mono (Set.subset_insert _ _)

-- General equational properties

@[simp]
lemma analz_empty [InvKey] : analz ∅ = ∅ :=
by 
  ext; constructor;
  · intro h; induction h <;> contradiction;
  · apply analz_increasing


@[simp]
lemma analz_union {G H : Set Msg} [InvKey] : analz G ∪ analz H ⊆ analz (G ∪ H) :=
  by
    intro x hx
    cases hx with
    | inl hG => aapply analz_mono (Set.subset_union_left)
    | inr hH => aapply analz_mono (Set.subset_union_right)

lemma analz_insert {X : Msg} {H : Set Msg} [InvKey] :
  insert X (analz H) ⊆ analz (insert X H) :=
  by
    intro x hx
    cases hx with
    | inl h => apply analz.inj; left; assumption
    | inr h => aapply analz_mono (Set.subset_insert _ _)

-- Rewrite rules for pulling out atomic messages

@[simp]
lemma analz_insert_Agent {H : Set Msg} {agt : Agent} [InvKey] :
  analz (insert (Agent agt) H) = insert (Agent agt) (analz H) :=
  by
    ext
    constructor
    · intro h
      induction h with
      | inj h => cases h with
        | inl h => exact Or.inl h
        | inr h => exact Or.inr (analz.inj h)
      | fst _ ih => right; cases ih; contradiction; aapply analz.fst;
      | snd _ ih => right; cases ih; contradiction; aapply analz.snd;
      | decrypt _ _ ih₁ ih₂ => right; cases ih₁; contradiction;
                               cases ih₂; contradiction;
                               aapply analz.decrypt;
    · apply analz_insert

@[simp]
lemma analz_insert_Nonce {H : Set Msg} {N : Nat} [InvKey] :
  analz (insert (Nonce N) H) = insert (Nonce N) (analz H) :=
  by
    ext
    constructor
    · intro h
      induction h with
      | inj h => cases h with
        | inl h => exact Or.inl h
        | inr h => exact Or.inr (analz.inj h)
      | fst _ ih => right; cases ih; contradiction; aapply analz.fst;
      | snd _ ih => right; cases ih; contradiction; aapply analz.snd;
      | decrypt _ _ ih₁ ih₂ => right; cases ih₁; contradiction;
                               cases ih₂; contradiction;
                               aapply analz.decrypt;
    · apply analz_insert

@[simp]
lemma analz_insert_Number {H : Set Msg} {N : Nat} [InvKey] :
  analz (insert (Number N) H) = insert (Number N) (analz H) :=
  by
    ext
    constructor
    · intro h
      induction h with
      | inj h => cases h with
        | inl h => exact Or.inl h
        | inr h => exact Or.inr (analz.inj h)
      | fst _ ih => right; cases ih; contradiction; aapply analz.fst;
      | snd _ ih => right; cases ih; contradiction; aapply analz.snd;
      | decrypt _ _ ih₁ ih₂ => right; cases ih₁; contradiction;
                               cases ih₂; contradiction;
                               aapply analz.decrypt;
    · apply analz_insert

@[simp]
lemma analz_insert_Hash {H : Set Msg} {X : Msg} [InvKey] :
  analz (insert (Hash X) H) = insert (Hash X) (analz H) :=
  by
    ext
    constructor
    · intro h
      induction h with
      | inj h => cases h with
        | inl h => exact Or.inl h
        | inr h => exact Or.inr (analz.inj h)
      | fst _ ih => right; cases ih; contradiction; aapply analz.fst;
      | snd _ ih => right; cases ih; contradiction; aapply analz.snd;
      | decrypt _ _ ih₁ ih₂ => right; cases ih₁; contradiction;
                               cases ih₂; contradiction;
                               aapply analz.decrypt;
    · apply analz_insert

@[simp]
lemma analz_insert_Key {H : Set Msg} {K : Key} [InvKey] :
  K ∉ keysFor (analz H) →
  analz (insert (Key K) H) = insert (Key K) (analz H) :=
  by
    intro hK
    ext x
    constructor
    · intro h
      induction h with
      | inj h => cases h with
        | inl h => exact Or.inl h
        | inr h => exact Or.inr (analz.inj h)
      | fst _ ih => right; cases ih; contradiction; aapply analz.fst;
      | snd _ ih => right; cases ih; contradiction; aapply analz.snd;
      | @decrypt _ _ _ _ ih₁ ih₂ =>
      right
      cases ih₂ with
      | inl h => simp at h; simp at ih₁; apply Crypt_imp_invKey_keysFor at ih₁
                 rw[h] at ih₁; contradiction;
      | inr => cases ih₁; simp_all; aapply analz.decrypt
    · apply analz_insert

@[simp]
lemma analz_insert_MPair {H : Set Msg} {X Y : Msg} [InvKey] :
  analz (insert ⦃X, Y⦄ H) = insert ⦃X, Y⦄ (analz (insert X (insert Y H))) :=
  by
    ext
    constructor
    · intro h
      induction h with
      | inj h => cases h with
        | inl h => exact Or.inl h
        | inr => right; apply analz.inj; right; right; assumption
      | fst _ ih => right; cases ih
                    · simp_all; apply analz.inj; left; trivial
                    · aapply analz.fst
      | snd _ ih => right; cases ih
                    · simp_all; apply analz.inj; right; left; trivial
                    · aapply analz.snd
      | decrypt h₁ h₂ ih₁ ih₂ =>
      right
      cases ih₂; contradiction
      cases ih₁; contradiction
      aapply analz.decrypt
    · intro h
      cases h with
      | inl => apply analz.inj; left; trivial
      | inr h => induction h with
        | inj a => cases a with
          | inl h => apply analz.fst; apply analz.inj; left; rw[h]
          | inr h => cases h with
            | inl h => apply analz.snd; apply analz.inj; left; rw[h]
            | inr => apply analz.inj; right; assumption
        | fst => aapply analz.fst
        | snd => aapply analz.snd
        | decrypt => aapply analz.decrypt

@[simp]
lemma analz_insert_Decrypt [InvKey]
  { h : Key (invKey K) ∈ analz H  } :
  analz (insert (Crypt K X) H) = insert (Crypt K X) (analz (insert X H)) :=
by
  ext
  constructor
  · intro h
    induction h with
    | inj h => cases h with
      | inl => left; assumption;
      | inr => right; apply analz.inj; right; assumption
    | fst _ ih => right; cases ih; contradiction; aapply analz.fst
    | snd _ ih => right; cases ih; contradiction; aapply analz.snd
    | decrypt _ _ ih₁ ih₂ => cases ih₁ with
      | inl h => right; apply analz.inj; left; simp_all
      | inr => right; cases ih₂; contradiction; aapply analz.decrypt
  · intro h
    cases h with
    | inl h => apply analz.inj; left; assumption
    | inr h => induction h with
      | inj a => cases a with
        | inl h => apply analz.decrypt; apply analz.inj; left; rw[h]
                   apply analz_mono; apply Set.subset_insert; assumption
        | inr => apply analz.inj; right; assumption
      | fst => aapply analz.fst
      | snd => aapply analz.snd
      | decrypt => aapply analz.decrypt

@[simp]
lemma analz_Crypt [InvKey]
  { h : (Key (invKey K) ∉ analz H)  } :
  (analz (insert (Crypt K X) H) = insert (Crypt K X) (analz H)) :=
by
  ext
  constructor
  · intro a; induction a with
    | inj a => cases a; left; assumption; right; aapply analz.inj
    | fst _ a_ih => cases a_ih; contradiction; right; aapply analz.fst
    | snd _ a_ih => cases a_ih; contradiction; right; aapply analz.snd
    | decrypt h₁ h₂ ih₁ ih₂ =>
    cases ih₂; contradiction 
    cases ih₁; simp_all; right; aapply analz.decrypt
  · intro a; cases a with
    | inl => apply analz.inj; left; assumption
    | inr => apply analz_mono; apply Set.subset_insert; assumption
          
-- This rule supposes "for the sake of argument" that we have the key.
lemma analz_insert_Crypt_subset {H : Set Msg} {K : Key} {X : Msg} [InvKey] :
  analz (insert (Crypt K X) H) ⊆ insert (Crypt K X) (analz (insert X H)) :=
by
  intro Y h
  induction h with
  | inj h => cases h with
    | inl => left; assumption
    | inr => right; apply analz.inj; right; assumption
  | fst _ ih => right; cases ih; contradiction; aapply analz.fst
  | snd _ ih => right; cases ih; contradiction; aapply analz.snd
  | decrypt _ _ ih₁ ih₂ =>
  right
  cases ih₂; contradiction
  cases ih₁ with
  | inl => simp_all; apply analz.inj; left; trivial
  | inr => aapply analz.decrypt
  
lemma analz_insert_Crypt_element [InvKey] :
  M ∈ analz (insert (Crypt K X) H) ↔ 
  ((Key (invKey K) ∈ analz H ∧ M ∈ insert (Crypt K X) (analz (insert X H))) ∨
   (Key (invKey K) ∉ analz H ∧ M ∈ insert (Crypt K X) (analz H)))
:= by
  constructor
  · intro h; by_cases invK_in_H : Msg.Key (invKey K) ∈ analz H
    · left; rw[←analz_insert_Decrypt]; aapply And.intro; assumption
    · right; rw[←analz_Crypt]; aapply And.intro; assumption
  · intro h; rcases h with (⟨_, _⟩ | ⟨_, _⟩)
    · rw[analz_insert_Decrypt]; assumption; assumption
    · rw[analz_Crypt]; assumption; assumption

@[simp]
lemma analz_image_Key {N : Set Key} [InvKey] : analz (Key '' N) = Key '' N :=
by
  apply Set.ext
  intro X
  constructor
  · intro h
    induction h <;> simp_all
  · intro h
    rcases h with ⟨k, hk, rfl⟩
    exact analz.inj ⟨k, hk, rfl⟩

-- Idempotence and transitivity

lemma analz_analzD [InvKey] {H : Set Msg} {X : Msg} :
  X ∈ analz (analz H) → X ∈ analz H :=
by
  intro h
  induction h with
  | inj h => exact h
  | fst h ih => exact analz.fst ih
  | snd h ih => exact analz.snd ih
  | decrypt h₁ h₂ ih₁ ih₂ => exact analz.decrypt ih₁ ih₂
 
@[simp]
abbrev analzClosureOperator [InvKey] : ClosureOperator (Set Msg) :=
  ClosureOperator.mk'
  analz (analz_mono)
  (λ x ↦ @analz_increasing _ x)
  (λ x ↦ @analz_analzD _ x)

lemma analz_idem {H : Set Msg} [InvKey] : analz (analz H) = analz H :=
by
  apply analzClosureOperator.idempotent
  

@[simp]
lemma analz_subset_iff {G H : Set Msg} [InvKey] : (G ⊆ analz H) ↔ (analz G ⊆ analz H) :=
  by apply analzClosureOperator.le_closure_iff

lemma analz_trans {G H : Set Msg} {X : Msg} [InvKey] :
  X ∈ analz G → G ⊆ analz H → X ∈ analz H :=
by
  intro hG hGH
  apply analz_mono at hGH; rw [ analz_idem ] at hGH; apply hGH; assumption;

-- Cut; Lemma 2 of Lowe
@[simp]
lemma analz_cut {H : Set Msg} {X Y : Msg} [InvKey] :
  Y ∈ analz (insert X H) → X ∈ analz H → Y ∈ analz H :=
by
  intro hY _
  apply analz_trans; apply hY
  intro a h; cases h; simp_all; aapply analz.inj

-- Simplification of messages involving forwarding of unknown components
@[simp]
lemma analz_insert_eq {H : Set Msg} {X : Msg} [InvKey] :
  X ∈ analz H → analz (insert X H) = analz H :=
by
  intro
  ext
  constructor
  · intro; aapply analz_cut
  · apply analz_mono; apply Set.subset_insert

-- A congruence rule for "analz"
@[simp]
lemma analz_subset_cong {G G' H H' : Set Msg} [InvKey] :
  analz G ⊆ analz G' → analz H ⊆ analz H' → analz (G ∪ H) ⊆ analz (G' ∪ H') :=
by
  intro hG hH
  have a_sub := analz_subset_iff (G := G ∪ H) (H := G' ∪ H')
  apply a_sub.mp
  apply subset_trans (b := analz G ∪ analz H)
  apply Set.union_subset_union (h₁ := @analz_increasing _ (H := G)) (h₂ := @analz_increasing _ (H := H))
  apply subset_trans (b := analz G' ∪ analz H')
  apply Set.union_subset_union (h₁ := hG) (h₂ := hH)
  apply analz_union

lemma analz_cong {G G' H H' : Set Msg} [InvKey] :
  analz G = analz G' → analz H = analz H' → analz (G ∪ H) = analz (G' ∪ H') :=
by
  intro hG hH
  ext
  constructor
  · apply analz_subset_cong; aapply Eq.subset; aapply Eq.subset
  · apply analz_subset_cong; rw[Eq.comm] at hG; aapply Eq.subset; rw[Eq.comm] at hH; aapply Eq.subset


lemma analz_insert_cong {H H' : Set Msg} {X : Msg} [InvKey] :
  analz H = analz H' → analz (insert X H) = analz (insert X H') :=
by
  intro hH
  rw [Set.insert_eq, Set.insert_eq]
  exact analz_cong rfl hH

-- If there are no pairs or encryptions, then analz does nothing
lemma analz_trivial [InvKey] {H : Set Msg} :
  (∀ X Y, ⦃X, Y⦄ ∉ H) → (∀ X K, Crypt K X ∉ H) → analz H = H :=
by
  intro hPairs hCrypts
  apply Set.ext
  intro X
  constructor
  · intro h
    induction h with
    | inj h => exact h
    | fst => simp_all
    | snd => simp_all
    | decrypt h₁ _ _ => simp_all
  · exact analz.inj

-- Inductive relation "synth"

inductive synth [InvKey] (H : Set Msg) : Set Msg
  | inj {X : Msg} : X ∈ H → synth H X
  | agent {agt : Agent} : synth H (Agent agt)
  | number {n : Nat} : synth H (Number n)
  | hash {X : Msg} : synth H X → synth H (Hash X)
  | mpair {X Y : Msg} : synth H X → synth H Y → synth H ⦃X, Y⦄
  | crypt {K : Key} {X : Msg} : synth H X → Key K ∈ H → synth H (Crypt K X)

-- Monotonicity
lemma synth_mono [InvKey] : Monotone synth := by
  intro _ _ h _ hx
  induction hx with
  | inj hG => exact synth.inj (h hG)
  | agent => exact synth.agent
  | number => exact synth.number
  | hash _ ih => exact synth.hash ih
  | mpair _ _ ihX ihY => exact synth.mpair ihX ihY
  | crypt => aapply synth.crypt; aapply h 

-- Simplification rules for `synth`
@[simp]
lemma synth_increasing [InvKey] {H : Set Msg} : H ⊆ synth H :=
  λ _ hx => synth.inj hx

-- Unions
lemma synth_union [InvKey] {G H : Set Msg} : synth G ∪ synth H ⊆ synth (G ∪ H) :=
by
  intro x hx
  cases hx with
  | inl hG => exact synth_mono (Set.subset_union_left) hG
  | inr hH => exact synth_mono (Set.subset_union_right) hH

lemma synth_insert [InvKey] {X : Msg} {H : Set Msg} :
  insert X (synth H) ⊆ synth (insert X H) :=
by
  intro x hx
  cases hx with
  | inl h => apply synth.inj; left; assumption
  | inr h => exact synth_mono (Set.subset_insert _ _) h

-- Idempotence and transitivity
lemma synth_synthD [InvKey] {H : Set Msg} {X : Msg} :
  X ∈ synth (synth H) → X ∈ synth H :=
by
  intro h
  induction h with
  | inj h => exact h
  | agent => exact synth.agent
  | number => exact synth.number
  | hash _ ih => exact synth.hash ih
  | mpair _ _ ihX ihY => exact synth.mpair ihX ihY
  | crypt _ a => cases a; aapply synth.crypt;
  
abbrev synthClosureOperator [InvKey] : ClosureOperator (Set Msg) :=
  ClosureOperator.mk' synth synth_mono 
  (λ x ↦ @synth_increasing _ x)
  (λ x ↦ @synth_synthD _ x)

@[simp]
lemma synth_idem {H : Set Msg} [InvKey] : synth (synth H) = synth H :=
  by apply synthClosureOperator.idempotent

@[simp]
lemma synth_subset_iff [InvKey] {G H : Set Msg} : (G ⊆ synth H) ↔ (synth G ⊆ synth H) :=
by apply synthClosureOperator.le_closure_iff

@[simp]
lemma synth_trans [InvKey] {G H : Set Msg} {X : Msg} :
  X ∈ synth G → G ⊆ synth H → X ∈ synth H :=
by
  intro hG hGH; apply synth_mono at hGH; rw[synth_idem] at hGH; apply hGH; apply hG

-- Cut; Lemma 2 of Lowe
@[simp]
lemma synth_cut [InvKey] {H : Set Msg} {X Y : Msg} :
  Y ∈ synth (insert X H) → X ∈ synth H → Y ∈ synth H :=
by
  intro hY hX; apply synth_trans; apply hY
  intro a h; cases h; simp_all; aapply synth.inj

lemma Crypt_synth_EK [InvKey] :
  (Crypt K X ∈ synth H) ↔
  (Crypt K X ∈ H ∨ ( Key K ∈ H ∧ X ∈ synth H)) :=
  by
  constructor
  · intro h; cases h <;> tauto
  · intro h; cases h
    · aapply synth.inj
    · apply synth.crypt <;> tauto

@[simp]
lemma Crypt_synth_eq [InvKey]
  { hK : Key K ∉ H } :
  (Crypt K X ∈ synth H ↔ Crypt K X ∈ H) :=
by
  constructor
  · intro h; simp[Crypt_synth_EK] at h; tauto
  · intro h; exact synth.inj h
  
@[simp]
lemma keysFor_synth [InvKey] {H : Set Msg} :
  keysFor (synth H) = keysFor H ∪ invKey '' {K | Key K ∈ H} :=
by
  ext K
  simp
  constructor
  · intro h
    rcases h with ⟨Y, ⟨⟨X, h⟩, _⟩⟩;
    cases h with
    | inj h => left; exists Y; apply And.intro; exists X; assumption
    | crypt _ _ => right; exists Y
  · intro h; cases h with
  | inl h =>  cases h with
    | intro Y h =>
    exists Y; rcases h with ⟨⟨X, _⟩, _⟩;
    apply And.intro
    · exists X; aapply synth.inj
    · assumption
  | inr h =>
    rcases h with ⟨Y, ⟨_, _⟩⟩;
    exists Y; apply And.intro
    · exists Number 0; aapply synth.crypt; apply synth.number
    · assumption

@[simp]
lemma Nonce_synth [InvKey] :
  Nonce NA ∈ synth H ↔ Nonce NA ∈ H
:= by
  constructor
  · intro h; cases h; assumption
  · aapply synth.inj
  
@[simp]
lemma Key_synth [InvKey] :
  Key K ∈ synth H ↔ Key K ∈ H
:= by
  constructor
  · intro h; cases h; assumption
  · aapply synth.inj
  
-- Combinations of parts, analz, and synth

@[simp]
lemma parts_synth [InvKey] {H : Set Msg} : parts (synth H) = parts H ∪ synth H :=
by
  apply Set.ext
  intro X
  constructor
  · intro h
    induction h with
    | inj => right; assumption
    | fst _ ih => cases ih with
      | inl => left; aapply parts.fst
      | inr h => cases h
                 · left; apply parts.fst; aapply parts.inj; 
                 · right; assumption
    | snd _ ih => cases ih with
      | inl => left; aapply parts.snd
      | inr h => cases h
                 · left; apply parts.snd; aapply parts.inj; 
                 · right; assumption
    | body h ih => cases ih with
      | inl h => left; aapply parts.body
      | inr h => cases h
                 · left; aapply parts.body; aapply parts.inj
                 · right; assumption
  · intro h
    cases h with
    | inl h =>
    apply parts_mono
    · apply synth_increasing
    · assumption
    | inr h => exact parts.inj h

@[simp]
lemma analz_analz_Un [InvKey] {G H : Set Msg} : analz (analz G ∪ H) = analz (G ∪ H) :=
by
  apply analz_cong
  · exact analz_idem
  · trivial

@[simp]
lemma analz_synth_Un [InvKey] {G H : Set Msg} : analz (synth G ∪ H) = analz (G ∪ H) ∪ synth G :=
by
  ext
  constructor
  · intro h; induction h with
    | inj a => cases a
               · right; assumption
               · left; apply analz.inj; right; assumption
    | fst _ ih =>
    cases ih with
        | inl => left; aapply analz.fst
        | inr h => 
        cases h
        · left; apply analz.fst; apply analz.inj; left; assumption
        · right; assumption
    | snd _ ih => 
    cases ih with
        | inl => left; aapply analz.snd
        | inr h => 
        cases h
        · left; apply analz.snd; apply analz.inj; left; assumption
        · right; assumption
    | decrypt _ _ ih₁ ih₂ =>
    cases ih₂ with
    | inl => cases ih₁ with
      | inl => left; aapply analz.decrypt
      | inr h => cases h
                 · left; aapply analz.decrypt; apply analz.inj; left; assumption
                 · right; assumption
    | inr h => cases h; cases ih₁ with
      | inl => left; aapply analz.decrypt; apply analz.inj; left; assumption
      | inr h => cases h
                 · left; aapply analz.decrypt <;> (apply analz.inj; left; assumption)
                 · right; assumption
  · apply sup_le (a := analz (G ∪ H)) (b := synth G) (c := analz ( synth G ∪ H ))
    · apply analz_mono; apply Set.union_subset_union; apply synth_increasing; trivial
    · apply analz_subset_iff.mpr; apply analz_mono; exact le_sup_left

@[simp]
lemma analz_synth [InvKey] {H : Set Msg} : analz (synth H) = analz H ∪ synth H :=
  by have asu := analz_synth_Un (G := H) (H := ∅); simp_all

-- For reasoning about the Fake rule in traces

lemma parts_insert_subset_Un {G H : Set Msg} {X : Msg} :
  X ∈ G → parts (insert X H) ⊆ parts G ∪ parts H :=
by
  intro hX
  rw [Set.insert_eq, parts_union]
  apply Set.union_subset_union
  · apply parts_mono; simp_all
  · trivial

lemma Fake_parts_insert [InvKey] {H : Set Msg} {X : Msg} :
  X ∈ synth (analz H) → parts (insert X H) ⊆ synth (analz H) ∪ parts H :=
by
  intro hX
  rw [parts_insert]
  apply sup_le
  · apply subset_trans (b := synth (analz H) ∪ parts (analz H))
    · rw [Set.union_comm, ←parts_synth]; apply parts_mono; simp; assumption
    · rw [parts_analz]
  · apply le_sup_of_le_right; trivial

lemma Fake_parts_insert_in_Un [InvKey] {H : Set Msg} {X Z : Msg} :
  Z ∈ parts (insert X H) → X ∈ synth (analz H) → Z ∈ synth (analz H) ∪ parts H :=
by
  intro hZ hX
  exact Set.mem_of_subset_of_mem (Fake_parts_insert hX) hZ

lemma Fake_analz_insert [InvKey] {G H : Set Msg} {X : Msg} :
  X ∈ synth (analz G) → analz (insert X H) ⊆ synth (analz G) ∪ analz (G ∪ H) :=
by
  intro
  apply subset_trans (b := analz (synth (analz G) ∪ H))
  · apply analz_mono; rw[Set.insert_eq]; apply Set.union_subset_union; simp_all; trivial;
  · rw[analz_synth_Un, Set.union_comm, analz_analz_Un]

@[simp]
lemma analz_conj_parts [InvKey] {H : Set Msg} {X : Msg} :
  (X ∈ analz H ∧ X ∈ parts H) ↔ X ∈ analz H :=
  by
    constructor
    · intro h
      exact h.1
    · intro h
      exact ⟨h, analz_subset_parts h⟩

@[simp]
lemma analz_disj_parts [InvKey] {H : Set Msg} {X : Msg} :
  (X ∈ analz H ∨ X ∈ parts H) ↔ X ∈ parts H :=
  by
    constructor
    · intro h
      cases h with
      | inl h => apply analz_conj_parts.mpr at h; cases h; assumption
      | inr h => assumption
    · intro h
      exact Or.inr h

@[simp]
lemma MPair_synth_analz [InvKey] :
  ⦃X, Y⦄ ∈ synth (analz H) ↔ X ∈ synth (analz H) ∧ Y ∈ synth (analz H) :=
  by
    constructor
    · intro h; cases h
      · apply And.intro
        · apply synth.inj; aapply analz.fst
        · apply synth.inj; aapply analz.snd
      · apply And.intro <;> assumption
    · intro h; exact synth.mpair h.1 h.2

@[simp]
lemma Crypt_synth_analz [InvKey]
  { h₁ : Key K ∈ analz H } 
  { h₂ : Key (invKey K) ∈ analz H } :
  ((Crypt K X ∈ synth (analz H)) ↔ X ∈ synth (analz H)) :=
  by
  constructor
  · intro h; cases h
    · apply synth.inj; aapply analz.decrypt
    · assumption
  · intro _; aapply synth.crypt

@[simp]
lemma Hash_synth_analz [InvKey] {H : Set Msg} {X Y : Msg} :
  X ∉ synth (analz H) → ((Hash ⦃X, Y⦄ ∈ synth (analz H)) ↔ Hash ⦃X, Y⦄ ∈ analz H) :=
  by
  intro _
  constructor
  · intro h; cases h with
    | inj => assumption
    | hash h => cases h with
      | inj a => apply analz.fst at a; apply synth.inj at a; contradiction
      | mpair => contradiction
  · intro h; aapply synth.inj

-- HPair: a combination of Hash and MPair

-- Freeness

-- @[simp]
lemma Agent_neq_HPair {A : Agent} {X Y : Msg} : Agent A ≠ ⦃X, Y⦄ₕ :=
  by simp [HPair]

-- @[simp]
lemma Nonce_neq_HPair {N : Nat} {X Y : Msg} : Nonce N ≠ ⦃X, Y⦄ₕ :=
  by simp [HPair]

-- @[simp]
lemma Number_neq_HPair {N : Nat} {X Y : Msg} : Number N ≠ ⦃X, Y⦄ₕ :=
  by simp [HPair]

-- @[simp]
lemma Key_neq_HPair {K : Key} {X Y : Msg} : Key K ≠ ⦃X, Y⦄ₕ :=
  by simp [HPair]

-- @[simp]
lemma Hash_neq_HPair {Z X Y : Msg} : Hash Z ≠ ⦃X, Y⦄ₕ :=
  by simp [HPair]

-- @[simp]
lemma Crypt_neq_HPair {K : Key} {X' X Y : Msg} : Crypt K X' ≠ ⦃X, Y⦄ₕ :=
  by simp [HPair]

@[simp]
lemma HPair_eq {X' Y' X Y : Msg} : (⦃X', Y'⦄ₕ = ⦃X, Y⦄ₕ) ↔ (X' = X ∧ Y' = Y) :=
  by simp [HPair]

@[simp]
lemma MPair_eq_HPair {X' Y' X Y : Msg} : (⦃X', Y'⦄ = ⦃X, Y⦄ₕ) ↔ (X' = Hash ⦃X, Y⦄ ∧ Y' = Y) :=
  by simp [HPair]

@[simp]
lemma HPair_eq_MPair {X' Y' X Y : Msg} : (⦃X, Y⦄ₕ = ⦃X', Y'⦄) ↔ (X' = Hash ⦃X, Y⦄ ∧ Y' = Y) :=
  by simp [HPair]; nth_rw 1 [Eq.comm]; nth_rw 2 [Eq.comm]

-- Specialized laws, proved in terms of those for Hash and MPair

@[simp]
lemma keysFor_insert_HPair [InvKey] {H : Set Msg} {X Y : Msg} :
  keysFor (insert (⦃X, Y⦄ₕ) H) = keysFor H :=
  by simp [HPair]

@[simp]
lemma parts_insert_HPair {H : Set Msg} {X Y : Msg} :
  parts (insert (⦃X, Y⦄ₕ) H) = insert (⦃X, Y⦄ₕ) (insert (Hash ⦃X, Y⦄) (parts (insert Y H))) :=
by
  simp [HPair]; grind

@[simp]
lemma analz_insert_HPair [InvKey] {H : Set Msg} {X Y : Msg} :
  analz (insert (⦃X, Y⦄ₕ) H) = insert (⦃X, Y⦄ₕ) (insert (Hash ⦃X, Y⦄) (analz (insert Y H))) :=
  by simp [HPair]

@[simp]
lemma HPair_synth_analz [InvKey] {H : Set Msg} {X Y : Msg} :
  X ∉ synth (analz H) →
  ((⦃X, Y⦄ₕ ∈ synth (analz H)) ↔ (Hash ⦃X, Y⦄ ∈ analz H ∧ Y ∈ synth (analz H))) :=
by
  intro _; simp [HPair, MPair_synth_analz]; intro _; constructor
  · intro h; cases h with
    | inj => assumption
    | hash a => cases a with
      | inj a => apply analz.fst at a; apply synth.inj at a; contradiction
      | mpair => contradiction
  · intro _; aapply synth.inj

-- We do NOT want Crypt... messages broken up in protocols!!
-- TODO rewrite this
-- attribute [-simp] parts.body

-- Rewrites to push in Key and Crypt messages, so that other messages can
-- be pulled out using the `analz_insert` rules

lemma pushKeysAgent {K : Key} {C : Agent} {H : Set Msg}:
  insert (Key K) (insert (Agent C) H) = insert (Agent C) (insert (Key K) H) :=
  by simp [Set.insert_comm]

lemma pushKeysNumber {K : Key} {N : Nat} {H : Set Msg}:
  insert (Key K) (insert (Number N) H) = insert (Number N) (insert (Key K) H) :=
  by simp [Set.insert_comm]

lemma pushKeysNonce {K : Key} {N : Nat} {H : Set Msg}:
  insert (Key K) (insert (Nonce N) H) = insert (Nonce N) (insert (Key K) H) :=
  by simp [Set.insert_comm]

lemma pushKeysHash {K : Key} {X : Msg} {H : Set Msg}:
  insert (Key K) (insert (Hash X) H) = insert (Hash X) (insert (Key K) H) :=
  by simp [Set.insert_comm]

lemma pushKeysMPair {K : Key} {X Y : Msg} {H : Set Msg}:
  insert (Key K) (insert ⦃X, Y⦄ H) = insert ⦃X, Y⦄ (insert (Key K) H) :=
  by simp [Set.insert_comm]

lemma pushKeysCrypt {K K' : Key} {X : Msg} {H : Set Msg}:
  insert (Key K) (insert (Crypt K' X) H) = insert (Crypt K' X) (insert (Key K) H) :=
  by simp [Set.insert_comm]

lemma pushCryptsAgent {X : Msg} {K : Key} {C : Agent} {H : Set Msg} :
  insert (Crypt K X) (insert (Agent C) H) = insert (Agent C) (insert (Crypt K X) H) :=
  by simp [Set.insert_comm]

lemma pushCryptsNonce {X : Msg} {K : Key} {N : Nat} {H : Set Msg} :
  insert (Crypt K X) (insert (Nonce N) H) = insert (Nonce N) (insert (Crypt K X) H) :=
  by simp [Set.insert_comm]

lemma pushCryptsNumber {X : Msg} {K : Key} {N : Nat} {H : Set Msg} :
  insert (Crypt K X) (insert (Number N) H) = insert (Number N) (insert (Crypt K X) H) :=
  by simp [Set.insert_comm]

lemma pushCryptsHash {X X' : Msg} {K : Key} {H : Set Msg} :
  insert (Crypt K X) (insert (Hash X') H) = insert (Hash X') (insert (Crypt K X) H) :=
  by simp [Set.insert_comm]

lemma pushCryptsMPair {X X' Y : Msg} {K : Key} {H : Set Msg} :
  insert (Crypt K X) (insert ⦃X', Y⦄ H) = insert ⦃X', Y⦄ (insert (Crypt K X) H) :=
  by simp [Set.insert_comm]

-- The set of key-free messages

inductive keyfree : Set Msg
  | agent {A : Agent} : keyfree (Agent A)
  | number {N : Nat} : keyfree (Number N)
  | nonce {N : Nat} : keyfree (Nonce N)
  | hash {X : Msg} : keyfree (Hash X)
  | mpair {X Y : Msg} : keyfree X → keyfree Y → keyfree ⦃X, Y⦄
  | crypt {K : Key} {X : Msg} : keyfree X → keyfree (Crypt K X)

-- Inductive cases
lemma keyfree_KeyE {K : Key} : ¬keyfree (Key K) :=
  by intro h; cases h

lemma keyfree_MPairEL {X Y : Msg} : keyfree ⦃X, Y⦄ → keyfree X :=
  by intro h; cases h; assumption

lemma keyfree_MPairER {X Y : Msg} : keyfree ⦃X, Y⦄ → keyfree Y :=
  by intro h; cases h; assumption

lemma keyfree_CryptE {K : Key} {X : Msg} : keyfree (Crypt K X) → keyfree X :=
  by intro h; cases h; assumption

-- Parts of key-free messages remain key-free
lemma parts_keyfree : parts keyfree ⊆ keyfree :=
by
  intro X h
  induction h with
  | inj => assumption
  | fst => aapply keyfree_MPairEL
  | snd => aapply keyfree_MPairER
  | body => aapply keyfree_CryptE

-- The key-free part of a set of messages can be removed from the scope of the `analz` operator
lemma analz_keyfree_into_Un [InvKey] {G H : Set Msg} {X : Msg} :
  X ∈ analz (G ∪ H) → G ⊆ keyfree → X ∈ parts G ∪ analz H :=
by
  intro hG hKeyFree
  induction hG with
  | inj h => cases h with
    | inl hG => exact Or.inl (parts.inj hG)
    | inr hH => exact Or.inr (analz.inj hH)
  | fst _ ih =>
  cases ih
  · left; aapply parts.fst
  · right; aapply analz.fst
  | snd _ ih => 
  cases ih
  · left; aapply parts.snd
  · right; aapply analz.snd
  | @decrypt K _ h₁ h₂ ih₁ ih₂ => cases ih₂ with
    | inl h => apply parts_mono at hKeyFree
               apply subset_trans (c := keyfree) at hKeyFree
               have pkf := parts_keyfree
               apply hKeyFree at pkf
               apply pkf at h
               have kf_k := keyfree_KeyE (K := invKey K)
               contradiction
    | inr =>
    cases ih₁
    · left; aapply parts.body
    · right; aapply analz.decrypt

-- Crypt and Hash are not in the image of Key
@[simp]
lemma Crypt_notin_image_Key {K : Key} {X : Msg} {A : Set Key} : Crypt K X ∉ Key '' A :=
  by simp

@[simp]
lemma Hash_notin_image_Key {X : Msg} {A : Set Key} : Hash X ∉ Key '' A :=
  by simp

-- Monotonicity of `synth` over `analz`
lemma synth_analz_mono [InvKey] {G H : Set Msg} : G ⊆ H → synth (analz G) ⊆ synth (analz H) :=
  λ h => synth_mono (analz_mono h)

-- Simplification for Fake cases
@[simp]
lemma Fake_analz_eq [InvKey] {H : Set Msg} {X : Msg} :
  X ∈ synth (analz H) → synth (analz (insert X H)) = synth (analz H) :=
by
  intro hX
  ext
  constructor
  · apply synth_subset_iff.mp
    apply subset_trans (b := synth (analz H) ∪ analz H)
    · apply Fake_analz_insert (G := H) (H := H) (X := X) at hX
      rw[Set.union_self] at hX
      assumption
    · apply sup_le_iff.mpr; apply And.intro
      · trivial
      · exact synth_increasing
  · apply synth_analz_mono; simp

-- Generalizations of `analz_insert_eq`
lemma gen_analz_insert_eq [InvKey] {H G : Set Msg} {X : Msg} :
  X ∈ analz H → H ⊆ G → analz (insert X G) = analz G :=
by
  intro hX hSubset
  apply Set.ext
  intro Y
  constructor
  · intro h
    induction h with
    | inj h => cases h with
      | inl hX' => apply analz_mono at hSubset; apply hSubset at hX; rw[hX']; assumption
      | inr hG => aapply analz.inj
    | fst h ih => exact analz.fst ih
    | snd h ih => exact analz.snd ih
    | decrypt h₁ h₂ ih₁ ih₂ => exact analz.decrypt ih₁ ih₂
  · intro h
    exact analz_mono (Set.subset_insert _ _) h

lemma synth_analz_insert_eq [InvKey] {H G : Set Msg} {X : Msg} {K : Key} :
  X ∈ synth (analz H) → H ⊆ G → ((Key K ∈ analz (insert X G)) ↔ (Key K ∈ analz G)) :=
by
  intro h₁ h₂
  constructor
  · induction h₁ with
    | inj a => apply gen_analz_insert_eq (G := G) at a; apply a at h₂; simp[h₂]
    | agent => rw [analz_insert_Agent]; intro h; cases h; contradiction; assumption
    | number => rw [analz_insert_Number]; intro h; cases h; contradiction; assumption
    | hash => rw [analz_insert_Hash]; intro h; cases h; contradiction; assumption
    | @mpair _ Y h₁ _ _ ih₂ => rw [analz_insert_MPair]; intro h; cases h with
      | inl => contradiction 
      | inr h => apply Fake_analz_insert (H := insert Y G) at h₁; apply h₁ at h;
                 cases h with
        | inl h => cases h; aapply analz_mono
        | inr h => apply ih₂; have b := h₂; apply subset_trans (c := insert Y G) at b
                   have c := Set.subset_insert (x := Y) (s := G)
                   apply b at c
                   apply sup_le (a := H) (b := insert Y G) (c := insert Y G) at c
                   have e := le_refl (a := insert Y G)
                   apply c at e
                   apply analz_mono at e
                   aapply e
    | crypt b a a_ih => intro h; apply a_ih; apply analz_insert_Crypt_subset at h
                        cases h; contradiction; assumption
  · apply analz_mono; apply Set.subset_insert

  

-- Fake parts for single messages
lemma Fake_parts_sing [InvKey] {H : Set Msg} {X : Msg} :
  X ∈ synth (analz H) → parts {X} ⊆ synth (analz H) ∪ parts H :=
by
  intro h
  rw[Set.singleton_def]
  apply subset_trans (b := parts (insert X H))
  · apply parts_mono; simp
  · aapply Fake_parts_insert
