/* 
Type summary:

  Identity
    |_ UnknownIdentity <-> UnknownAttributes
    |
    |_ AnonymousIdentity <-> AnonymousAttributes
    |
    |_ ServiceAccountIdentity <-> ServiceAccountAttributes
    |     |_ ClientServiceAccountIdentity <-> ClientServiceAccountAttributes
    |     |_ UserServiceAccountIdentity <-> UserServiceAccountAttributes
    |
    |_ UserIdentity <-> UserAttributes
          |_ CitizenIdentity      <-> CitizenAttributes
          |_ EmployeeIdentity     <-> EmployeeAttributes
          |_ ExternalUserIdentity <-> ExternalUserAttributes
          |_ GenericUserIdentity  <-> GenericUserAttributes
          |_ GuestUserIdentity    <-> GuestUserAttributes
          |_ UnknownUserIdentity  <-> UnknownUserAttributes

Existing ID mappings:

| ---------------------------- | -------------------- | -------------------------------------------------------------- |
| Entity                       | ID                   | Example                                                        |
| ---------------------------- | -------------------- | -------------------------------------------------------------- |
| UnknownIdentity              | sub                  | foo1234                                                        |
| AnonymousIdentity            | username             | srvAccAnonymous                                                |
| UserServiceAccountAttributes | username             | srvAccDiagCanary                                               |
| ClientServiceAccountIdentity | aud (appId/clientId) | e5dd632b-cb97-48d7-a310-cde5147be717                           |
| CitizenIdentity              | mtlIdentityId        | @!4025.CA62.9BB6.16C5!0001!2212.0010!0000!3F39.BEDB.4ADB.F74D  |
| EmployeeIdentity             | username             | umartw8                                                        |
| ExternalUserIdentity         | username             | xdoejo3                                                        |
| GenericUserIdentity          | username             | cgdsecdev2                                                     |
| GuestUserIdentity            | username             | doe.daniel_hydro.qc.ca#EXT#@lavilledemontreal.omnicrosoft.com  |
| ---------------------------- | -------------------- | -------------------------------------------------------------- |

Identity.toString() examples:

| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Identity                     | Example                                                                                                                          |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| UnknownIdentity              | unknown:12345:John Doe                                                                                                           |
| AnonymousIdentity            | anonymous:srvAccAnonymous:srvAcc Anonymous                                                                                       |
| UserServiceAccountAttributes | service-account:user:srvAccDiagCanary:srvAcc Diagnostics Canary                                                                  |
| ClientServiceAccountIdentity | service-account:client:e5dd632b-cb97-48d7-a310-cde5147be717:infra-auth-auth-playground-dev                                       |
| CitizenIdentity              | user:citizen:@!4025.CA62.9BB6.16C5!0001!2212.0010!0000!3F39.BEDB.4ADB.F74D:John Doe:john.doe@mailinator.com                      |
| EmployeeIdentity             | user:employee:udoejo3:John DOE:john.doe@montreal.ca:100674051:421408000000:vdm                                                   |
| ExternalUserIdentity         | user:external:xdoejo3:John DOE:john.doe@montreal.ca::vdm                                                                         |
| GenericUserIdentity          | user:generic:cgdsecdev2:C.Generique dsec developpeur2::4211:vdm                                                                  |
| GuestUserIdentity            | user:guest:employees:doe.daniel_hydro.qc.ca#EXT#@lavilledemontreal.omnicrosoft.com:doe.daniel@hydro.qc.ca:doe.daniel@hydro.qc.ca |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |


*/

/************************************************************************************************
 * User attributes
 * ---------------
 * All user attributes extend the CommonUserAttributes type, which defines a couple of common but optional attributes.
 * All user attributes have at least a username or an email to qualify.
 *
 * We have identified the following types of user:
 *   - CitizenAttributes
 *   - EmployeeAttributes
 *   - ExternalUserAttributes
 *   - GenericUserAttributes
 *   - GuestUserAttributes
 *
 * If the subtype of user is not recognized, it will default to UnknownUserAttributes.
 *
 * The UserAttributes type is a union of all those types. It provides access to the
 * common optional attributes through the CommonUserAttributes type,
 * but it is better to test the 'type' of the attributes to access the strongly types attributes
 * with additional guarantees.
 *
 * Since some users might have multiple profiles and thus multiple emails (like @montreal.ca, .adm@montreal.ca or @spvm.qc.ca)
 * that they can select at login time, you might want to consider the "accountProfile" attribute which categorizes them.
 * The ID will correctly identify the physical person but not the selected role.
 * So, you could combine ID+accountProfile or prefer the email in some cases, like when you need to evaluate the permissions.
 *
 ************************************************************************************************/

