#!/usr/bin/env node
/*
    secrets management program

    Secrets are stored encrypted on S3 in a bucket called ${profile}.config.
    The secrets are defined in gitlab/keys and are copied to s3 via "MakeMe"
 */

const USAGE = 'Usage: secrets --aws-profile profile [--env prefix] [--key file] [--key-string key] [--profile profile] [set filename| get fields...]'

require('module').Module._initPaths()

var Spawn = require('child_process')
var Fs = require('fs')
var Crypto = require('crypto')

const IV_LENGTH = 16
const CIPHER = 'aes-256-gcm'
const SECRETS_FILE = 'secrets.json'
const SALT = 'dead-sea:'
const RECURSE_LIMIT = 75

var awsProfile = null
var env = null
var json = false
var command
var fields
var key = "/etc/farm/secret.key"
var keyString
var profile

global.dump = (...args) => { for (let item of args) print(JSON.stringify(item, null, 4)) }
global.print = (...args) => console.log(...args)


function usage() {
    process.stderr.write(USAGE + '\n')
    process.exit(1)
}

function parseArgs() {
    let argv = process.argv.slice(2)
    for (let i = 0; i < argv.length; i++) {
        let arg = argv[i]
        if (arg == '--env') {
            env = argv[++i]

        } else if (arg == '-a' || arg == '--aws-profile') {
            awsProfile = argv[++i]

        } else if (arg == '-j' || arg == '--json') {
            json = true

        } else if (arg == '-k' || arg == '--key') {
            key = argv[++i]

        } else if (arg == '--key-string') {
            keyString = argv[++i]

        } else if (arg == '-p' || arg == '--profile') {
            profile = argv[++i]

        } else {
            argv = argv.slice(i)
            break
        }
    }
    command = argv[0]
    if (!command || !awsProfile) {
        usage()
    }
    if (command == 'get') {
        fields = argv.slice(1)
    } else if (command == 'set') {
        filename = argv[1]
        if (!filename) {
            usage()
        }
    }
}

function decrypt(text, password, inCode = 'base64', outCode = 'utf8') {
    if (text) {
        let [cipher, tag, iv, data] = text.split(':')
        iv = Buffer.from(iv, inCode)
        let secret = Crypto.createHash('sha256').update(SALT + password, 'utf8').digest()
        let crypt = Crypto.createDecipheriv(CIPHER, secret, iv)
        if (tag) {
            tag = Buffer.from(tag, inCode)
            crypt.setAuthTag(tag)
        }
        text = crypt.update(data.toString(), inCode, outCode) + crypt.final(outCode)
    }
    return JSON.parse(text.toString())
}

function encrypt(text, password, inCode = 'utf8', outCode = 'base64') {
    if (text) {
        let iv = Crypto.randomBytes(IV_LENGTH)
        let secret = Crypto.createHash('sha256').update(SALT + password, 'utf8').digest()
        let crypt = Crypto.createCipheriv(CIPHER, secret, iv)
        let crypted = crypt.update(text, inCode, outCode) + crypt.final(outCode)
        let tag = (CIPHER.indexOf('-gcm') > 0) ? crypt.getAuthTag().toString(outCode) : ''
        text = `${CIPHER}:${tag}:${iv.toString(outCode)}:${crypted}`
    }
    return text
}

function getSecrets() {
    let args = ['s3', 'cp', `s3://${awsProfile}.config/${awsProfile}/${SECRETS_FILE}`, '-', '--profile', awsProfile]
    let cmd = Spawn.spawnSync('aws', args)
    if (cmd.status != 0) {
        process.stderr.write(`Command failure: aws ${args.join(' ')}\n`)
        process.stderr.write(cmd.stderr.toString() + '\n')
        process.exit(2)
    }
    let data = cmd.stdout.toString()
    let password = (keyString) ? keyString : Fs.readFileSync(key)
    data = decrypt(data, password)
    return data
}

async function setSecrets() {
    let password = (keyString) ? keyString : Fs.readFileSync(key)
    let data = Fs.readFileSync(filename)
    JSON.parse(data)
    data = encrypt(data, password)
    await new Promise((resolve, reject) => {
        let args = ['s3', 'cp', '-', `s3://${awsProfile}.config/${awsProfile}/${SECRETS_FILE}`, '--profile', awsProfile]
        let cmd = Spawn.spawn('aws', args)
        cmd.stdin.write(data)
        cmd.stdin.end()
        cmd.stderr.on('data', (data) => {
            process.stderr.write(`Command failure: aws ${args.join(' ')}\n`)
            process.stderr.write(data + '\n')
        })
        cmd.on('close', (status) => {
            if (status != 0) {
                reject('aws command failed')
            } else {
                resolve(true)
            }
        })
    })
}

