/**
 * @fileOverview
 *
 * Implements the NTLM over HTTP specification: [MS-NTHT] https://msdn.microsoft.com/en-us/library/cc237488.aspx
 * Also see [MS-NLMP]: https://msdn.microsoft.com/en-us/library/cc236621.aspx
 *
 * @note NTLM supports a number of different variations, where an actual TCP connection is signed etc. This file
 * does _not_ implement those cases.
 */

var ntlmUtil = require('httpntlm').ntlm,
    _ = require('lodash'),

    EMPTY = '',
    NTLM = 'NTLM',
    STATE = 'state',
    NEGOTIATE = 'negotiate',
    NTLM_HEADER = 'ntlmHeader',
    AUTHORIZATION = 'Authorization',
    WWW_AUTHENTICATE = 'www-authenticate',
    DISABLE_RETRY_REQUEST = 'disableRetryRequest',
    NTLM_PARAMETERS = {
        DOMAIN: 'domain',
        WORKSTATION: 'workstation',
        USERNAME: 'username',
        PASSWORD: 'password'
    },
    STATES = {
        INITIALIZED: 'INITIALIZED',
        T1_MSG_CREATED: 'T1_MSG_CREATED',
        T3_MSG_CREATED: 'T3_MSG_CREATED'
    };

/**
 * Parses the username to separate username and domain. It can handle two formats:
 *   - Down-Level Logon name format `DOMAIN\USERNAME`
 *   - User Principal Name format `USERNAME@DOMAIN`
 *
 * @private
 * @param {String} username - Username string to parse from
 * @return {Object} - An object with `username` and `domain` fields, which are `strings`.
 */
function parseParametersFromUsername (username) {
    var dllParams,
        upnParams;

    if (!(username && typeof username === 'string')) {
        return {
            username: EMPTY,
            domain: EMPTY
        };
    }

    dllParams = username.split('\\');
    upnParams = username.split('@');

    // username should be either of the two formats, not both
    if (dllParams.length > 1 && upnParams.length > 1) {
        return {
            username,
            domain: EMPTY
        };
    }

    // try to parse from "down level logon" format
    if (dllParams.length === 2 && dllParams[0] && dllParams[1]) {
        return {
            username: dllParams[1],
            domain: dllParams[0]
        };
    }

    // try to parse from "user principal name" format
    if (upnParams.length === 2 && upnParams[0] && upnParams[1]) {
        return {
            username: upnParams[0],
            domain: upnParams[1]
        };
    }

    return {
        username,
        domain: EMPTY
    };
}

/**
 * Check if `WWW-Authenticate` header has NTLM challenge.
 *
 * @private
 * @param {*} headers - Postman headers instance
 * @returns {Boolean}
 */
function hasNTLMChallenge (headers) {
    // Case 1: multiple headers
    // - WWW-Authenticate: NTLM
    // - WWW-Authenticate: Negotiate
    if (headers.has(WWW_AUTHENTICATE, NTLM) || headers.has(WWW_AUTHENTICATE, NEGOTIATE)) {
        return true;
    }

    // Case 2: single header
    // - WWW-Authenticate: Negotiate, NTLM
    return String(headers.get(WWW_AUTHENTICATE)).includes(NTLM);
}

/**
 * NTLM auth while authenticating requires negotiateMessage (type 1) and authenticateMessage (type 3) to be stored.
 * Also it needs to know which stage is it in (INITIALIZED, T1_MSG_CREATED and T3_MSG_CREATED).
 * After the first successful authentication, it just relies on the TCP connection, no other state is needed.
 * @todo Currenty we don't close the connection. So there is no way to de-authenticate.
 *
 * @implements {AuthHandlerInterface}
 */