/**
 * The type of profile selected by the user at login time, when a user has multiple identities
 * such as a VDM employee with a SPVM email.
 * 'vdm-admin' whould be selected when the authenticated user is a sysadmin who selected his superadmin profile,
 * instead of the regular one.
 * Usually defaults to 'vdm'.
 */
export type AccountProfile = 'vdm' | 'vdm-admin' | 'spvm';

/**
 * Common attributes optionally shared by all types of users.
 * You should test the 'type' attribute in order to strongly type the allowed attributes.
 */
export type CommonUserAttributes = {
  /**
   * The type of user that will specify which attributes are allowed.
   */
  type: string;
  /**
   * The username of the user, which could be an email, a UPN or a short code depending on the 'type'.
   */
  username: string;
  /**
   * The email of the user.
   */
  email?: string;
  /**
   * The first name of the user.
   */
  firstName?: string;
  /**
   * The last name of the user.
   */
  lastName?: string;
  /**
   * The registration number of the employee as part of the HR process.
   * For instance: 100375065
   */
  registrationNumber?: string;
  /**
   * The department of the user. This is the primary organization unit that the user has been assigned to.
   * Note that a user could belong to multiple organization units.
   * Note that the department can be a 12 chars long string containing a number such as '040403040000'
   * or a text description such as 'DIV. PREVENTION ET SECURITE URBAINE'.
   */
  department?: string;
  /**
   * The account profile that was selected upon logon.
   */
  accountProfile?: AccountProfile;
};

/**
 * The attributes of a citizen: a user consuming Montreal digital services.
 * There are regular citizen accounts for families as well as citizens acting on behalf of their organization.
 *
 * The ID of the citizen is managed by the DCI (Dossier Citoyen Intégré) and is known as mtlIdentityId.
 */
export type CitizenAttributes = CommonUserAttributes & {
  /**
   * The type of user that will specify which attributes are allowed.
   */
  type: 'citizen';
  /**
   * The username of the citizen which should be the email.
   */
  username: string;
  /**
   * The email of the citizen.
   * This is his own mail used for registering to Montreal digital services.
   */
  email: string;
  /**
   * The first name of the citizen.
   * For instance: John
   */
  firstName: string;
  /**
   * The last name of the citizen.
   * For instance: DOE
   */
  lastName: string;
};

/**
 * The attributes of an employee: a user on the payroll of the city of Montreal.
 *
 * Note that a generic user that has all the required attributes of an employee would be detected as such,
 * for testing purposes.
 *
 * The ID is mapped to the username.
 */
export type EmployeeAttributes = CommonUserAttributes & {
  /**
   * The type of user that will specify which attributes are allowed.
   */
  type: 'employee';
  /**
   * The username of the employee which should be a short code starting with the letter 'u'.
   * For instance: uzartw1
   */
  username: string;
  /**
   * The email of the employee.
   * There are multiple domains such as montreal.ca or spvm.qc.ca
   */
  email: string;
  /**
   * The first name of the employee.
   * For instance: John
   */
  firstName: string;
  /**
   * The last name of the employee.
   * For instance: DOE
   */
  lastName: string;
  /**
   * The registration number of the employee as part of the HR process.
   * For instance: 100375065
   */
  registrationNumber: string;
  /**
   * The department of the employee. This is the primary organization unit that the employee has been assigned to.
   * Note that an employee could belong to multiple organization units.
   * Note that the department can be a 12 chars long string containing a number such as '040403040000'
   * or a text description such as 'DIV. PREVENTION ET SECURITE URBAINE'.
   */
  department: string;
  /**
   * The account profile that was selected upon logon.
   */
  accountProfile: AccountProfile;
  /**
   * Specifies if the account presented as an external user is a generic account, used for testing purposes.
   */
  isGeneric: boolean;
};

/**
 * The attributes of an external user: a user that is not on the payroll of the city of Montreal but actively collaborates
 * with the city and has his own internal email and short code (starting with the letter x).
 *
 * Note that a generic user that has all the required attributes of an external user would be detected as such,
 * for testing purposes.
 *
 * The ID is mapped to the username.
 */
