import { getCode } from '../input/http.js';
import { distance } from '../input/distance.js';
import { parse as parseJWT } from '../model/jwt.js';
import { pkce, state as getState, getRedirectUri } from '../model/oauth.js';
import { getCertificate } from '../model/certificate.js';
import { getServiceKey } from '../model/service-key.js';
import { warning, header } from '../output/style.js';
import { getInfo } from '../srv/get-info.js';
import { getPasscode } from '../srv/get-passcode.js';
import { getToken } from '../srv/get-token.js';
import { createProfile } from '../model/configuration.js';

const extractApptid = (profile) => {
    const url = new URL(profile.server_url);
    if (url.searchParams.has('apptid')) {
        profile.apptid = url.searchParams.get('apptid');
        url.searchParams.delete('apptid');
        profile.server_url = url.toString();
    } else if (profile.token?.access_token) {
        const jwt = parseJWT(profile.token.access_token);
        profile.apptid = jwt.app_tid ?? jwt.ext_attr?.subaccountid;
    }
    return profile.apptid;
};

const isUntrustedUrl = (config, url) => {
    const defaultDomain = 'appfront.cloud.sap';
    const { hostname } = new URL(url);
    const domain = hostname.split('.').slice(-3).join('.');
    if (domain === defaultDomain) {
        // default domain
        return false;
    }
    const knownHostnames = Array.from(Object.values(config.profiles).reduce((acc, cur) => {
        if (cur.server_url) {
            acc.add((new URL(cur.server_url)).hostname);
        }
        return acc;
    }, new Set()));
    if (knownHostnames.includes(hostname)) {
        // known hostname
        return false;
    }
    if (distance(domain, defaultDomain) <= 2) {
        // maybe typo?
        return hostname.split('.').slice(0, -3).concat(defaultDomain).join('.');
    }
    const closeToHostname = knownHostnames.find(
        knownHostname => distance(knownHostname, hostname) <=2
    );
    if (closeToHostname) {
        // close to one of known hostnames
        return closeToHostname;
    }
    // unknown
    return true;
};

const getTrustedUrl = async ({ config, console, prompt }, server_url) => {
    const maybeError = isUntrustedUrl(config, server_url);
    if (maybeError) {
        const warningMessage = warning(
            'Provided URI domain is not in the list of known ' +
            'Application Frontend service domains!\n'
        );
        let consent;
        if (typeof maybeError === 'string') {
            console.debug(
                `Provided server_url '${server_url}' is unknown, `+
                `but is similar to known hostname '${maybeError}'. ` +
                'Prompting user consent to trust provided URL or fix it'
            );
            consent = await prompt({
                message: warningMessage +
                `It has domain similar to known trusted domain '${maybeError}'\n` +
                'Are you sure that URL you provided is correct and should be trusted or ' +
                'you would like to fix it and use proposed domain that is already known?\n' +
                `${header('f')} - fix\n` +
                `${header('y')} - yes\n` +
                `${header('n')} - no\n` +
                'Type your answer'
            });
            if (consent === 'f') {
                const url = new URL(server_url);
                url.hostname = maybeError;
                server_url = url.toString();
            }
        } else {
            console.debug(
                `Provided server_url '${server_url}' is unknown.`+
                'Prompting user consent to trust provided URL'
            );
            consent = await prompt({
                message: warningMessage +
                'Are you sure that it is correct and should be trusted?\n' +
                `${header('y')} - yes\n` +
                `${header('n')} - no\n` +
                'Type your answer'
            });
        }
        console.debug(`User reply to consent prompt: '${consent}'`);
        if (consent === 'n') {
            const err = new Error('Untrusted URL');
            throw err;
        }
    } else {
        console.debug('Provided server_url is known and trusted');
    }
    return server_url;
};

