import { PublicClientApplication } from '@azure/msal-node';
import type { DeviceCodeRequest } from '@azure/msal-node';
import {
  BaseConfigBuilder,
  type ConfigBuilderCallbackArgs,
  type ModulesInstance,
} from '@equinor/fusion-framework-module';

import type { MsalNodeModule } from './module.js';
import type { AuthConfig } from './AuthConfigurator.interface.js';

/**
 * Internal builder for MSAL Node authentication configuration.
 *
 * This class provides the implementation for the fluent API exposed via the public interface.
 * Most consumer-facing documentation is in the interface; see {@link IAuthConfigurator} for usage details.
 *
 * @see IAuthConfigurator
 * @extends BaseConfigBuilder
 *
 * Maintainers: Extend or refactor this class to support new authentication modes or configuration options.
 * Ensure changes are reflected in the interface and validated in `_processConfig`.
 */
export class AuthConfigurator extends BaseConfigBuilder<AuthConfig> {
  /**
   * Creates the builder with the `interactive` authentication mode as the default.
   */
  constructor() {
    super();
    this.setMode('interactive');
  }

  /**
   * Sets the authentication mode for the module.
   *
   * @param mode - The authentication mode: `'interactive'`, `'silent'`, or `'token_only'`.
   */
  setMode(mode: AuthConfig['mode']) {
    this._set('mode', mode);
  }

  /**
   * Sets a pre-configured MSAL `PublicClientApplication` instance.
   *
   * Use this when you need full control over the MSAL client configuration.
   * For most cases, prefer {@link AuthConfigurator.setClientConfig | setClientConfig}.
   *
   * @param client - The MSAL `PublicClientApplication` instance.
   */
  setClient(client: AuthConfig['client']) {
    this._set('client', client);
  }

  /**
   * Configures the MSAL client using Azure AD tenant and client IDs.
   *
   * Lazily creates a `PublicClientApplication` with a secure persistence cache.
   * The dynamic import avoids requiring `libsecret` in environments that do not need it.
   *
   * @param tenantId - Azure AD tenant (directory) ID.
   * @param clientId - Azure AD application (client) ID.
   */
  setClientConfig(tenantId: string, clientId: string): void {
    this._set('client', async () => {
      // Dynamically import the createAuthClient function since the client uses `libsecret``
      // which is not default installed in all environments.
      // This avoids installing `libsecret` in environments where it is not needed, like CI/CD pipelines.
      const { createAuthClient } = await import('./create-auth-client.js');
      return createAuthClient(tenantId, clientId);
    });
  }

  /**
   * Sets the port for the local HTTP callback server used in interactive mode.
   *
   * @param port - Port number for the local server.
   */
  setServerPort(port: number) {
    this._set('server.port', port);
  }

  /**
   * Sets a callback invoked when the login URL is ready in interactive mode.
   *
   * Use this to display or log the authentication URL for the user.
   *
   * @param onOpen - Callback receiving the authentication URL, or `undefined` to disable.
   */
  setServerOnOpen(onOpen: ((url: string) => void) | undefined) {
    this._set('server.onOpen', onOpen);
  }

  /**
   * Sets a pre-obtained access token for `token_only` mode.
   *
   * @param token - The static access token string.
   */
  setAccessToken(token: string) {
    this._set('accessToken', token);
  }

  /**
   * Sets the callback invoked with the device code response during `device_code` authentication.
   *
   * If not set, the default behaviour is `console.log(response.message)`.
   *
   * @param callback - The callback, or `undefined` to restore the default handler.
   */
  setDeviceCodeCallback(callback: DeviceCodeRequest['deviceCodeCallback'] | undefined) {
    this._set('deviceCodeCallback', callback ?? undefined);
  }

  /**
   * Prepares and finalizes the authentication configuration before validation and use.
   *
   * This method injects the parent authentication provider reference (if available)
   * into the configuration. It is called before `_processConfig` and allows for
   * dynamic or contextual configuration adjustments based on the current module instance.
   *
   * Future maintainers: If additional contextual setup is needed (e.g., injecting
   * dependencies, environment-specific values, or chaining providers), extend this method.
   *
   * @inheritdoc
   * @param init - Initialization arguments, including module references.
   * @param initial - Optional initial configuration values.
   * @returns The prepared configuration object, ready for validation.
   */
  protected _buildConfig(
    init: ConfigBuilderCallbackArgs,
    initial?: Partial<AuthConfig> | undefined,
  ) {
    // Inject the parent auth provider from the current module instance, if present
    this._set('parent', (init.ref as ModulesInstance<[MsalNodeModule]>)?.auth);
    // Call the base builder to finalize the config
    return super._buildConfig(init, initial);
  }

  /**
   * Validates and processes the authentication configuration before use.
   *
   * This method ensures that all required properties are present and correctly typed
   * for the selected authentication mode. Throws descriptive errors if configuration
   * is incomplete or invalid, helping catch misconfigurations early.
   *
   * Future maintainers: Update this logic if new authentication modes or required
   * properties are introduced. Keep error messages clear to aid debugging.
   *
   * @inheritdoc
   * @param config - The authentication configuration object to validate.
   * @returns The validated configuration object.
   * @throws Error if required properties are missing or invalid for the selected mode.
   */
  async _processConfig(config: AuthConfig): Promise<AuthConfig> {
    // Validate the required properties for the selected authentication mode
    switch (config.mode) {
      case 'interactive': {
        // Interactive mode requires a valid MSAL client instance
        if (config.client instanceof PublicClientApplication === false) {
          throw new Error('Client is required when mode is interactive');
        }
        // Server configuration must be present
        if (!config.server) {
          throw new Error('Server is required when mode is interactive');
        }
        // Server port must be a number
        if (typeof config.server.port !== 'number') {
          throw new Error('Server port must be a number when mode is interactive');
        }
        break;
      }
      case 'silent': {
        // Silent mode requires a valid MSAL client instance
        if (config.client instanceof PublicClientApplication === false) {
          throw new Error('Client is required when mode is silent');
        }
        break;
      }
      case 'device_code': {
        // Device code mode requires a valid MSAL client instance
        if (config.client instanceof PublicClientApplication === false) {
          throw new Error('Client is required when mode is device_code');
        }
        break;
      }
      case 'token_only': {
        // Token only mode requires a string access token
        if (typeof config.accessToken !== 'string') {
          throw new Error('Access token is required when mode is token_only');
        }
        break;
      }
      // If new modes are added, ensure validation is implemented here
    }
    // Return the validated config for use by the module
    return config;
  }
}
