#!/usr/bin/env node

/*
    ___ usage ___ en_US ___
    usage: prolific <options> <program>

    options:

        -i, --inherit   <number>
            file handles to inherit

        -s, --scram     <number>
            number of seconds to wait for children to exit

        -p, --processor <path>
            path to processor

        --help
            display this message

    ___ $ ___ en_US ___

        log is required:
            the `--log` address and port is a required argument

        port is not an integer:
            the `--log` port must be an integer

    ___ . ___
*/

// Node.js API.
const assert = require('assert')
const path = require('path')
const url = require('url')
const children = require('child_process')
const crypto = require('crypto')
const fs = require('fs').promises

const once = require('prospective/once')
const callback = require('prospective/callback')

const rimraf = require('rimraf')

const fnv = require('hash.fnv')

const Isochronous = require('isochronous')

// Route messages through a process hierarchy using Node.js IPC.
const Descendent = require('descendent')

// Exceptions that you can catch by type.
const Interrupt = require('interrupt').create('prolific')

// Command line and environment interpretation utilities.
const inherit = require('prolific.inherit')

// Monitoring of streams that contain logging messages.
const Collector = require('prolific.collector')
const Watcher = require('prolific.watcher')

// Pass messages and sockets all around the process tree.
const descendent = require('foremost')('descendent')

const coalesce = require('extant')

const Tmp = require('./tmp')
const Killer = require('./killer')

// TODO Note that; we now require that anyone standing between a root Prolific
// sidecar and a leaf child process use the Descendent library. When I write
// multi-process applications I use [Olio](https://github.com/bigeasy/olio)
// where this is already implemented.
require('arguable')(module, {}, async arguable => {
    arguable.ultimate.ipc = true

    arguable.helpIf(arguable.ultimate.help)

    const processor = arguable.ultimate.processor

    process.env.PROLIFIC_SUPERVISOR_PROCESS_ID = process.pid

    const sidecars = {}
    const pids = {}
    const pipes = {} // See race condition below.

    const supervise = {
        sidecar: async (sidecar, pid) => {
            const [ exitCode, signal ] = await once(sidecar, 'exit').promise
            Interrupt.assert(exitCode == 0, 'sidecar.exit', {
                exitCode: exitCode,
                signal: signal,
                argv: arguable.argv
            })
            delete sidecars[pid]
        }
    }

    const exited = { '0': 0 }

    descendent.increment()
    try {
        // TODO What do you really want to name this?
        const Destructible = require('destructible')
        const destructible = new Destructible(1500, 'prolific')

        const tmp = await Tmp(coalesce(process.env.TMPDIR, '/tmp'), async () => {
            const [ bytes ] = await callback(callback => crypto.randomBytes(16, callback))
            return bytes.toString('hex')
        }, process.pid)
        destructible.destruct(() => callback(callback => rimraf(tmp, callback)))

        await fs.mkdir(path.resolve(tmp, 'stage'))
        await fs.mkdir(path.resolve(tmp, 'publish'))

        process.env.PROLIFIC_TMPDIR = tmp

        const toucher = new Isochronous(60000, true, async () => {
            const now = Date.now()
            await fs.utimes(tmp, now, now)
            await fs.utimes(path.resolve(tmp, 'stage'), now, now)
            await fs.utimes(path.resolve(tmp, 'publish'), now, now)
        })

        destructible.durable('toucher', toucher.start())
        destructible.destruct(() => toucher.stop())

        const killer = new Killer(100, 30000)

        killer.on('killed', pid => {
            watcher.killed(pid)
        })

        destructible.durable('killer', killer.run())
        destructible.destruct(() => killer.destroy())

        const watcher = new Watcher(destructible.durable('watcher'), buffer => {
            return fnv(0, buffer, 0, buffer.length)
        }, path.join(tmp, 'publish'))

        const collector = new Collector

        // watcher.on('notice', notice => console.log(notice))

        watcher.on('data', data => collector.data(data))

        collector.on('data', data => {
            killer.purge()
            const pid = data.pid
            switch (data.body.method) {
            case 'start': {
                    const sidecar = children.spawn('node', [
                        path.join(__dirname, 'sidecar.bin.js'),
                        '--configuration', processor,
                        '--supervisor', process.pid
                    ], {
                        stdio: [ 'inherit', 'inherit', 'inherit', 'pipe', 'ipc' ]
                    })
                    sidecars[pid] = sidecar
                    pipes[pid] = sidecar.stdio[3]
                    descendent.addChild(sidecar, { path: data.path, pid: pid })
                    destructible.durable([ 'sidecar', sidecar.pid ], supervise.sidecar(sidecar, pid))
                }
                break
            case 'eos': {
                    killer.exited(pid)
                    // TODO No need to `killer.purge()`, we can absolutely remove
                    // the pid from the `Killer` here.
                    const sidecar = sidecars[pid]
                    descendent.down([ sidecar.pid ], 'prolific:synchronous', null)
                }
                break
            default: {
                    killer.exit(pid)
                    const sidecar = sidecars[pid]
                    // **TODO** Wait on callback?
                    descendent.down([ sidecar.pid ], 'prolific:synchronous', data.body)
                }
                break
            }
        })

        const stdio = inherit(arguable)
        stdio.push('ipc')

        const argv = arguable.argv.slice()
        // TODO Restore inheritance.
        const child = children.spawn(argv.shift(), argv, { stdio: stdio })
        // TODO Maybe have something to call to notify of failure to finish.
        // destructible.destruct(() => child.kill())

        descendent.addChild(child, null)

        descendent.on('prolific:pipe', function (message) {
            const pid = message.cookie.path[message.cookie.path.length - 1]
            descendent.down(message.cookie.path, 'prolific:pipe', true, coalesce(pipes[pid]))
        })

        const close = async () => {
            const [ exitCode, signal ] = await once(child, 'exit').promise
            // Will only ever equal zero. We do not have the `null, "SIGTERM"`
            // combo because we always register a `SIGTERM` handler. The
            // `"SIGTERM"` response is only when the default hander fires. The
            // `"SIGTERM"` is determined by whether or not the child has a
            // `"SIGTERM"` handler, not by any action by the parent. (i.e.
            // whether or not the parent calles `child.kill()`. The behavior is
            // still the same if we send a kill signal from the shell.
            Interrupt.assert(exitCode == 0, 'child.exit', {
                exitCode: exitCode,
                signal: signal,
                argv: arguable.argv
            })
        }

        destructible.ephemeral('close', close())

        await arguable.destroyed
        destructible.destroy()
        await destructible.promise

        return 0
    } finally {
        descendent.decrement()
    }
})

@@ -4385,35 +4385,8 @@
 %3E %7B%0A
-            killer.purge()%0A
     

Remove call to deleted method.