import { IncomingMessage, ServerResponse } from 'http';
import urlJoin from 'url-join';
import { strict as assert } from 'assert';
import { Config, LoginOptions } from '../config';
import TransientStore, { StoreOptions } from '../transient-store';
import { encodeState } from '../hooks/get-login-state';
import { ClientFactory } from '../client';
import createDebug from '../utils/debug';

const debug = createDebug('handlers');

function getRedirectUri(config: Config): string {
  return urlJoin(config.baseURL, config.routes.callback);
}

export type HandleLogin = (req: IncomingMessage, res: ServerResponse, options?: LoginOptions) => Promise<void>;

export default function loginHandlerFactory(
  config: Config,
  getClient: ClientFactory,
  transientHandler: TransientStore
): HandleLogin {
  return async (req, res, options = {}) => {
    const client = await getClient();
    const returnTo = options.returnTo || config.baseURL;

    const opts = {
      returnTo,
      getLoginState: config.getLoginState,
      ...options
    };

    // Ensure a redirect_uri, merge in configuration options, then passed-in options.
    opts.authorizationParams = {
      redirect_uri: getRedirectUri(config),
      ...config.authorizationParams,
      ...(opts.authorizationParams || {})
    };

    const transientOpts: StoreOptions = {
      sameSite: opts.authorizationParams.response_mode === 'form_post' ? 'none' : 'lax'
    };

    const stateValue = await opts.getLoginState(req as any, opts);
    if (typeof stateValue !== 'object') {
      throw new Error('Custom state value must be an object.');
    }
    stateValue.nonce = transientHandler.generateNonce();
    stateValue.returnTo = stateValue.returnTo || opts.returnTo;

    const usePKCE = (opts.authorizationParams.response_type as string).includes('code');
    if (usePKCE) {
      debug('response_type includes code, the authorization request will use PKCE');
      stateValue.code_verifier = transientHandler.generateCodeVerifier();
    }

    const authParams = {
      ...opts.authorizationParams,
      nonce: transientHandler.save('nonce', req, res, transientOpts),
      state: transientHandler.save('state', req, res, {
        ...transientOpts,
        value: encodeState(stateValue)
      }),
      ...(opts.authorizationParams.signup ? {
        signup: true
      } : undefined),
      ...(usePKCE
        ? {
          code_challenge: transientHandler.calculateCodeChallenge(
            transientHandler.save('code_verifier', req, res, transientOpts)
          ),
          code_challenge_method: 'S256'
        }
        : undefined)
    };

    const validResponseTypes = ['id_token', 'code id_token', 'code'];
    assert(
      validResponseTypes.includes(authParams.response_type as string),
      `response_type should be one of ${validResponseTypes.join(', ')}`
    );
    assert(/\bopenid\b/.test(authParams.scope as string), 'scope should contain "openid"');

    if (authParams.max_age) {
      transientHandler.save('max_age', req, res, {
        ...transientOpts,
        value: authParams.max_age.toString()
      });
    }

    const authorizationUrl = client.authorizationUrl(authParams);
    debug('redirecting to %s', authorizationUrl);

    res.writeHead(302, {
      Location: authorizationUrl
    });
    res.end();
  };
}