function printEnv(obj, prefix = '', vars = {}) {
    try {
        for (let name of Object.keys(obj)) {
            let value = obj[name]
            if (name == 'profiles') {
                continue
            }
            if (typeof value == 'object') {
                printEnv(value, prefix + name.toUpperCase() + '_', vars)
            } else {
                name = (prefix + name).toUpperCase().replace(/\./g, '_').replace(/-/g, '_')
                // vars[name] = value
                print('export ' + name + '="' + value + '"')
            }
        }
    } catch (e) {
        process.stderr.write("CATCH" + e.toString() + '\n')
        process.exit(2)
    }
}

async function run() {
    if (command == 'get') {
        let data = getSecrets()
        if (profile) {
            data = blendProfile(data, profile)
        }
        data = selectFields(data, fields)
        if (json) {
            dump(data)
        } else if (env != null) {
            printEnv(data, env)
        } else {
            print(data)
        }
    } else if (command == 'set') {
        await setSecrets()
    } else {
        usage()
    }
}

function blendProfile(obj, fields) {
    if (obj.profiles[profile]) {
        obj = blend(obj, obj.profiles[profile])
    }
    return obj
}

function selectFields(obj, fields) {
    let result = {}
    if (!fields || fields.length == 0) {
        return obj
    }
    for (field of Object.values(fields)) {
        let set = obj
        for (let part of field.split('.')) {
            set = set[part]
            if (!set) {
                break
            }
        }
        let type = typeof set
        if (type == 'string' || type == 'boolean' || type == 'number' || set == null) {
            result = set
            break
        } else {
            result = Object.assign(result, set)
        }
    }
    return result
}

function cleanup() {
}

async function main() {
    parseArgs()
    await run()
    cleanup()
}

async function start() {
    try {
        await main()
    } catch (e) {
        process.stderr.write(e.toString() + '\n')
        process.exit(1)
    }
}

start()

function blend(dest, src, combine = '', recurse = 0) {
    if (recurse > RECURSE_LIMIT) {
        return
    }
    if (!src) {
        return dest
    }
    if (!dest || typeof dest != 'object' || Array.isArray(dest)) {
        return dest
    }
    for (let key of Object.getOwnPropertyNames(src)) {
        let property = key
        let op = key[0]
        if (op == '+') {
            property = key.slice(1)
        } else if (op == '-') {
            property = key.slice(1)
        } else if (op == '?') {
            property = key.slice(1)
        } else if (op == '=') {
            property = key.slice(1)
        } else if (combine) {
            op = combine
        } else {
            /* Default is to blend objects and assign arrays */
            op = ''
        }
        let s = src[key]
        let d = dest[property]
        if (!dest.hasOwnProperty(property)) {
            if (op == '-') {
                continue
            }
            dest[property] = clone(s)
            continue
        } else if (op == '?' && d != null) {
            continue
        }
        if (Array.isArray(d)) {
            if (op == '=') {
                /* op == '=' */
                dest[property] = clone(s)
            } else if (op == '-') {
                if (Array.isArray(s)) {
                    for (let item of s) {
                        let index = d.indexOf(item)
                        if (index >= 0) d.slice(index, 1)
                    }
                } else {
                    let index = d.indexOf(s)
                    if (index >= 0) d.slice(index, 1)
                }
            } else if (op == '+') {
                /*
                    This was the default, but blending Package.sensors.http.path from PackageOverride needs to
                    overwrite and not union.
                 */
                if (Array.isArray(s)) {
                    for (let item of s) {
                        if (d.indexOf(s) < 0) d.push(item)
                    }
                } else {
                    d.push(s)
                }
            } else {
                dest[property] = clone(s)
            }
        } else if (d instanceof Date) {
            if (op == '+') {
                dest[property] += s
            } else if (op == '-') {
                dest[property] -= s
            } else {
                /* op == '=' */
                dest[property] = s
            }
        } else if (typeof d == 'object' && d !== null && d !== undefined) {
            if (op == '=') {
                dest[property] = clone(s)
            } else if (op == '-') {
                delete dest[property]
            } else if (s === null) {
                dest[property] = s
            } else if (typeof s == 'object') {
                blend(d, s, op, recurse + 1)
            } else {
                dest[property] = s
            }
        } else if (typeof d == 'string') {
            if (op == '+') {
                dest[property] += ' ' + s
            } else if (op == '-') {
                if (d == s) {
                    delete dest[property]
                } else {
                    dest[property] = d.replace(s, '')
                }
            } else {
                /* op == '=' */
                dest[property] = s
            }
        } else if (typeof d == 'number') {
            if (op == '+') {
                dest[property] += s
            } else if (op == '-') {
                dest[property] -= s
            } else {
                /* op == '=' */
                dest[property] = s
            }
        } else {
            if (op == '=') {
                dest[property] = s
            } else if (op == '-') {
                delete dest[property]
            } else {
                dest[property] = s
            }
        }
    }
    return dest
}