export const getMetadata = () => ({
    name: 'login',
    desc: 'Login with named user',
    usage: 'afctl login [-a URI] [-u USERNAME] [-p PASSWORD] ' +
        '[--sso | --p12 DER | --key KEY --cert CERT | --service-key SERVICE_KEY] [PROFILE]',
    options: {
        api: {
            keys: ['--api', '-a'],
            type: 'string',
            mandatory: false,
            multiple: false,
            helpLiteral: 'URI',
            helpText: 'Application Frontend API server URI',
            index: 0
        },
        username: {
            keys: ['--username', '-u'],
            type: 'string',
            mandatory: false,
            multiple: false,
            helpLiteral: 'USERNAME',
            helpText: 'User name',
            index: 1
        },
        password: {
            keys: ['--password', '-p'],
            type: 'string',
            mandatory: false,
            multiple: false,
            helpLiteral: 'PASSWORD',
            helpText: 'Password',
            index: 2
        },
        sso: {
            keys: ['--sso'],
            type: 'boolean',
            mandatory: false,
            multiple: false,
            helpLiteral: '',
            helpText: 'Single sign-on login (requires browser)',
            index: 3
        },
        p12: {
            keys: ['--p12'],
            type: 'string',
            mandatory: false,
            multiple: false,
            helpLiteral: 'DER',
            helpText: 'P12 certificate bundle (--password will be used to open bundle)',
            index: 4
        },
        key: {
            keys: ['--key'],
            type: 'string',
            mandatory: false,
            multiple: false,
            helpLiteral: 'KEY',
            helpText: 'PEM private key',
            index: 5
        },
        cert: {
            keys: ['--cert'],
            type: 'string',
            mandatory: false,
            multiple: false,
            helpLiteral: 'CERT',
            helpText: 'PEM certificate',
            index: 6
        },
        serviceKey: {
            keys: ['--service-key'],
            type: 'string',
            mandatory: false,
            multiple: false,
            helpLiteral: 'SERVICE_KEY',
            helpText: 'Application Frontend service key',
            index: 7
        },
        profile: {
            keys: [],
            type: 'string',
            mandatory: false,
            multiple: false,
            helpLiteral: 'PROFILE',
            helpText: 'Name of the login profile',
            index: 8
        }
    }
});