export type ExternalUserAttributes = CommonUserAttributes & {
  type: 'external';
  /**
   * The username of the external user which should be a short code starting with the letter 'x'.
   * For instance: xzartw1
   */
  username: string;
  /**
   * The email of the external user.
   * The name part of the email should end with ".ext", like john.doe.ext@montreal.ca
   */
  email?: string;
  /**
   * The first name of the user.
   * For instance: John
   */
  firstName: string;
  /**
   * The last name of the user.
   * For instance: DOE
   */
  lastName: string;
  /**
   * The department of the user. This is the primary organization unit that the user has been assigned to.
   * Note that a user could belong to multiple organization units.
   * Note that the department can be a 12 chars long string containing a number such as '040403040000'
   * or a text description such as 'DIV. PREVENTION ET SECURITE URBAINE'.
   */
  department?: string;
  /**
   * The account profile that was selected upon logon.
   */
  accountProfile: AccountProfile;
  /**
   * Specifies if the account presented as an external user is a generic account, used for testing purposes.
   */
  isGeneric: boolean;
};

/**
 * The attributes of a generic user: a fake user used for testing only, that has the right profile or role to perform specific tasks.
 *
 * Note that if a generic user has all the required attributes of an employee or an external user, then it will be detected
 * as such an employee or external user, for testing purposes, and not as a generic user.
 * However, citizens and guest users cannot be generic.
 *
 * The ID is mapped to the username.
 */
export type GenericUserAttributes = CommonUserAttributes & {
  type: 'generic';
  /**
   * The username of the user which should be a short code starting with the letters 'cg'.
   * For instance: cgdsecdev2
   */
  username: string;
  /**
   * The first name of the user.
   * For instance: John
   */
  firstName: string;
  /**
   * The last name of the user.
   * For instance: DOE
   */
  lastName: string;
  /**
   * The account profile that was selected upon logon.
   */
  accountProfile: AccountProfile;
};

/**
 * The attributes of a guest: a user who is invited in the city of Montreal tenant in order to collaborate with some specific services.

* The ID is mapped to the username.
 */
export type GuestUserAttributes = CommonUserAttributes & {
  type: 'guest';
  /**
   * The username of the guest user which should contain the '#EXT#' suffix and belong to the "lavilledemontreal.onmicrosoft.com" domain.
   * For instance: john.doe_hydro.qc.ca#EXT#@lavilledemontreal.onmicrosoft.com
   */
  username: string;
  /**
   * The email of the guest user.
   * The domain of the email should not belong to the City of Montreal, such as montreal.ca or spvm.qc.ca.
   * For instance: john.doe@hydro.qc.ca
   */
  email: string;
};

/**
 * An unknown user has no guaranteed attributes and defaults to optional common user attributes.
 * However, since it is a user, we know that it has at least a username or an email.
 *
 * The ID is mapped to the username or the email.
 */
export type UnknownUserAttributes = CommonUserAttributes & {
  type: 'unknown';
};

/**
 * The different sets of user attributes based on the user subtype.
 */
export type UserAttributes =
  | CitizenAttributes
  | EmployeeAttributes
  | ExternalUserAttributes
  | GenericUserAttributes
  | GuestUserAttributes
  | UnknownUserAttributes;

/************************************************************************************************
 * Anonymous attributes
 * --------------------
 * There is a single type of attributes for the anonymous identity.
 *
 ************************************************************************************************/

/**
 * The specific attributes for an anonymous identity.
 */
export type AnonymousAttributes = {
  type: 'anonymous';
  /**
   * The username of the anonymous user.
   * For instance: srvAccAnonymous
   */
  username: string;
};

/************************************************************************************************
 * Service account attributes
 * --------------------------
 * There are 2 types of service accounts:
 *   - client: this is a non-interactive client that used the OAuth2 client_credentials flow.
 *     This is the only flow for Azure AD B2C and Entra ID.
 *   - user: this is a specific user that used the OAuth2 password flow.
 *     This flow is deprecated.
 *
 ************************************************************************************************/

/**
 * The specific attributes for a service account of subtype 'client'.
 * Note that 'client' means that the service authenticated using the OAuth2 client_credentials flow.
 */
export type ClientServiceAccountAttributes = {
  type: 'client';
};

/**
 * The specific attributes for a service account of subtype 'user'.
 * Note that 'user' means that the service authenticated using the OAuth2 password flow. (Deprecated)
 */
export type UserServiceAccountAttributes = {
  type: 'user';
  /**
   * The username of the user service account.
   * For instance: SrvAccDiagCanary
   */
  username: string;
};

