// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Queue a ClientHello carrying `shares`, and the server's cookie when it sent one.
fn Conn::send_hello(
  self : Conn,
  shares : Array[(Int32, Bytes)],
  cookie : Bytes?,
) -> Unit raise @alert.Alert {
  guard self.side is Client(config, host) else { return }
  let exts : Array[@ext.Ext] = []
  if !is_address(host) {
    exts.push({ kind: ServerName, data: @ext.server_name(host), })
  }
  exts.push({
    kind: SupportedVersions,
    data: @ext.versions([@ext.version_13][:]),
  })
  exts.push({ kind: SupportedGroups, data: @ext.groups(config.groups[:]), })
  exts.push({ kind: SignatureAlgorithms, data: @ext.schemes(offered[:]), })
  exts.push({ kind: KeyShare, data: @ext.shares(shares[:]), })
  if config.alpn.length() > 0 {
    exts.push({ kind: Alpn, data: @ext.protocols(config.alpn[:]), })
  }
  if cookie is Some(c) {
    exts.push({ kind: Cookie, data: @ext.cookie(c[:]), })
  }
  let raw = @msg.hello({
    random: (self.rand)(32),
    session_id: self.session,
    suites: config.suites.map(s => s.code()),
    extensions: exts,
  })
  if self.first_hello.length() == 0 {
    self.first_hello = raw
  } else {
    self.record(raw[:])
  }
  self.send_handshake(raw)
}

///|
/// Whether `host` is an IP literal, which RFC 6066 ยง3 keeps out of server_name.
fn is_address(host : String) -> Bool {
  host.contains(":") || host.iter().all(c => c == '.' || (c >= '0' && c <= '9'))
}

///|
fn Conn::on_client(
  self : Conn,
  config : Client,
  host : String,
  kind : @msg.Kind,
  body : Bytes,
  raw : Bytes,
) -> Unit raise @alert.Alert {
  match (self.phase, kind) {
    (Hello, ServerHello) => self.on_server_hello(config, body, raw)
    (Flight, EncryptedExtensions) => {
      guard @msg.read_encrypted_extensions(body[:]) is Some(exts) else {
        raise @alert.fatal(DecodeError)
      }
      if @ext.find(exts[:], Alpn) is Some(e) {
        match @ext.read_protocols(e.data[:]) {
          [p] if config.alpn.contains(p) => self.alpn = Some(p)
          _ => raise @alert.fatal(IllegalParameter)
        }
      }
      self.record(raw[:])
    }
    (Flight, CertificateRequest) => {
      self.requested = true
      self.record(raw[:])
    }
    (Flight, Certificate) => {
      guard @msg.read_certificate(body[:]) is Some((_, certs)) &&
        certs.length() > 0 else {
        raise @alert.fatal(DecodeError)
      }
      self.peer = certs
      match config.verify {
        Chain => {
          let _ = @x509.verify(
            certs[:],
            config.roots,
            now=self.now,
            host~,
            policy=config.policy,
          ) catch {
            e => {
              self.refused = Some(e)
              raise @alert.fatal(alert_of(e))
            }
          }
        }
        Custom(judge) =>
          judge(certs, host) catch {
            _ => raise @alert.fatal(BadCertificate)
          }
      }
      self.record(raw[:])
    }
    (Flight, CertificateVerify) => {
      guard self.peer.get(0) is Some(der) else {
        raise @alert.fatal(UnexpectedMessage)
      }
      guard @msg.read_certificate_verify(body[:]) is Some((scheme, sig)) else {
        raise @alert.fatal(DecodeError)
      }
      let leaf = @x509.parse(der[:]) catch {
        _ => raise @alert.fatal(BadCertificate)
      }
      let content = @msg.signed(@msg.server_context, self.th()[:])
      if !offered.contains(scheme) || !verified(leaf, scheme, content, sig) {
        raise @alert.fatal(DecryptError)
      }
      self.record(raw[:])
    }
    (Flight, Finished) => {
      if self.peer.length() == 0 {
        raise @alert.fatal(UnexpectedMessage)
      }
      let d = self.digest()
      if !@keys.finished_ok(
          self.server_secret[:],
          self.th()[:],
          body[:],
          digest=d,
        ) {
        raise @alert.fatal(DecryptError)
      }
      self.record(raw[:])
      self.open_application()
      if self.requested {
        let empty = @msg.frame(Certificate, @msg.certificate([][:])[:])
        self.record(empty[:])
        self.send_handshake(empty)
      }
      let fin = @msg.frame(
        Finished,
        @keys.finished(self.client_secret[:], self.th()[:], digest=d)[:],
      )
      self.record(fin[:])
      self.send_handshake(fin)
      self.writer = Some(
        @record.Cipher::new(self.client_app[:], suite=self.suite.unwrap()),
      )
      self.phase = Open
    }
    (Open, NewSessionTicket) => ()
    (Open, KeyUpdate) => self.on_update(body)
    _ => raise @alert.fatal(UnexpectedMessage)
  }
}

