///|
/// Credentials that may be used directly or as the source for an S3
/// assume-role request. Values are copied when the source is constructed.
pub struct S3CredentialSource {
  priv kind : S3CredentialSourceKind
}

///|
priv enum S3CredentialSourceKind {
  DefaultChain(disable_ec2_metadata~ : Bool)
  StaticCredentials(
    access_key_id~ : String,
    secret_access_key~ : String,
    session_token~ : String?
  )
}

///|
/// Authentication policy for an S3 operator. Credential material is opaque
/// and has no debug or display representation.
pub struct S3Auth {
  priv kind : S3AuthKind
}

///|
priv enum S3AuthKind {
  Credentials(S3CredentialSource)
  Unsigned
  AssumeRole(
    role_arn~ : String,
    source~ : S3CredentialSource,
    external_id~ : String?,
    role_session_name~ : String?,
    duration_seconds~ : UInt?
  )
}

///|
priv struct S3NativeSource {
  kind : UInt
  disable_ec2_metadata : Bool
  access_key_id : String
  secret_access_key : String
  has_session_token : Bool
  session_token : String
}

///|
priv struct S3NativeAuth {
  kind : UInt
  source : S3NativeSource
  role_arn : String
  has_external_id : Bool
  external_id : String
  has_role_session_name : Bool
  role_session_name : String
  has_duration_seconds : Bool
  duration_seconds : UInt
}

///|
fn s3_invalid_argument(message : String) -> OpenDalError {
  binding_error(InvalidArgument, NewOperator, message, None, None)
}

///|
fn s3_required_text(
  value : StringView,
  message : String,
) -> String raise OpenDalError {
  let value = value.to_owned()
  if value.length() == 0 {
    raise s3_invalid_argument(message)
  }
  value
}

///|
fn s3_optional_text(
  value : StringView?,
  message : String,
) -> String? raise OpenDalError {
  match value {
    Some(value) => Some(s3_required_text(value, message))
    None => None
  }
}

///|
fn s3_duration_seconds(value : UInt64?) -> UInt? raise OpenDalError {
  match value {
    Some(value) => {
      if value > 0xffff_ffffUL {
        raise s3_invalid_argument(
          "S3 assume-role duration_seconds exceeds the UInt32 ceiling",
        )
      }
      Some(value.to_uint())
    }
    None => None
  }
}

///|
fn s3_native_optional_text(value : String?) -> (Bool, String) {
  match value {
    Some(value) => (true, value)
    None => (false, "")
  }
}

///|
fn S3CredentialSource::native_parts(
  self : S3CredentialSource,
) -> S3NativeSource {
  match self.kind {
    DefaultChain(disable_ec2_metadata~) =>
      {
        kind: 0U,
        disable_ec2_metadata,
        access_key_id: "",
        secret_access_key: "",
        has_session_token: false,
        session_token: "",
      }
    StaticCredentials(access_key_id~, secret_access_key~, session_token~) => {
      let (has_session_token, session_token) = s3_native_optional_text(
        session_token,
      )
      {
        kind: 1U,
        disable_ec2_metadata: false,
        access_key_id,
        secret_access_key,
        has_session_token,
        session_token,
      }
    }
  }
}

///|
fn S3Auth::native_parts(self : S3Auth) -> S3NativeAuth {
  match self.kind {
    Credentials(source) => {
      let source = source.native_parts()
      {
        kind: source.kind,
        source,
        role_arn: "",
        has_external_id: false,
        external_id: "",
        has_role_session_name: false,
        role_session_name: "",
        has_duration_seconds: false,
        duration_seconds: 0U,
      }
    }
    Unsigned =>
      {
        kind: 2U,
        source: S3CredentialSource::default_chain().native_parts(),
        role_arn: "",
        has_external_id: false,
        external_id: "",
        has_role_session_name: false,
        role_session_name: "",
        has_duration_seconds: false,
        duration_seconds: 0U,
      }
    AssumeRole(
      role_arn~,
      source~,
      external_id~,
      role_session_name~,
      duration_seconds~
    ) => {
      let (has_external_id, external_id) = s3_native_optional_text(external_id)
      let (has_role_session_name, role_session_name) = s3_native_optional_text(
        role_session_name,
      )
      let (has_duration_seconds, duration_seconds) = match duration_seconds {
        Some(value) => (true, value)
        None => (false, 0U)
      }
      {
        kind: 3U,
        source: source.native_parts(),
        role_arn,
        has_external_id,
        external_id,
        has_role_session_name,
        role_session_name,
        has_duration_seconds,
        duration_seconds,
      }
    }
  }
}