/**
 * The different sets of service account attributes based on the account subtype.
 */
export type ServiceAccountAttributes =
  | ClientServiceAccountAttributes
  | UserServiceAccountAttributes;

/***********************************************************************************************************************
 * Identities:
 * -----------
 * Each type of identity has some required properties, such as:
 *   - the ID
 *   - the display name
 *   - the source of this identity
 *   - the attributes specific to the subtype of identity
 *   - a toString() helper method that can format the identity for auditing or logging
 *
 * We have 3 types of identity:
 *   - user (employee, citizen, external user, generic user, guest user, unknown user...)
 *   - service account (client, user)
 *   - anonymous
 *
 * If we don't recognize one of those types of identity, we will default to UnknownIdentity.
 *
 * You'll have to test the type of identity before accessing the attributes, then you'll have to test the type of attributes.
 *
 * Note that a UserIdentity will default to the CommonUserAttributes type for its attributes, in order to provide
 * a quick access to some common user attributes. But those attributes will be optional and should be checked,
 * whereas the typed attributes will provide strong guarantees.
 *
 ***********************************************************************************************************************/

/**
 * Contains some common attributes that provide some traceability for understanding which claim we have selected for the ID of the identity,
 * where does the token come from and which was the internal ID of the user in the IDP.
 */
export type IdentitySource = {
  /**
   * The audience of the JWT, which is usually the clientID of our appId.
   */
  aud: string;
  /**
   * The display name of the audience of the JWT, which is usually the name of our app.
   */
  audDisplayName?: string;
  /**
   * Specifies which service issued the JWT that we parsed into an identity.
   * Usually, this would be 'security-identity-token-api'.
   */
  issuer: string;
  /**
   * Specifies which IDP produced the access token that was submitted to TokenAPI.
   * For EntraID, this would be https://login.microsoftonline.com/9f15d2dc-8753-4f83-aac2-a58288d3a4bc/v2.0,
   * for Azure AD B2C https://connexion.montreal.ca,
   * for Gluu employee https://idp.montreal.ca,
   * for Gluu citizens https://auth.montreal.ca,
   * for anonymous tokens security-identity-anonymous-token-api
   */
  accessTokenIssuer?: string;
  /**
   * Specifies which claim was used for the unique ID of the identity.
   * This could be: userName, email, mtlIdentityId, aud, sub
   */
  claim: string;
  /**
   * Specifies the internal ID that would provide access to the user object in the IDP itself.
   * For Azure, this would be the 'oid' (or objectID) and for Gluu this would be the inum (or sub).
   */
  internalId: string;
  /**
   * Specifies the realm that produced the access token.
   * This could be: employees, citizens, anonymous
   */
  realm: string;
  /**
   * Specifies the name of the environment that produced the JWT.
   * This could be:  lab, dev, accept, prod
   */
  env?: string;
};

/**
 * A BaseIdentity contains attributes shared by all types of Identity.
 * They also have a strongly typed 'attributes' property that is specific
 * to each kind of identity and its variants.
 */
export type BaseIdentity<TAttributes> = {
  /**
   * A stable unique ID for the authenticated user.
   * An ID can be mapped to a username, email, appId according to the type of user.
   * The name of the selected attribute will be specified in the source object, in the 'claim' attribute.
   */
  id: string;
  /**
   * A display name for the authenticated user
   */
  displayName: string;
  /**
   * The information about the source of the JWT and its associated access token.
   * It would also specify which claim has been selected for the unique ID of the identity.
   */
  source: IdentitySource;
  /**
   * The attributes specific to the subtype of identity.
   */
  attributes: TAttributes;
  /**
   * A helper function for formatting the Identity in order to log it or audit it.
   * This is for diagnostics only.
   */
  toString(): string;
};

/**
 * This is a user that can interact with the systems of the city of Montreal.
 * The attributes will vary according to the type of user (citizen, employee, external user...).
 *
 * Since some users might have multiple profiles and thus multiple emails (like @montreal.ca, .adm@montreal.ca or @spvm.qc.ca)
 * that they can select at login time, you might want to consider the "accountProfile" attribute which categorizes them.
 * The ID will correctly identify the physical person but not the selected role.
 * So, you could combine ID+accountProfile or prefer the email in some cases, like when you need to evaluate the permissions.
 */
export type UserIdentity<TAttributes extends UserAttributes = UserAttributes> =
  BaseIdentity<TAttributes> & {
    /** The type of identity
     */
    type: 'user';
  };