module.exports = {
    /**
     * @property {AuthHandlerInterface~AuthManifest}
     */
    manifest: {
        info: {
            name: 'ntlm',
            version: '1.0.0'
        },
        updates: [
            {
                property: 'Authorization',
                type: 'header'
            }
        ]
    },

    /**
     * Initializes an item (extracts parameters from intermediate requests if any, etc)
     * before the actual authorization step.
     *
     * @param {AuthInterface} auth
     * @param {Response} response
     * @param {AuthHandlerInterface~authInitHookCallback} done
     */
    init: function (auth, response, done) {
        done(null);
    },

    /**
     * Verifies whether the request has valid basic auth credentials (which is always).
     * Sanitizes the auth parameters if needed.
     *
     * @param {AuthInterface} auth
     * @param {AuthHandlerInterface~authPreHookCallback} done
     */
    pre: function (auth, done) {
        !auth.get(STATE) && auth.set(STATE, STATES.INITIALIZED);

        done(null, true);
    },

    /**
     * Verifies whether the basic auth succeeded.
     *
     * @param {AuthInterface} auth
     * @param {Response} response
     * @param {AuthHandlerInterface~authPostHookCallback} done
     */
    post: function (auth, response, done) {
        if (auth.get(DISABLE_RETRY_REQUEST)) {
            return done(null, true);
        }

        var state = auth.get(STATE),
            domain = auth.get(NTLM_PARAMETERS.DOMAIN) || EMPTY,
            workstation = auth.get(NTLM_PARAMETERS.WORKSTATION) || EMPTY,
            username = auth.get(NTLM_PARAMETERS.USERNAME) || EMPTY,
            password = auth.get(NTLM_PARAMETERS.PASSWORD) || EMPTY,
            negotiateMessage, // type 1
            challengeMessage, // type 2
            authenticateMessage, // type 3
            ntlmType2Header,
            parsedParameters;

        if (response.code !== 401 && response.code !== 403) {
            return done(null, true);
        }

        // we try to extract domain from username if not specified.
        if (!domain) {
            parsedParameters = parseParametersFromUsername(username) || {};

            username = parsedParameters.username;
            domain = parsedParameters.domain;
        }

        if (state === STATES.INITIALIZED) {
            // Nothing to do if the server does not ask us for auth in the first place.
            if (!hasNTLMChallenge(response.headers)) {
                return done(null, true);
            }

            // Create a type 1 message to send to the server
            negotiateMessage = ntlmUtil.createType1Message({
                domain: domain,
                workstation: workstation
            });

            // Add the type 1 message as the auth header
            auth.set(NTLM_HEADER, negotiateMessage);

            // Update the state
            auth.set(STATE, STATES.T1_MSG_CREATED);

            // ask runtime to replay the request
            return done(null, false);
        }
        else if (state === STATES.T1_MSG_CREATED) {
            // At this point, we can assume that the type 1 message was sent to the server

            // there can be multiple headers present with key `www-authenticate`.
            // iterate to get the one which has the NTLM hash. if multiple
            // headers have the NTLM hash, use the first one.
            ntlmType2Header = response.headers.find(function (header) {
                return String(header.key).toLowerCase() === WWW_AUTHENTICATE &&
                    header.valueOf().startsWith('NTLM ');
            });

            if (!ntlmType2Header) {
                return done(new Error('ntlm: server did not send NTLM type 2 message'));
            }

            challengeMessage = ntlmUtil.parseType2Message(ntlmType2Header.valueOf(), _.noop);

            if (!challengeMessage) {
                return done(new Error('ntlm: server did not correctly process authentication request'));
            }

            authenticateMessage = ntlmUtil.createType3Message(challengeMessage, {
                domain: domain,
                workstation: workstation,
                username: username,
                password: password
            });

            // Now create the type 3 message, and add it to the request
            auth.set(NTLM_HEADER, authenticateMessage);
            auth.set(STATE, STATES.T3_MSG_CREATED);

            // ask runtime to replay the request
            return done(null, false);
        }
        else if (state === STATES.T3_MSG_CREATED) {
            // Means we have tried to authenticate, so we should stop here without worrying about anything
            return done(null, true);
        }

        // We are in an undefined state
        return done(null, true);
    },

    /**
     * Signs a request.
     *
     * @param {AuthInterface} auth
     * @param {Request} request
     * @param {AuthHandlerInterface~authSignHookCallback} done
     */
    sign: function (auth, request, done) {
        var ntlmHeader = auth.get(NTLM_HEADER);

        request.removeHeader(AUTHORIZATION, {ignoreCase: true});
        ntlmHeader && request.addHeader({
            key: AUTHORIZATION,
            value: ntlmHeader,
            system: true
        });

        return done();
    }
};
