import { Request, Response, NextFunction, RequestHandler } from 'express';
import { bg, black, blue, bold, boldOff, reset, rs } from 'af-color';
import { Buffer } from 'buffer';
import { IAuthNtlmOptions, IRsn } from '../interfaces';
import { handleAuthenticate } from './handle-authenticate';
import { handleNegotiate } from './handle-negotiate';
import { debugNtlmAuthFlow, debugNtlmLdapProxyId, hnColor, hvInColor, hvOutColor } from './debug';
import { NTLMMessageParsed, NTLMMessageType, ntlmParse, NTLMType1, NTLMType2, NTLMType3 } from '../ntlm-parser';
import { prepareOptions } from '../prepare-options';
import { arrowR, Larrow } from './lib/constants';
import { transferExistingProps } from './lib/utils';

/**
 * Returns data from the Authorization header: NTLM <data>
 * If they can be parsed, then req.ntlm is filled
 */
const getNtlmAuthorizationData = (req: Request): string | undefined => {
  const [title, data] = req.headers?.authorization?.split(' ') || [];
  if (title === 'NTLM' && data) {
    return data;
  }
};

/**
 * Fills req.ntlm with data from the Authorization header: NTLM <data>
 * If they can be parsed.
 */
const fillReqNtlm = (req: Request, data: string): NTLMMessageParsed => {
  const parsedData = ntlmParse(data, { compact: true }) as NTLMType1 | NTLMType2 | NTLMType3;
  debugNtlmAuthFlow(`Decoded Authorization header: ${hvInColor}${JSON.stringify(parsedData, undefined, 2)}`);
  ['domain', 'username', 'workstation'].forEach((p) => {
    if (parsedData[p]) {
      req.ntlm[p] = parsedData[p];
    }
  });
  return parsedData;
};

export const authNTLM = (authNtlmOptions?: IAuthNtlmOptions): RequestHandler => {
  const options = prepareOptions(authNtlmOptions);
  return async (req: Request, res: Response, next: NextFunction) => {
    const rsn: IRsn = { req, res, next, options };
    let userData = options.getCachedUserData(rsn);
    const uri = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
    const requestedURI = `${arrowR} ${req.method}: ${req.protocol}://${req.get('host')}${req.originalUrl}`;
    const authorizationHeader = req.headers.authorization;
    const uriA = `${requestedURI} : ${authorizationHeader ? `${hnColor}Authorization: ${hvInColor}${
      authorizationHeader || ''}` : `${hnColor}No Authorization header`}`;

    req.ntlm = req.ntlm || { uri };

    const mTitle = `============ Start NTLM Authorization. Strategy: '${options.getStrategy(rsn)}' ==================`;

    // req.ntlm.isAuthenticated must be filled in earlier when determining the presence of a session cookie
    if (userData.isAuthenticated) {
      if (!authorizationHeader || (authorizationHeader && req.method !== 'POST')) {
        const { username, domain } = transferExistingProps({ ...userData, uri }, req.ntlm);
        debugNtlmAuthFlow(`${requestedURI}\nConnection already authenticated / user: ${username} / domain: ${domain}`);
        return next();
      }
      debugNtlmAuthFlow(`The connection is authenticated, but the "Authorization" header sent using the POST method was detected`);
    }

    debugNtlmAuthFlow(uriA);
    if (!authorizationHeader) {
      debugNtlmAuthFlow(mTitle);
      debugNtlmAuthFlow(`${Larrow} Return ${blue}401${reset}: ${hnColor}WWW-Authenticate${blue}: ${hvOutColor}NTLM`);
      return res
        .setHeader('Content-Type', 'text/plain; charset=utf-8')
        .setHeader('WWW-Authenticate', 'NTLM')
        .setHeader('Date', (new Date()).toUTCString())
        .status(401)
        .send('401 UNAUTHORIZED');
    }

    // Returns data from the Authorization header: NTLM <data>
    const ntlmAuthData = getNtlmAuthorizationData(req);

    if (!ntlmAuthData) {
      return options.handleHttpError400(res, `Authorization header does not contain NTLM data. URI ${uri}`);
    }
    // Fills req.ntlm with data from the Authorization header: NTLM <data>.
    const { domain, messageType } = fillReqNtlm(req, ntlmAuthData);
    // Domain names from NTLM messages - we believe
    if (domain) {
      debugNtlmLdapProxyId(`↓ ${domain}`);
      req.ntlm.domain = domain;
    }

    const dataBuf = Buffer.from(ntlmAuthData, 'base64');
    if (messageType === NTLMMessageType.UNKNOWN) {
      return options.handleHttpError400(res, `Incorrect NTLM message Type ${dataBuf.readUInt8(8)}`);
    }

    if (messageType === NTLMMessageType.NEGOTIATE_MESSAGE) {
      return handleNegotiate(rsn, dataBuf).then(() => 0);
    }

    if (messageType === NTLMMessageType.AUTHENTICATE_MESSAGE) {
      const isNoErrors = await handleAuthenticate(rsn, dataBuf);
      if (!isNoErrors) {
        return; // In this case the error has already been sent over HTTP
      }
      userData = options.getCachedUserData(rsn);
      if (!userData.isAuthenticated) {
        return options.handleHttpError403(rsn);
      }
      if (debugNtlmAuthFlow.enabled) {
        // eslint-disable-next-line no-console
        console.log(`\n${bg.lGreen + black}req.ntlm:${bg.def + rs}`, userData, `\n`);
      }
      options.handleSuccessAuthentication(rsn);
      debugNtlmAuthFlow(`${Larrow} handle success authorisation (Default ${bold + reset}next${blue}()${boldOff}${reset})`);
      return;
    }

    return options.handleHttpError400(res, 'NTLM: Unexpected Type 2 message (CHALLENGE) in client request');
  };
};