export const run = async ({ console, prompt, fetch, fs, args, config, isAppStudio = false }) => {
    let isUrlPrompted = false;
    let profile = config.profiles[config.active_profile];
    let username = args.username;
    let password = args.password;

    // Profile argument provided
    if (args.profile && (args.profile !== config.active_profile)) {
        console.debug(`Setting active profile name to '${args.profile}'`);
        config.active_profile = args.profile;
        // New profile
        if (!config.profiles[config.active_profile]) {
            console.debug(`Creating new profile ${args.profile}`);
            profile = config.profiles[config.active_profile] = createProfile();
        }
    }

    // Login with service key
    if (args.serviceKey) {
        if (args.sso) {
            console.debug('Starting login with SSO, using service key');
            // Parse service key
            const serviceKey = await getServiceKey({ console, fs, path: args.serviceKey });
            profile.server_url = serviceKey.content_endpoint;
            // Get authorization code
            const state = getState();
            const code = await getCode({ console, serviceKey, state });
            // Get token
            profile.token = {
                access_token: (await getToken({
                    console, profile, fetch,
                    serviceKey,
                    code,
                    redirectUri: 'http://localhost:8080/'
                })).access_token
            };
        } else {
            console.debug('Starting non-interactive login with service key');
            const serviceKey = await getServiceKey({ console, fs, path: args.serviceKey });
            profile.server_url = serviceKey.content_endpoint;
            // Get token
            profile.token = {
                access_token: (await getToken({ console, profile, fetch, serviceKey })).access_token
            };
        }
        extractApptid(profile);
        profile.service_key = args.serviceKey;
        await config.save();
        return;
    }

    // Server URL argument provided
    if (args.api && (args.api !== profile.server_url)) {
        // [SEC-278]
        const server_url = await getTrustedUrl({ console, prompt, config }, args.api);
        console.debug(`Setting active profile server_url to '${server_url}'`);
        profile.server_url = server_url;
    }

    // Try to extract username from JWT
    if (!username && profile?.token?.access_token) {
        console.debug('User name is not provided, but previous access token is available');
        const jwt = parseJWT(profile.token.access_token);
        if (jwt.email) {
            console.debug('Using username extracted from \'email\' claim of previous access token');
            username = jwt.email;
        }
    }

    // Prompt server URL, if needed
    if (!profile.server_url) {
        console.debug('Prompting to get active profile server_url');
        let server_url = await prompt({
            message: 'Application Frontend service API server URI'
        });
        // [SEC-278]
        server_url = await getTrustedUrl({ console, prompt, config }, server_url);
        console.debug(`Setting active profile server_url to '${server_url}'`);
        profile.server_url = server_url;
        isUrlPrompted = true;
    }

    // Login with SSO (OAuth authorization code grant - PKCE)
    if (args.sso) {
        console.debug('Starting login with SSO');
        extractApptid(profile);
        const info = await getInfo({ console, profile, fetch });
        console.debug('Generating code challenge and verifier');
        const state = getState();
        const { codeChallenge, codeVerifier } = pkce();
        console.debug(`Using codeChallenge='${codeChallenge}', codeVerifier='${codeVerifier} and state='${state}'`);
        // Get authorization code
        const redirectUri = await getRedirectUri({fetch, isAppStudio});
        const code = await getCode({ console, profile, info, state, codeChallenge, redirectUri });
        // Get token
        const url = new URL(
            (info.issuer.startsWith('http://') || info.issuer.startsWith('https://')) ?
                info.issuer :
                'https://' + info.issuer
        );
        profile.token = {
            access_token: (await getToken({
                console, profile, fetch,
                issuerUrl: url.origin,
                code,
                codeVerifier,
                clientId: info.client_id,
                redirectUri,
                apptid: profile.apptid
            })).access_token
        };
        await config.save();
        return;
    }

    // Login with KEY + CERT
    if (!Object.keys(args).some(arg => ['username', 'password', 'p12', 'sso'].includes(arg))) {
        const key = args.key ?? profile.key;
        const cert = args.cert ?? profile.cert;
        // Non-interactive
        if (key && cert) {
            console.debug('Starting non-interactive login with private key and certificate');
            extractApptid(profile);
            const info = await getInfo({ console, profile, fetch });
            const { subject: { CN: username } } = await getCertificate({ console, fs, cert });
            const passcode = await getPasscode({ console, info, fetch, key, cert });
            profile.token = {
                access_token: (await getToken({ console, profile, fetch, username, password: passcode })).access_token
            };
            profile.key = key;
            profile.cert = cert;
            await config.save();
            return;
        }
    }

    // Login with P12
    if (args.p12) {
        // Certificate bundle password
        if (!password) {
            console.debug('Prompting to get certificate bundle password');
            password = await prompt({
                message: 'Password',
                type: 'password'
            });
            console.debug('Certificate bundle password provided.');
        }
        console.debug('Starting non-interactive login with certificate bundle and password');
        extractApptid(profile);
        const info = await getInfo({ console, profile, fetch });
        const { subject: { CN: username } } = await getCertificate({ console, fs, p12: args.p12, password });
        const passcode = await getPasscode({ console, info, fetch, p12: args.p12, password });
        profile.token = {
            access_token: (await getToken({ console, profile, fetch, username, password: passcode })).access_token
        };
        await config.save();
        return;
    }

    // Server URL
    if(!isUrlPrompted && !args.api) {
        console.debug('Prompting to get server_url');
        profile.server_url = await prompt({
            message: 'Application Frontend service API server URI',
            default: profile.server_url
        });
        console.debug(`Setting active profile server_url to '${profile.server_url}'`);
    }

    // Username
    if (!args.username) {
        console.debug('Prompting to get username');
        username = await prompt({
            message: 'Username',
            default: username
        });
        console.debug(`Username provided: '${username}'`);
    }

    // Password
    if (!password) {
        console.debug('Prompting to get password');
        password = await prompt({
            message: 'Password',
            type: 'password'
        });
        console.debug('Password provided.');
    }

    // Login with username + password
    console.debug('Getting token using username and password');
    extractApptid(profile);
    profile.token = {
        access_token: (await getToken({ console, profile, fetch, username, password })).access_token
    };
    await config.save();

    return;
};