///|
fn Conn::on_server_hello(
  self : Conn,
  config : Client,
  body : Bytes,
  raw : Bytes,
) -> Unit raise @alert.Alert {
  guard @msg.read_server_hello(body[:]) is Some(sh) else {
    raise @alert.fatal(DecodeError)
  }
  guard @ext.find(sh.extensions[:], SupportedVersions) is Some(v) &&
    @ext.read_selected_version(v.data[:]) == Some(@ext.version_13) else {
    raise @alert.fatal(ProtocolVersion)
  }
  guard @record.Suite::of(sh.suite) is Some(suite) &&
    config.suites.contains(suite) else {
    raise @alert.fatal(IllegalParameter)
  }
  if sh.session_id != self.session ||
    (self.retried && self.suite != Some(suite)) {
    raise @alert.fatal(IllegalParameter)
  }
  self.suite = Some(suite)
  if @hs.is_retry(sh) {
    if self.retried {
      raise @alert.fatal(UnexpectedMessage)
    }
    guard @hs.retry_group(sh) is Some(group) &&
      config.groups.contains(group) &&
      !self.shares.iter().any(s => s.0 == group) else {
      raise @alert.fatal(IllegalParameter)
    }
    self.retried = true
    self.begin_transcript(Some(raw))
    let (secret, public) = share(group, self.rand)
    self.shares = [(group, secret)]
    let cookie = match @ext.find(sh.extensions[:], Cookie) {
      Some(e) => @ext.read_cookie(e.data[:])
      None => None
    }
    self.send_hello([(group, public)], cookie)
    return
  }
  guard @ext.find(sh.extensions[:], KeyShare) is Some(ks) &&
    @ext.read_selected_share(ks.data[:]) is Some((group, public)) else {
    raise @alert.fatal(MissingExtension)
  }
  guard self.shares.iter().find_first(s => s.0 == group) is Some((_, secret)) else {
    raise @alert.fatal(IllegalParameter)
  }
  if self.transcript is None {
    self.begin_transcript(None)
    self.record(self.first_hello[:])
  }
  self.record(raw[:])
  self.open_handshake(agree(group, secret, public))
  self.writer = Some(@record.Cipher::new(self.client_secret[:], suite~))
  self.reader = Some(@record.Cipher::new(self.server_secret[:], suite~))
  self.phase = Flight
}

///|
/// The handshake secrets from the shared secret, over the transcript through ServerHello.
fn Conn::open_handshake(self : Conn, shared : Bytes) -> Unit {
  let d = self.digest()
  let early = @keys.early(digest=d)
  self.handshake_secret = @keys.handshake(early[:], shared[:], digest=d)
  let th = self.th()
  self.client_secret = @keys.client_handshake(
    self.handshake_secret[:],
    th[:],
    digest=d,
  )
  self.server_secret = @keys.server_handshake(
    self.handshake_secret[:],
    th[:],
    digest=d,
  )
}

///|
/// The application secrets, over the transcript through the server Finished; the side that reads
/// the server's traffic switches its reader here.
fn Conn::open_application(self : Conn) -> Unit {
  let d = self.digest()
  let master = @keys.master(self.handshake_secret[:], digest=d)
  let th = self.th()
  self.client_app = @keys.client_application(master[:], th[:], digest=d)
  self.server_app = @keys.server_application(master[:], th[:], digest=d)
  if self.side is Client(_, _) {
    self.reader = Some(
      @record.Cipher::new(self.server_app[:], suite=self.suite.unwrap()),
    )
  }
}

///|
fn alert_of(e : @x509.Refused) -> @alert.Kind {
  match e {
    Expired(..) | Premature(..) => CertificateExpired
    Issuer => UnknownCa
    Algorithm(..) | Weak(..) => UnsupportedCertificate
    _ => BadCertificate
  }
}