/**
 * This is an employee, on the payroll of the city of Montreal.
 *
 * The ID is mapped to the username.
 */
export type EmployeeIdentity = UserIdentity<EmployeeAttributes>;

/**
 * This is an external user: an external collaborator or consultant that is not on the payroll of the city of Montreal,
 * but can access all internal services and has an email in the domain of Montreal (or SPVM).
 *
 * The ID is mapped to the username.
 */
export type ExternalUserIdentity = UserIdentity<ExternalUserAttributes>;

/**
 * This is a generic user: a user shared by employees or external users in order to perform QA tests in a non production environment.
 * Each generic user would have its own role or profile in the tested application, allowing one to perform the tasks required by the test.
 *
 * The ID is mapped to the username.
 */
export type GenericUserIdentity = UserIdentity<GenericUserAttributes>;

/**
 * This is a guest user: a user who doesn't work for the city of Montreal but needs to colloborate with a set of limited and specific services.
 * Note that, in theory, we could have guest users in other realms such as citizens (like sysadmins), but they cannot login at the moment and thus
 * we should only have tokens from the 'employees' realm.
 * Note that this kind of user might not have a firstName and lastName.
 *
 * The ID is mapped to the username.
 */
export type GuestUserIdentity = UserIdentity<GuestUserAttributes>;

/**
 * This is a citizen: a user consuming Montreal digital services.
 * There are regular citizen accounts for families as well as citizens acting on behalf of their organization.
 *
 * The ID of the citizen is managed by the DCI (Dossier Citoyen Intégré) and is known as mtlIdentityId.
 */
export type CitizenIdentity = UserIdentity<CitizenAttributes>;

/**
 * This is a user but we could not detect its type (employee, citizen...).
 * In this case, it means that the user has at least a username or an email.
 * All other common attributes might be defined but are not guaranteed.
 *
 * The ID of the unknown user will be mapped to the username when available, otherwise to the email.
 */
export type UnknownUserIdentity = UserIdentity<UnknownUserAttributes>;

/**
 * This is an anonymous user: a user consuming some basic digitital services that don't require to be identified,
 * such as reporting a pot hole in the street.
 *
 * The ID is mapped to the username.
 */
export type AnonymousIdentity = BaseIdentity<AnonymousAttributes> & {
  /** The type of identity */
  type: 'anonymous';
};

/**
 * This is a service account, without interactive logon, that allows a backend service or an automation to perform some tasks
 * with the required privileges (least privilege).
 * A service account has an ID, a display name and a secret (that expires after a few months).
 *
 * The ID is mapped to the 'aud' or the 'username', depending on the subtype.
 */
export type ServiceAccountIdentity<
  TAttributes extends ServiceAccountAttributes = ServiceAccountAttributes,
> = BaseIdentity<TAttributes> & {
  /**
   * The type of identity
   */
  type: 'service-account';
};

/**
 * This is a service account, without interactive logon, that allows a backend service or an automation to perform some tasks
 * with the required privileges (least privilege).
 * A service account has an ID, a display name and a secret (that expires after a few months).
 *
 * The ID is mapped to the 'aud' which contains the appId in Azure or the inum in Gluu.
 */
export type ClientServiceAccountIdentity = ServiceAccountIdentity<ClientServiceAccountAttributes>;

/**
 * This is the old way of managing service accounts, with a real user provisioned (that's why there is a username property).
 * This kind of account as been deprecated (hence the legacy part) in favor of real service accounts that don't allow interactive logon.
 *
 * The id is mapped to the username.
 */
export type UserServiceAccountIdentity = ServiceAccountIdentity<UserServiceAccountAttributes>;

/**
 * An unknown identity has no specific attributes and defaults to this empty definition.
 */
export type UnknownAttributes = {
  type: 'unknown';
};

/**
 * This is the default identity when we could not match the proper one from the submitted JWT.
 * This might happen when we introduce a new type and this lib has not been updated in the client application,
 * but it should be very rare.
 *
 * The ID is mapped to the sub.
 */
export type UnknownIdentity = BaseIdentity<UnknownAttributes> & {
  /**
   * The type of identity
   */
  type: 'unknown';
};

/**
 * This is the Identity of the agent performing a request.
 * You would have to discriminate the right identity based on the 'type' property.
 * All identities will have a unique ID, a display name, a source, an attributes struct and a toString() function for formatting the values.
 */
export type Identity = AnonymousIdentity | UserIdentity | ServiceAccountIdentity | UnknownIdentity;
