#! /usr/bin/env node

// TODO
// - deduplicate with Vault's seed script at https://github.com/scality/Vault/pull/1627
// - add permission boundaries to user when https://scality.atlassian.net/browse/VAULT-4 is implemented
process.env.AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE = '1';
const { errors } = require('arsenal');
const { program } = require('commander');
const werelogs = require('werelogs');
const async = require('async');
const { IAM } = require('aws-sdk');
const { version } = require('../package.json');

const systemPrefix = '/scality-internal/';


function generateUserPolicyDocument() {
    return {
        Version: '2012-10-17',
        Statement: [
            {
                Sid: 'RateLimitAdminAPIs',
                Effect: 'Allow',
                Action: [
                    'scality:GetBucketRateLimit',
                    'scality:PutBucketRateLimit',
                    'scality:DeleteBucketRateLimit',
                ],
                Resource: 'arn:aws:s3:::bucket/*',
            },
        ],
    };
}

function createIAMClient(opts) {
    return new IAM({
        endpoint: opts.iamEndpoint,
        region: 'us-east-1',
    });
}

function needsCreation(v) {
    if (Array.isArray(v)) {
        return !v.length;
    }

    return !v;
}

class BaseHandler {
    resourceType;

    constructor(serviceName, iamClient, log
    ) {
        this.serviceName = serviceName;
        this.iamClient = iamClient;
        this.log = log;
    }

    collect() {};
    create(values) {};
    conflicts(v) {}

    applyWaterfall(values, done) {
        this.log.debug('applyWaterfall', { values, type: this.resourceType });

        const v = values[this.resourceType];

        if (needsCreation(v)) {
            this.log.debug('creating', { v, type: this.resourceType });
            return this.create(values)
                .then(res =>
                    done(
                        null,
                        Object.assign(values, {
                            [this.resourceType]: res,
                        }),
                    ),
                )
                .catch(done);
        }

        this.log.debug('conflicts check', { v, type: this.resourceType });
        if (this.conflicts(v)) {
            return done(
                errors.EntityAlreadyExists.customizeDescription(
                    `${this.resourceType} ${this.serviceName} already exists and conflicts with the expected value.`,
                ),
            );
        }

        this.log.debug('nothing to do', { v, type: this.resourceType });
        return done(null, values);
    }
}

class UserHandler extends BaseHandler {
    resourceType = 'user';

    async collect() {
        return this.iamClient
            .getUser({
                UserName: this.serviceName,
            })
            .promise()
            .then(res => res.User);
    }

    async create() {
        return this.iamClient
            .createUser({
                UserName: this.serviceName,
                Path: systemPrefix,
            })
            .promise()
            .then(res => res.User);
    }

    conflicts(u) {
        return u.Path !== systemPrefix;
    }
}

class UserPolicyHandler extends BaseHandler {
    resourceType = 'userPolicy';

    async collect() {
        return this.iamClient
            .getUserPolicy({
                UserName: this.serviceName,
                PolicyName: this.serviceName,
            })
            .promise()
            .then(() => true);
    }

    async create() {
        const doc = generateUserPolicyDocument();

        return this.iamClient
            .putUserPolicy({
                UserName: this.serviceName,
                PolicyName: this.serviceName,
                PolicyDocument: JSON.stringify(doc),
            })
            .promise()
            .then(() => true);
    }

    conflicts() {
        return false;
    }
}

class AccessKeyHandler extends BaseHandler {
    resourceType = 'accessKey';

    async collect() {
        // if at least one key already exists, the script won't create a new key
        // and will display the list of existing keys
        return this.iamClient
            .listAccessKeys({
                UserName: this.serviceName,
                MaxItems: 100,
            })
            .promise()
            .then(res => res.AccessKeyMetadata);
    }

    async create() {
        return this.iamClient
            .createAccessKey({
                UserName: this.serviceName,
            })
            .promise()
            .then(res => res.AccessKey);
    }

    conflicts() {
        return false;
    }
}

function collectResource(v, done) {
    v.collect()
        .then(res => done(null, res))
        .catch((err) => {
            if (err.code === 'NoSuchEntity') {
                return done();
            }

            return done(err);
        });
}

function collectResourcesFromHandlers(handlers, cb) {
    const tasks = handlers.reduce(
        (acc, v) => ({
            [v.resourceType]: (done) => collectResource(v, done),
            ...acc,
        }),
        {},
    );
    async.parallel(tasks, cb);
}

function buildServiceUserHandlers(serviceName, client, log) {
    return [UserHandler, UserPolicyHandler, AccessKeyHandler].map(Handler => new Handler(serviceName, client, log));
}

function apply(client, serviceName, log, cb) {
    const handlers = buildServiceUserHandlers(serviceName, client, log);

    async.waterfall(
        [
            (done) => collectResourcesFromHandlers(handlers, done),
            ...handlers.map(h => h.applyWaterfall.bind(h)),
            (values, done) => done(null, values.accessKey),
        ],
        cb,
    );
}

function wrapAction(actionFunc, serviceName, options) {
    werelogs.configure({
        level: options.logLevel,
        dump: options.logDumpLevel,
    });

    const log = new werelogs.Logger(process.argv[1]).newRequestLogger();
    const client = createIAMClient(options);
    actionFunc(client, serviceName, log, (err, data) => {
        if (err) {
            log.error('failed', {
                data,
                error: err,
            });
            if (err.EntityAlreadyExists) {
                log.error(`run "${process.argv[1]} purge ${serviceName}" to fix.`);
            }
            process.exit(1);
        }
        log.info('success', { data });
        process.exit();
    });
}

program.version(version);

[
    {
        name: 'apply <service-name>',
        actionFunc: apply,
    },
].forEach(cmd => {
    program
        .command(cmd.name)
        .option('--iam-endpoint <url>', 'IAM endpoint', 'http://localhost:8600')
        .option('--log-level <level>', 'log level', 'info')
        .option('--log-dump-level <level>', 'log level that triggers a dump of the debug buffer', 'error')
        .action(wrapAction.bind(null, cmd.actionFunc));
});

const validCommands = program.commands.map(n => n._name);

// Is the command given invalid or are there too few arguments passed
if (!validCommands.includes(process.argv[2])) {
    program.outputHelp();
    process.stdout.write('\n');
    process.exit(1);
} else {
    program.parse(process.argv);
}
