import type { Request } from 'express';
import basicAuth from 'basic-auth';
import type {
  Authenticator as AuthenticatorInterface,
  AuthResponse as NepheleAuthResponse,
} from 'nephele';
import { UnauthorizedError } from 'nephele';

import User from './User.js';

export type AuthenticatorConfig = {
  /**
   * The realm is the name reported by the server when the user is prompted to
   * authenticate.
   *
   * It should be HTTP header safe (shouldn't include double quotes or
   * semicolon).
   */
  realm?: string;
  /**
   * Allow the user to proceed, even if they are not authenticated.
   *
   * The authenticator will advertise that authentication is available, but the
   * user will have access to the server without providing authentication.
   *
   * In the unauthorized state, the `user` presented to the Nephele adapter will
   * have the username "nobody".
   *
   * WARNING: It is very dangerous to allow unauthorized access if write actions
   * are allowed!
   */
  unauthorizedAccess?: boolean;
  /**
   * Comma separated UID ranges that are allowed to log in.
   *
   * You can set, for example, "0,1000-1999" to allow the first 1000 normal
   * users and root to log in.
   *
   * Root is always UID 0. On most systems, daemon users are assigned UIDs in
   * the range 2-999, normal users are assigned UIDs in the range 1000-65533,
   * and the "nobody" user is assigned UID 65534. On some systems (including
   * macOS), normal users are assigned IDs starting at 500, which is why the
   * default includes this range.
   */
  allowedUIDs?: string;
};

export type AuthResponse = NepheleAuthResponse<any, { user: User }>;

/**
 * Nephele PAM authenticator.
 *
 * Read the details on https://www.npmjs.com/package/authenticate-pam, which is
 * required for PAM authentication.
 */
export default class Authenticator implements AuthenticatorInterface {
  realm: string;
  unauthorizedAccess: boolean;
  allowedUIDs: string[];

  constructor({
    realm = 'Nephele WebDAV Service',
    unauthorizedAccess = false,
    allowedUIDs = '500-59999',
  }: AuthenticatorConfig = {}) {
    this.realm = realm;
    this.unauthorizedAccess = unauthorizedAccess;
    this.allowedUIDs = allowedUIDs.split(',').map((range) => range.trim());
  }

  async authenticate(request: Request, response: AuthResponse) {
    const authorization = request.get('Authorization');
    let username = '';
    let password = '';

    if (authorization) {
      const auth = basicAuth.parse(authorization);
      if (auth) {
        username = auth.name;
        password = auth.pass;
      }
    }

    try {
      if (username.trim() === '') {
        throw new UnauthorizedError(
          'Authentication is required to use this server.',
        );
      }

      const user = new User({ username });

      await user.authenticate(password, request.ip);
      await user.checkUID(this.allowedUIDs);

      return user;
    } catch (e: any) {
      if (e instanceof UnauthorizedError) {
        response.set(
          'WWW-Authenticate',
          `Basic realm="${this.realm}", charset="UTF-8"`,
        );
      }

      if (this.unauthorizedAccess) {
        return new User({ username: 'nobody' });
      }

      throw e;
    }
  }

  async cleanAuthentication(_request: Request, _response: AuthResponse) {
    // Nothing is required for auth cleanup.
    return;
  }
}
