/// otpauth:// key URI support (the de-facto format used by Google
/// Authenticator and compatible apps).

///|
/// OTP type carried by an otpauth URI.
pub(all) enum OtpKind {
  /// Time-based one-time password.
  Totp
  /// Event/counter-based one-time password.
  Hotp
} derive(Eq, @debug.Debug)

///|
/// Parameters of an otpauth enrollment URI.
pub(all) struct OtpAuth {
  /// Provider or service name, e.g. "Acme Corp".
  issuer : String
  /// User account, e.g. "alice@example.com".
  account : String
  /// Raw shared-secret bytes.
  secret : Bytes
  /// HMAC hash algorithm.
  algorithm : HashAlgorithm
  /// Number of code digits.
  digits : Int
  /// Time step in seconds (TOTP).
  period : Int
  /// TOTP or HOTP.
  otp_type : OtpKind
  /// Initial counter (HOTP).
  counter : UInt64
}

///|
/// Create the default parameter set for a secret (TOTP, SHA-1, 6 digits,
/// 30 s).
pub fn OtpAuth::new(
  issuer : String,
  account : String,
  secret : Bytes,
) -> OtpAuth {
  {
    issuer,
    account,
    secret,
    algorithm: Sha1,
    digits: 6,
    period: 30,
    otp_type: Totp,
    counter: 0UL,
  }
}

///|
fn pct_encode(s : StringView) -> String {
  @percent.encode(s)
}

///|
fn pct_decode(s : StringView) -> String raise OtpError {
  @percent.decode(s) catch {
    _ => raise InvalidUri("bad percent encoding")
  }
}

///|
fn uri_algorithm_name(algorithm : HashAlgorithm) -> String {
  match algorithm {
    Sha1 => "SHA1"
    Sha256 => "SHA256"
    Sha512 => "SHA512"
  }
}

///|
fn algorithm_from_uri(s : StringView) -> HashAlgorithm? {
  if s == "SHA1" {
    Some(Sha1)
  } else if s == "SHA256" {
    Some(Sha256)
  } else if s == "SHA512" {
    Some(Sha512)
  } else {
    None
  }
}

///|
/// Build an `otpauth://` URI (TOTP or HOTP).
pub fn otpauth_uri(otp : OtpAuth) -> String raise OtpError {
  if otp.secret.length() == 0 {
    raise EmptySecret
  }
  if otp.digits < 1 || otp.digits > 8 {
    raise InvalidDigits(otp.digits)
  }
  let (type_name, extra_param) = match otp.otp_type {
    Totp => {
      if otp.period <= 0 {
        raise InvalidPeriod(otp.period)
      }
      ("totp", "&period=" + otp.period.to_string())
    }
    Hotp => ("hotp", "&counter=" + otp.counter.to_string())
  }
  let label = pct_encode(otp.issuer) + ":" + pct_encode(otp.account)
  let secret = base32_encode_unpadded(otp.secret)
  let query = "?secret=" +
    secret +
    "&issuer=" +
    pct_encode(otp.issuer) +
    "&algorithm=" +
    uri_algorithm_name(otp.algorithm) +
    "&digits=" +
    otp.digits.to_string() +
    extra_param
  "otpauth://" + type_name + "/" + label + query
}

///|
/// Parse an `otpauth://totp/` or `otpauth://hotp/` URI.
pub fn parse_otpauth_uri(uri : String) -> OtpAuth raise OtpError {
  let (otp_type, rest) = if uri.has_prefix("otpauth://totp/") {
    (Totp, uri["otpauth://totp/".length():].to_owned())
  } else if uri.has_prefix("otpauth://hotp/") {
    (Hotp, uri["otpauth://hotp/".length():].to_owned())
  } else {
    raise InvalidUri("not an otpauth://totp or otpauth://hotp URI")
  }
  let (label_view, query_view) = match rest.split_once("?") {
    Some(parts) => parts
    None => raise InvalidUri("missing query component")
  }
  // Label: "Issuer:account" or just "account".
  let (label_issuer, account_view) = match label_view.split_once(":") {
    Some((issuer, account)) => (Some(issuer), account)
    None => (None, label_view)
  }
  let account = pct_decode(account_view)
  // Query parameters.
  let mut secret_raw : StringView? = None
  let mut query_issuer : StringView? = None
  let mut algorithm = Sha1
  let mut digits = 6
  let mut period = 30
  let mut counter = 0UL
  for pair in query_view.split("&") {
    let (k, v) = match pair.split_once("=") {
      Some(parts) => parts
      None => raise InvalidUri("malformed query pair: " + pair.to_owned())
    }
    if k == "secret" {
      secret_raw = Some(v)
    } else if k == "issuer" {
      query_issuer = Some(v)
    } else if k == "algorithm" {
      match algorithm_from_uri(v) {
        Some(a) => algorithm = a
        None => raise InvalidUri("unknown algorithm: " + v.to_owned())
      }
    } else if k == "digits" {
      digits = @string.parse_int(v) catch {
        _ => raise InvalidUri("bad digits: " + v.to_owned())
      }
    } else if k == "period" {
      period = @string.parse_int(v) catch {
        _ => raise InvalidUri("bad period: " + v.to_owned())
      }
    } else if k == "counter" {
      counter = @string.parse_uint64(v) catch {
        _ => raise InvalidUri("bad counter: " + v.to_owned())
      }
    } else {
      ignore(())
    }
  }
  let secret_str = match secret_raw {
    Some(s) => s.to_owned()
    None => raise InvalidUri("missing secret parameter")
  }
  let secret = base32_decode(secret_str) catch {
    _ => raise InvalidUri("secret is not valid Base32")
  }
  let issuer = match query_issuer {
    Some(i) => pct_decode(i)
    None =>
      match label_issuer {
        Some(i) => pct_decode(i)
        None => ""
      }
  }
  { issuer, account, secret, algorithm, digits, period, otp_type, counter, }
}