import type { UnvalidatedObject } from "../json";
import * as json from "../json";
import { isDefined } from "../util";

/**
 * After parsing configurations from JSON, we don't know whether all the keys we expect are
 * present or not. This type is used to represent such values, which we expect to be
 * `Credential` values, but haven't validated yet.
 */
export type RawCredential = UnvalidatedObject<Credential>;

/** Usernames may be present for both authentication with tokens or passwords. */
export type Username = {
  /** The username needed to authenticate to the package registry, if any. */
  username?: string;
};

/** Decides whether `config` has a username. */
export function hasUsername(config: AuthConfig): config is Username {
  return "username" in config;
}

/**
 * Fields expected for authentication based on a username and password.
 * Both username and password are optional.
 */
export type UsernamePassword = {
  /** The password needed to authenticate to the package registry, if any. */
  password?: string;
} & Username;

/** Decides whether `config` is based on a username and password. */
export function isUsernamePassword(
  config: AuthConfig,
): config is UsernamePassword {
  return hasUsername(config) && "password" in config;
}

/**
 * Fields expected for token-based authentication.
 * Both username and token are optional.
 */
export type Token = {
  /** The token needed to authenticate to the package registry, if any. */
  token?: string;
} & Username;

/** Decides whether `config` is token-based. */
export function isToken(
  config: UnvalidatedObject<AuthConfig>,
): config is Token {
  // The "username" field is optional, but should be a string if present.
  if ("username" in config && !json.isStringOrUndefined(config.username)) {
    return false;
  }

  // The "token" field is required, and must be a string or undefined.
  return "token" in config && json.isStringOrUndefined(config.token);
}

/** Configuration for Azure OIDC. */
export type AzureConfig = { tenant_id: string; client_id: string };

/** Decides whether `config` is an Azure OIDC configuration. */
export function isAzureConfig(
  config: UnvalidatedObject<AuthConfig>,
): config is AzureConfig {
  return (
    "tenant_id" in config &&
    "client_id" in config &&
    isDefined(config.tenant_id) &&
    isDefined(config.client_id) &&
    json.isString(config.tenant_id) &&
    json.isString(config.client_id)
  );
}

/** Configuration for AWS OIDC. */
export type AWSConfig = {
  aws_region: string;
  account_id: string;
  role_name: string;
  domain: string;
  domain_owner: string;
  audience?: string;
};

/** Decides whether `config` is an AWS OIDC configuration. */
export function isAWSConfig(
  config: UnvalidatedObject<AuthConfig>,
): config is AWSConfig {
  // All of these properties are required.
  const requiredProperties = [
    "aws_region",
    "account_id",
    "role_name",
    "domain",
    "domain_owner",
  ];

  for (const property of requiredProperties) {
    if (
      !(property in config) ||
      !isDefined(config[property]) ||
      !json.isString(config[property])
    ) {
      return false;
    }
  }

  // The "audience" field is optional, but should be a string if present.
  if ("audience" in config && !json.isStringOrUndefined(config.audience)) {
    return false;
  }

  return true;
}

/** Configuration for JFrog OIDC. */
export type JFrogConfig = {
  jfrog_oidc_provider_name: string;
  audience?: string;
  identity_mapping_name?: string;
};

/** Decides whether `config` is a JFrog OIDC configuration. */
export function isJFrogConfig(
  config: UnvalidatedObject<AuthConfig>,
): config is JFrogConfig {
  // The "audience" and "identity_mapping_name" fields are optional, but should be strings if present.
  if ("audience" in config && !json.isStringOrUndefined(config.audience)) {
    return false;
  }
  if (
    "identity_mapping_name" in config &&
    !json.isStringOrUndefined(config.identity_mapping_name)
  ) {
    return false;
  }

  return (
    "jfrog_oidc_provider_name" in config &&
    isDefined(config.jfrog_oidc_provider_name) &&
    json.isString(config.jfrog_oidc_provider_name)
  );
}

/** Represents all supported OIDC configurations. */
export type OIDC = AzureConfig | AWSConfig | JFrogConfig;

/** All authentication-related fields. */
export type AuthConfig = UsernamePassword | Token | OIDC;

/**
 * A package registry configuration includes identifying information as well as
 * authentication credentials.
 */
export type Credential = AuthConfig & Registry;

/**
 * Pretty-prints a `Credential` value to a string, but hides the actual password or token values.
 *
 * @param credential The credential to convert to a string.
 */
export function credentialToStr(credential: Credential): string {
  let result: string = `Type: ${credential.type};`;

  const appendIfDefined = (name: string, val: string | undefined) => {
    if (isDefined(val)) {
      result += ` ${name}: ${val};`;
    }
  };

  appendIfDefined("Url", credential.url);
  appendIfDefined("Host", credential.host);

  if (hasUsername(credential)) {
    appendIfDefined("Username", credential.username);
  }

  if ("password" in credential) {
    appendIfDefined(
      "Password",
      isDefined(credential.password) ? "***" : undefined,
    );
  }
  if (isToken(credential)) {
    appendIfDefined("Token", isDefined(credential.token) ? "***" : undefined);
  }

  if (isAzureConfig(credential)) {
    appendIfDefined("Tenant", credential.tenant_id);
    appendIfDefined("Client", credential.client_id);
  } else if (isAWSConfig(credential)) {
    appendIfDefined("AWS Region", credential.aws_region);
    appendIfDefined("AWS Account", credential.account_id);
    appendIfDefined("AWS Role", credential.role_name);
    appendIfDefined("AWS Domain", credential.domain);
    appendIfDefined("AWS Domain Owner", credential.domain_owner);
    appendIfDefined("AWS Audience", credential.audience);
  } else if (isJFrogConfig(credential)) {
    appendIfDefined("JFrog Provider", credential.jfrog_oidc_provider_name);
    appendIfDefined("JFrog Identity Mapping", credential.identity_mapping_name);
    appendIfDefined("JFrog Audience", credential.audience);
  }

  return result;
}

/** A package registry is identified by its type and address. */
export type Registry = {
  /** The type of the package registry. */
  type: string;
} & Address;

// If a registry has an `url`, then that takes precedence over the `host` which may or may
// not be defined.
interface HasUrl {
  url: string;
  host?: string;
}

// If a registry does not have an `url`, then it must have a `host`.
interface WithoutUrl {
  url: undefined;
  host: string;
}

/**
 * A valid `Registry` value must either have a `url` or a `host` value. If it has a `url` value,
 * then that takes precedence over the `host` value. If there is no `url` value, then it must
 * have a `host` value.
 */
export type Address = HasUrl | WithoutUrl;

/** Gets the address as a string. This will either be the `url` if present, or the `host` if not. */
export function getAddressString(address: Address): string {
  if (address.url === undefined) {
    return address.host;
  } else {
    return address.url;
  }
}

export interface ProxyInfo {
  host: string;
  port: number;
  cert: string;
  registries: Registry[];
}

export type CertificateAuthority = {
  cert: string;
  key: string;
};

export type BasicAuthCredentials = {
  username: string;
  password: string;
};

/**
 * Represents configurations for the authentication proxy.
 */
export type ProxyConfig = {
  /** The validated configurations for the proxy. */
  all_credentials: Credential[];
  ca: CertificateAuthority;
  proxy_auth?: BasicAuthCredentials;
};