///|
/// Use the standard AWS credential chain. EC2 instance metadata remains
/// available unless explicitly disabled.
pub fn S3CredentialSource::default_chain(
  disable_ec2_metadata? : Bool = false,
) -> S3CredentialSource {
  { kind: DefaultChain(disable_ec2_metadata~) }
}

///|
/// Use an explicit access-key pair, with an optional session token.
pub fn S3CredentialSource::static_credentials(
  access_key_id~ : StringView,
  secret_access_key~ : StringView,
  session_token? : StringView,
) -> S3CredentialSource raise OpenDalError {
  let access_key_id = s3_required_text(
    access_key_id, "S3 access_key_id must not be empty",
  )
  let secret_access_key = s3_required_text(
    secret_access_key, "S3 secret_access_key must not be empty",
  )
  let session_token = s3_optional_text(
    session_token, "S3 session_token must not be empty when provided",
  )
  {
    kind: StaticCredentials(access_key_id~, secret_access_key~, session_token~),
  }
}

///|
/// Use the standard AWS credential chain. EC2 instance metadata remains
/// available unless explicitly disabled.
pub fn S3Auth::default_chain(disable_ec2_metadata? : Bool = false) -> S3Auth {
  {
    kind: Credentials(S3CredentialSource::default_chain(disable_ec2_metadata~)),
  }
}

///|
/// Use an explicit access-key pair, with an optional session token.
pub fn S3Auth::static_credentials(
  access_key_id~ : StringView,
  secret_access_key~ : StringView,
  session_token? : StringView,
) -> S3Auth raise OpenDalError {
  {
    kind: Credentials(
      S3CredentialSource::static_credentials(
        access_key_id~,
        secret_access_key~,
        session_token?,
      ),
    ),
  }
}

///|
/// Send unsigned S3 requests. This is intended for public buckets or custom
/// endpoints that explicitly allow anonymous access.
pub fn S3Auth::unsigned() -> S3Auth {
  { kind: Unsigned }
}

///|
/// Assume an IAM role using either the standard credential chain or explicit
/// static source credentials. Omitting `source` selects the standard chain.
pub fn S3Auth::assume_role(
  role_arn~ : StringView,
  source? : S3CredentialSource,
  external_id? : StringView,
  role_session_name? : StringView,
  duration_seconds? : UInt64,
) -> S3Auth raise OpenDalError {
  let role_arn = s3_required_text(role_arn, "S3 role_arn must not be empty")
  let source = match source {
    Some(source) => source
    None => S3CredentialSource::default_chain()
  }
  let external_id = s3_optional_text(
    external_id, "S3 external_id must not be empty when provided",
  )
  let role_session_name = s3_optional_text(
    role_session_name, "S3 role_session_name must not be empty when provided",
  )
  let duration_seconds = s3_duration_seconds(duration_seconds)
  {
    kind: AssumeRole(
      role_arn~,
      source~,
      external_id~,
      role_session_name~,
      duration_seconds~,
    ),
  }
}

///|
/// Construct an S3 operator with a typed, secret-safe authentication policy.
/// Omitting `auth` selects the standard AWS credential chain.
pub fn Operator::s3(
  bucket : StringView,
  region~ : StringView,
  root? : StringView,
  endpoint? : StringView,
  auth? : S3Auth,
  virtual_host_style? : Bool = false,
) -> Operator raise OpenDalError {
  let bucket = s3_required_text(bucket, "S3 bucket must not be empty")
  let region = s3_required_text(region, "S3 region must not be empty")
  let root = s3_optional_text(root, "S3 root must not be empty when provided")
  let endpoint = s3_optional_text(
    endpoint, "S3 endpoint must not be empty when provided",
  )
  let auth = match auth {
    Some(auth) => auth
    None => S3Auth::default_chain()
  }
  let auth = auth.native_parts()
  let (has_root, root) = s3_native_optional_text(root)
  let (has_endpoint, endpoint) = s3_native_optional_text(endpoint)
  let result = native_operator_s3(
    bucket,
    region,
    has_root,
    root,
    has_endpoint,
    endpoint,
    auth.kind,
    auth.source.kind,
    auth.source.disable_ec2_metadata,
    auth.source.access_key_id,
    auth.source.secret_access_key,
    auth.source.has_session_token,
    auth.source.session_token,
    auth.role_arn,
    auth.has_external_id,
    auth.external_id,
    auth.has_role_session_name,
    auth.role_session_name,
    auth.has_duration_seconds,
    auth.duration_seconds,
    virtual_host_style,
  )
  if native_result_status(result) != 0U {
    raise error_from_native_result(result, NewOperator, None, None)
  }
  let operator = native_result_take_operator(result)
  if native_result_status(result) != 0U {
    raise error_from_native_result(result, NewOperator, None, None)
  }
  native_result_release(result)
  operator
}