var util = require('util')
var os = require('os')

var program = require('commander')
var Promise = require('bluebird')
var ip = require('my-local-ip')

var pkg = require('../package')
var cliutil = require('./util/cliutil')
var logger = require('./util/logger')

Promise.longStackTraces()

program
  .version(pkg.version)

program
  .command('provider [serial..]')
  .description('start provider')
  .option('-s, --connect-sub <endpoint>'
    , 'sub endpoint'
    , cliutil.list)
  .option('-p, --connect-push <endpoint>'
    , 'push endpoint'
    , cliutil.list)
  .option('-n, --name <name>'
    , 'name (or os.hostname())'
    , String
    , os.hostname())
  .option('--min-port <port>'
    , 'minimum port number for worker use'
    , Number
    , 7400)
  .option('--max-port <port>'
    , 'maximum port number for worker use'
    , Number
    , 7700)
  .option('--public-ip <ip>'
    , 'public ip for global access'
    , String
    , ip())
  .option('-t, --group-timeout <seconds>'
    , 'group timeout'
    , Number
    , 900)
  .option('-r, --storage-url <url>'
    , 'URL to storage client'
    , String)
  .option('--heartbeat-interval <ms>'
    , 'heartbeat interval'
    , Number
    , 10000)
  .option('--adb-host <host>'
    , 'ADB host (defaults to 127.0.0.1)'
    , String
    , '127.0.0.1')
  .option('--adb-port <port>'
    , 'ADB port (defaults to 5037)'
    , Number
    , 5037)
  .option('-R, --allow-remote'
    , 'Whether to allow remote devices to be set up')
  .option('--screen-ws-url-pattern <pattern>'
    , 'screen WebSocket URL pattern'
    , String
    , 'ws://${publicIp}:${publicPort}')
  .action(function() {
    var serials = cliutil.allUnknownArgs(arguments)
      , options = cliutil.lastArg(arguments)

    if (!options.connectSub) {
      this.missingArgument('--connect-sub')
    }
    if (!options.connectPush) {
      this.missingArgument('--connect-push')
    }
    if (!options.storageUrl) {
      this.missingArgument('--storage-url')
    }

    require('./units/provider')({
      name: options.name
    , killTimeout: 10000
    , ports: cliutil.range(options.minPort, options.maxPort)
    , filter: function(device) {
        return serials.length === 0 || serials.indexOf(device.id) !== -1
      }
    , allowRemote: options.allowRemote
    , fork: function(device, ports) {
        var fork = require('child_process').fork
        return fork(__filename, [
          'device', device.id
        , '--provider', options.name
        , '--connect-sub', options.connectSub.join(',')
        , '--connect-push', options.connectPush.join(',')
        , '--screen-port', ports.shift()
        , '--connect-port', ports.shift()
        , '--public-ip', options.publicIp
        , '--group-timeout', options.groupTimeout
        , '--storage-url', options.storageUrl
        , '--adb-host', options.adbHost
        , '--adb-port', options.adbPort
        , '--screen-ws-url-pattern', options.screenWsUrlPattern
        , '--heartbeat-interval', options.heartbeatInterval
        ])
      }
    , endpoints: {
        sub: options.connectSub
      , push: options.connectPush
      }
    , adbHost: options.adbHost
    , adbPort: options.adbPort
    })
  })

program
  .command('device <serial>')
  .description('start device worker')
  .option('-n, --provider <name>'
    , 'provider name'
    , String)
  .option('-s, --connect-sub <endpoint>'
    , 'sub endpoint'
    , cliutil.list)
  .option('-p, --connect-push <endpoint>'
    , 'push endpoint'
    , cliutil.list)
  .option('--screen-port <port>'
    , 'port allocated to the screen websocket'
    , Number)
  .option('--connect-port <port>'
    , 'port allocated to adb connect'
    , Number)
  .option('--public-ip <ip>'
    , 'public ip for global access'
    , String
    , ip())
  .option('-t, --group-timeout <seconds>'
    , 'group timeout'
    , Number
    , 900)
  .option('-r, --storage-url <url>'
    , 'URL to storage client'
    , String)
  .option('--adb-host <host>'
    , 'ADB host (defaults to 127.0.0.1)'
    , String
    , '127.0.0.1')
  .option('--adb-port <port>'
    , 'ADB port (defaults to 5037)'
    , Number
    , 5037)
  .option('--screen-ws-url-pattern <pattern>'
    , 'screen WebSocket URL pattern'
    , String
    , 'ws://${publicIp}:${publicPort}')
  .option('--heartbeat-interval <ms>'
    , 'heartbeat interval'
    , Number
    , 10000)
  .action(function(serial, options) {
    if (!options.connectSub) {
      this.missingArgument('--connect-sub')
    }
    if (!options.connectPush) {
      this.missingArgument('--connect-push')
    }
    if (!options.provider) {
      this.missingArgument('--provider')
    }
    if (!options.screenPort) {
      this.missingArgument('--screen-port')
    }
    if (!options.connectPort) {
      this.missingArgument('--connect-port')
    }
    if (!options.storageUrl) {
      this.missingArgument('--storage-url')
    }

    require('./units/device')({
      serial: serial
    , provider: options.provider
    , publicIp: options.publicIp
    , endpoints: {
        sub: options.connectSub
      , push: options.connectPush
      }
    , groupTimeout: options.groupTimeout * 1000 // change to ms
    , storageUrl: options.storageUrl
    , adbHost: options.adbHost
    , adbPort: options.adbPort
    , screenWsUrlPattern: options.screenWsUrlPattern
    , screenPort: options.screenPort
    , connectPort: options.connectPort
    , heartbeatInterval: options.heartbeatInterval
    })
  })

program
  .command('processor <name>')
  .description('start processor')
  .option('-a, --connect-app-dealer <endpoint>'
    , 'app dealer endpoint'
    , cliutil.list)
  .option('-d, --connect-dev-dealer <endpoint>'
    , 'device dealer endpoint'
    , cliutil.list)
  .action(function(name, options) {
    if (!options.connectAppDealer) {
      this.missingArgument('--connect-app-dealer')
    }
    if (!options.connectDevDealer) {
      this.missingArgument('--connect-dev-dealer')
    }

    require('./units/processor')({
      name: name
    , endpoints: {
        appDealer: options.connectAppDealer
      , devDealer: options.connectDevDealer
      }
    })
  })

program
  .command('reaper <name>')
  .description('start reaper')
  .option('-p, --connect-push <endpoint>'
    , 'push endpoint'
    , cliutil.list)
  .option('-s, --connect-sub <endpoint>'
    , 'sub endpoint'
    , cliutil.list)
  .option('-t, --heartbeat-timeout <ms>'
    , 'consider devices with heartbeat older than this value dead'
    , Number
    , 30000)
  .action(function(name, options) {
    require('./units/reaper')({
      name: name
    , heartbeatTimeout: options.heartbeatTimeout
    , endpoints: {
        push: options.connectPush
      , sub: options.connectSub
      }
    })
  })

program
  .command('triproxy <name>')
  .description('start triproxy')
  .option('-u, --bind-pub <endpoint>'
    , 'pub endpoint'
    , String
    , 'tcp://*:7111')
  .option('-d, --bind-dealer <endpoint>'
    , 'dealer endpoint'
    , String
    , 'tcp://*:7112')
  .option('-p, --bind-pull <endpoint>'
    , 'pull endpoint'
    , String
    , 'tcp://*:7113')
  .action(function(name, options) {
    require('./units/triproxy')({
      name: name
    , endpoints: {
        pub: options.bindPub
      , dealer: options.bindDealer
      , pull: options.bindPull
      }
    })
  })

program
  .command('auth-ldap')
  .description('start LDAP auth client')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7120)
  .option('-s, --secret <secret>'
    , 'secret (or $SECRET)'
    , String
    , process.env.SECRET)
  .option('-i, --ssid <ssid>'
    , 'session SSID (or $SSID)'
    , String
    , process.env.SSID || 'ssid')
  .option('-a, --app-url <url>'
    , 'URL to app'
    , String)
  .option('-u, --ldap-url <url>'
    , 'LDAP server URL (or $LDAP_URL)'
    , String
    , process.env.LDAP_URL)
  .option('-t, --ldap-timeout <timeout>'
    , 'LDAP timeout (or $LDAP_TIMEOUT)'
    , Number
    , process.env.LDAP_TIMEOUT || 1000)
  .option('--ldap-bind-dn <dn>'
    , 'LDAP bind DN (or $LDAP_BIND_DN)'
    , String
    , process.env.LDAP_BIND_DN)
  .option('--ldap-bind-credentials <credentials>'
    , 'LDAP bind credentials (or $LDAP_BIND_CREDENTIALS)'
    , String
    , process.env.LDAP_BIND_CREDENTIALS)
  .option('--ldap-search-dn <dn>'
    , 'LDAP search DN (or $LDAP_SEARCH_DN)'
    , String
    , process.env.LDAP_SEARCH_DN)
  .option('--ldap-search-scope <scope>'
    , 'LDAP search scope (or $LDAP_SEARCH_SCOPE)'
    , String
    , process.env.LDAP_SEARCH_SCOPE || 'sub')
  .option('--ldap-search-class <class>'
    , 'LDAP search objectClass (or $LDAP_SEARCH_CLASS)'
    , String
    , process.env.LDAP_SEARCH_CLASS || 'top')
  .option('--ldap-search-field <name>'
    , 'LDAP search field (or $LDAP_SEARCH_FIELD)'
    , String
    , process.env.LDAP_SEARCH_FIELD)
  .action(function(options) {
    if (!options.secret) {
      this.missingArgument('--secret')
    }
    if (!options.appUrl) {
      this.missingArgument('--app-url')
    }

    require('./units/auth/ldap')({
      port: options.port
    , secret: options.secret
    , ssid: options.ssid
    , appUrl: options.appUrl
    , ldap: {
        url: options.ldapUrl
      , timeout: options.ldapTimeout
      , bind: {
          dn: options.ldapBindDn
        , credentials: options.ldapBindCredentials
        }
      , search: {
          dn: options.ldapSearchDn
        , scope: options.ldapSearchScope
        , objectClass: options.ldapSearchClass
        , field: options.ldapSearchField
        }
      }
    })
  })

program
  .command('auth-oauth2')
  .description('start OAuth 2.0 auth client')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7120)
  .option('-s, --secret <secret>'
    , 'secret (or $SECRET)'
    , String
    , process.env.SECRET)
  .option('-i, --ssid <ssid>'
    , 'session SSID (or $SSID)'
    , String
    , process.env.SSID || 'ssid')
  .option('-a, --app-url <url>'
    , 'URL to app'
    , String)
  .option('--oauth-authorization-url <url>'
    , 'OAuth 2.0 authorization URL (or $OAUTH_AUTHORIZATION_URL)'
    , String
    , process.env.OAUTH_AUTHORIZATION_URL)
  .option('--oauth-token-url <url>'
    , 'OAuth 2.0 token URL (or $OAUTH_TOKEN_URL)'
    , String
    , process.env.OAUTH_TOKEN_URL)
  .option('--oauth-userinfo-url <url>'
    , 'OAuth 2.0 token URL (or $OAUTH_USERINFO_URL)'
    , String
    , process.env.OAUTH_USERINFO_URL)
  .option('--oauth-client-id <id>'
    , 'OAuth 2.0 client ID (or $OAUTH_CLIENT_ID)'
    , String
    , process.env.OAUTH_CLIENT_ID)
  .option('--oauth-client-secret <value>'
    , 'OAuth 2.0 client secret (or $OAUTH_CLIENT_SECRET)'
    , String
    , process.env.OAUTH_CLIENT_SECRET)
  .option('--oauth-callback-url <url>'
    , 'OAuth 2.0 callback URL (or $OAUTH_CALLBACK_URL)'
    , String
    , process.env.OAUTH_CALLBACK_URL)
  .option('--oauth-scope <scope>'
    , 'Space-separated OAuth 2.0 scope (or $OAUTH_SCOPE)'
    , String
    , process.env.OAUTH_SCOPE)
  .action(function(options) {
    if (!options.secret) {
      this.missingArgument('--secret')
    }
    if (!options.appUrl) {
      this.missingArgument('--app-url')
    }
    if (!options.oauthAuthorizationUrl) {
      this.missingArgument('--oauth-authorization-url')
    }
    if (!options.oauthTokenUrl) {
      this.missingArgument('--oauth-token-url')
    }
    if (!options.oauthUserinfoUrl) {
      this.missingArgument('--oauth-userinfo-url')
    }
    if (!options.oauthClientId) {
      this.missingArgument('--oauth-client-id')
    }
    if (!options.oauthClientSecret) {
      this.missingArgument('--oauth-client-secret')
    }
    if (!options.oauthCallbackUrl) {
      this.missingArgument('--oauth-callback-url')
    }
    if (!options.oauthScope) {
      this.missingArgument('--oauth-scope')
    }

    require('./units/auth/oauth2')({
      port: options.port
    , secret: options.secret
    , ssid: options.ssid
    , appUrl: options.appUrl
    , oauth: {
        authorizationURL: options.oauthAuthorizationUrl
      , tokenURL: options.oauthTokenUrl
      , userinfoURL: options.oauthUserinfoUrl
      , clientID: options.oauthClientId
      , clientSecret: options.oauthClientSecret
      , callbackURL: options.oauthCallbackUrl
      , scope: options.oauthScope.split(/\s+/)
      }
    })
  })

program
  .command('auth-mock')
  .description('start mock auth client')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7120)
  .option('-s, --secret <secret>'
    , 'secret (or $SECRET)'
    , String
    , process.env.SECRET)
  .option('-i, --ssid <ssid>'
    , 'session SSID (or $SSID)'
    , String
    , process.env.SSID || 'ssid')
  .option('-a, --app-url <url>'
    , 'URL to app'
    , String)
  .action(function(options) {
    if (!options.secret) {
      this.missingArgument('--secret')
    }
    if (!options.appUrl) {
      this.missingArgument('--app-url')
    }

    require('./units/auth/mock')({
      port: options.port
    , secret: options.secret
    , ssid: options.ssid
    , appUrl: options.appUrl
    })
  })

program
  .command('notify-hipchat')
  .description('start HipChat notifier')
  .option('-t, --token <token>'
    , 'HipChat v2 API token (or $HIPCHAT_TOKEN)'
    , String
    , process.env.HIPCHAT_TOKEN)
  .option('-r, --room <room>'
    , 'HipChat room (or $HIPCHAT_ROOM)'
    , String
    , process.env.HIPCHAT_ROOM)
  .option('-p, --priority <level>'
    , 'minimum log level'
    , Number
    , logger.Level.IMPORTANT)
  .option('-n, --notify-priority <level>'
    , 'minimum log level to cause a notification'
    , Number
    , logger.Level.WARNING)
  .option('-s, --connect-sub <endpoint>'
    , 'sub endpoint'
    , cliutil.list)
  .action(function(options) {
    if (!options.token) {
      this.missingArgument('--token')
    }
    if (!options.room) {
      this.missingArgument('--room')
    }
    if (!options.connectSub) {
      this.missingArgument('--connect-sub')
    }

    require('./units/notify/hipchat')({
      token: options.token
    , room: options.room
    , priority: options.priority
    , notifyPriority: options.notifyPriority
    , endpoints: {
        sub: options.connectSub
      }
    })
  })

program
  .command('poorxy')
  .description('start a poor reverse proxy for local development')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7100)
  .option('-u, --app-url <url>'
    , 'URL to app'
    , String)
  .option('-a, --auth-url <url>'
    , 'URL to auth client'
    , String)
  .option('-w, --websocket-url <url>'
    , 'URL to websocket client'
    , String)
  .option('-r, --storage-url <url>'
    , 'URL to storage client'
    , String)
  .option('--storage-plugin-image-url <url>'
    , 'URL to image storage plugin'
    , String)
  .option('--storage-plugin-apk-url <url>'
    , 'URL to apk storage plugin'
    , String)
  .action(function(options) {
    if (!options.appUrl) {
      this.missingArgument('--app-url')
    }
    if (!options.authUrl) {
      this.missingArgument('--auth-url')
    }
    if (!options.websocketUrl) {
      this.missingArgument('--websocket-url')
    }
    if (!options.storageUrl) {
      this.missingArgument('--storage-url')
    }
    if (!options.storagePluginImageUrl) {
      this.missingArgument('--storage-plugin-image-url')
    }
    if (!options.storagePluginApkUrl) {
      this.missingArgument('--storage-plugin-apk-url')
    }

    require('./units/poorxy')({
      port: options.port
    , appUrl: options.appUrl
    , authUrl: options.authUrl
    , websocketUrl: options.websocketUrl
    , storageUrl: options.storageUrl
    , storagePluginImageUrl: options.storagePluginImageUrl
    , storagePluginApkUrl: options.storagePluginApkUrl
    })
  })

program
  .command('app')
  .description('start app')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7105)
  .option('-s, --secret <secret>'
    , 'secret (or $SECRET)'
    , String
    , process.env.SECRET)
  .option('-i, --ssid <ssid>'
    , 'session SSID (or $SSID)'
    , String
    , process.env.SSID || 'ssid')
  .option('-a, --auth-url <url>'
    , 'URL to auth client'
    , String)
  .option('-w, --websocket-url <url>'
    , 'URL to websocket client'
    , String)
  .option('--user-profile-url <url>'
    , 'URL to external user profile page'
    , String)
  .action(function(options) {
    if (!options.secret) {
      this.missingArgument('--secret')
    }
    if (!options.authUrl) {
      this.missingArgument('--auth-url')
    }
    if (!options.websocketUrl) {
      this.missingArgument('--websocket-url')
    }

    require('./units/app')({
      port: options.port
    , secret: options.secret
    , ssid: options.ssid
    , authUrl: options.authUrl
    , websocketUrl: options.websocketUrl
    , userProfileUrl: options.userProfileUrl
    })
  })

program
  .command('websocket')
  .description('start websocket')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7110)
  .option('-s, --secret <secret>'
    , 'secret (or $SECRET)'
    , String
    , process.env.SECRET)
  .option('-i, --ssid <ssid>'
    , 'session SSID (or $SSID)'
    , String
    , process.env.SSID || 'ssid')
  .option('-r, --storage-url <url>'
    , 'URL to storage client'
    , String)
  .option('-u, --connect-sub <endpoint>'
    , 'sub endpoint'
    , cliutil.list)
  .option('-c, --connect-push <endpoint>'
    , 'push endpoint'
    , cliutil.list)
  .action(function(options) {
    if (!options.secret) {
      this.missingArgument('--secret')
    }
    if (!options.storageUrl) {
      this.missingArgument('--storage-url')
    }
    if (!options.connectSub) {
      this.missingArgument('--connect-sub')
    }
    if (!options.connectPush) {
      this.missingArgument('--connect-push')
    }

    require('./units/websocket')({
      port: options.port
    , secret: options.secret
    , ssid: options.ssid
    , storageUrl: options.storageUrl
    , endpoints: {
        sub: options.connectSub
      , push: options.connectPush
      }
    })
  })

program
  .command('storage-temp')
  .description('start temp storage')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7100)
  .option('--save-dir <dir>'
    , 'where to save files'
    , String
    , os.tmpdir())
  .action(function(options) {
    require('./units/storage/temp')({
      port: options.port
    , saveDir: options.saveDir
    })
  })

program
  .command('storage-plugin-image')
  .description('start storage image plugin')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7100)
  .option('-r, --storage-url <url>'
    , 'URL to storage client'
    , String)
  .option('-c, --concurrency <num>'
    , 'maximum number of simultaneous transformations'
    , Number)
  .option('--cache-dir <dir>'
    , 'where to cache images'
    , String
    , os.tmpdir())
  .action(function(options) {
    if (!options.storageUrl) {
      this.missingArgument('--storage-url')
    }

    require('./units/storage/plugins/image')({
      port: options.port
    , storageUrl: options.storageUrl
    , cacheDir: options.cacheDir
    , concurrency: options.concurrency || os.cpus().length
    })
  })

program
  .command('storage-plugin-apk')
  .description('start storage apk plugin')
  .option('-p, --port <port>'
    , 'port (or $PORT)'
    , Number
    , process.env.PORT || 7100)
  .option('-r, --storage-url <url>'
    , 'URL to storage client'
    , String)
  .option('--cache-dir <dir>'
    , 'where to cache images'
    , String
    , os.tmpdir())
  .action(function(options) {
    if (!options.storageUrl) {
      this.missingArgument('--storage-url')
    }

    require('./units/storage/plugins/apk')({
      port: options.port
    , storageUrl: options.storageUrl
    , cacheDir: options.cacheDir
    })
  })

program
  .command('migrate')
  .description('migrates the database to the latest version')
  .action(function() {
    var log = logger.createLogger('cli:migrate')
      , db = require('./db')

    db.setup()
      .then(function() {
        process.exit(0)
      })
      .catch(function(err) {
        log.fatal('Migration had an error:', err.stack)
        process.exit(1)
      })
  })

program
  .command('generate-fake-device [model]')
  .description('generates a fake device for testing')
  .option('-n, --number <n>'
    , 'how many devices to create (defaults to 1)'
    , Number
    , 1)
  .action(function(model, options) {
    var log = logger.createLogger('cli:generate-fake-device')
      , fake = require('./util/fakedevice')
      , n = options.number

    function next() {
      return fake.generate(model)
        .then(function(serial) {
          log.info('Created fake device "%s"', serial)

          if (--n) {
            return next()
          }
        })
    }

    next()
      .then(function() {
        process.exit(0)
      })
      .catch(function(err) {
        log.fatal('Fake device creation had an error:', err.stack)
        process.exit(1)
      })
  })

program
  .command('local [serial..]')
  .description('start everything locally')
  .option('--bind-app-pub <endpoint>'
    , 'app pub endpoint'
    , String
    , 'tcp://127.0.0.1:7111')
  .option('--bind-app-dealer <endpoint>'
    , 'app dealer endpoint'
    , String
    , 'tcp://127.0.0.1:7112')
  .option('--bind-app-pull <endpoint>'
    , 'app pull endpoint'
    , String
    , 'tcp://127.0.0.1:7113')
  .option('--bind-dev-pub <endpoint>'
    , 'device pub endpoint'
    , String
    , 'tcp://127.0.0.1:7114')
  .option('--bind-dev-dealer <endpoint>'
    , 'device dealer endpoint'
    , String
    , 'tcp://127.0.0.1:7115')
  .option('--bind-dev-pull <endpoint>'
    , 'device pull endpoint'
    , String
    , 'tcp://127.0.0.1:7116')
  .option('--auth-type <mock|ldap|oauth2>'
    , 'auth type'
    , String
    , 'mock')
  .option('-a, --auth-url <url>'
    , 'URL to auth client'
    , String)
  .option('--auth-port <port>'
    , 'auth port'
    , Number
    , 7120)
  .option('--auth-secret <secret>'
    , 'auth secret'
    , String
    , 'kute kittykat')
  .option('--auth-options <json>'
    , 'array of options to pass to the auth implementation'
    , String
    , '[]')
  .option('--poorxy-port <port>'
    , 'poorxy port'
    , Number
    , 7100)
  .option('--app-port <port>'
    , 'app port'
    , Number
    , 7105)
  .option('--websocket-port <port>'
    , 'websocket port'
    , Number
    , 7110)
  .option('--storage-port <port>'
    , 'storage port'
    , Number
    , 7102)
  .option('--storage-plugin-image-port <port>'
    , 'storage image plugin port'
    , Number
    , 7103)
  .option('--storage-plugin-apk-port <port>'
    , 'storage apk plugin port'
    , Number
    , 7104)
  .option('--provider <name>'
    , 'provider name (or os.hostname())'
    , String
    , os.hostname())
  .option('-t, --group-timeout <seconds>'
    , 'group timeout'
    , Number
    , 900)
  .option('--public-ip <ip>'
    , 'public ip for global access'
    , String
    , 'localhost')
  .option('--adb-host <host>'
    , 'ADB host (defaults to 127.0.0.1)'
    , String
    , '127.0.0.1')
  .option('--adb-port <port>'
    , 'ADB port (defaults to 5037)'
    , Number
    , 5037)
  .option('-R, --allow-remote'
    , 'Whether to allow remote devices to be set up')
  .option('--user-profile-url <url>'
    , 'URL to external user profile page'
    , String)
  .action(function() {
    var log = logger.createLogger('cli:local')
      , args = arguments
      , options = cliutil.lastArg(args)
      , procutil = require('./util/procutil')

    // Each forked process waits for signals to stop, and so we run over the
    // default limit of 10. So, it's not a leak, but a refactor wouldn't hurt.
    process.setMaxListeners(20)

    function run() {
      var procs = [
        // app triproxy
        procutil.fork(__filename, [
            'triproxy', 'app001'
          , '--bind-pub', options.bindAppPub
          , '--bind-dealer', options.bindAppDealer
          , '--bind-pull', options.bindAppPull
          ])

        // device triproxy
      , procutil.fork(__filename, [
            'triproxy', 'dev001'
          , '--bind-pub', options.bindDevPub
          , '--bind-dealer', options.bindDevDealer
          , '--bind-pull', options.bindDevPull
          ])

        // processor one
      , procutil.fork(__filename, [
            'processor', 'proc001'
          , '--connect-app-dealer', options.bindAppDealer
          , '--connect-dev-dealer', options.bindDevDealer
          ])

        // processor two
      , procutil.fork(__filename, [
            'processor', 'proc002'
          , '--connect-app-dealer', options.bindAppDealer
          , '--connect-dev-dealer', options.bindDevDealer
          ])

        // reaper one
      , procutil.fork(__filename, [
            'reaper', 'reaper001'
          , '--connect-push', options.bindDevPull
          , '--connect-sub', options.bindAppPub
          ])

        // provider
      , procutil.fork(__filename, [
            'provider'
          , '--name', options.provider
          , '--connect-sub', options.bindDevPub
          , '--connect-push', options.bindDevPull
          , '--group-timeout', options.groupTimeout
          , '--public-ip', options.publicIp
          , '--storage-url'
          , util.format('http://localhost:%d/', options.poorxyPort)
          , '--adb-host', options.adbHost
          , '--adb-port', options.adbPort
          ]
          .concat(options.allowRemote ? ['--allow-remote'] : [])
          .concat(cliutil.allUnknownArgs(args)))

        // auth
      , procutil.fork(__filename, [
            util.format('auth-%s', options.authType)
          , '--port', options.authPort
          , '--secret', options.authSecret
          , '--app-url', util.format(
              'http://%s:%d/'
            , options.publicIp
            , options.poorxyPort
            )
          ].concat(JSON.parse(options.authOptions)))

        // app
      , procutil.fork(__filename, [
            'app'
          , '--port', options.appPort
          , '--secret', options.authSecret
          , '--auth-url', options.authUrl || util.format(
              'http://%s:%d/auth/%s/'
            , options.publicIp
            , options.poorxyPort
            , ({oauth2: 'oauth'}[options.authType]) || options.authType
            )
          , '--websocket-url', util.format(
              'http://%s:%d/'
            , options.publicIp
            , options.websocketPort
            )
          ].concat((function() {
            var extra = []
            if (options.userProfileUrl) {
              extra.push('--user-profile-url', options.userProfileUrl)
            }
            return extra
          })()))

        // websocket
      , procutil.fork(__filename, [
            'websocket'
          , '--port', options.websocketPort
          , '--secret', options.authSecret
          , '--storage-url'
          , util.format('http://localhost:%d/', options.poorxyPort)
          , '--connect-sub', options.bindAppPub
          , '--connect-push', options.bindAppPull
          ])

        // storage
      , procutil.fork(__filename, [
            'storage-temp'
          , '--port', options.storagePort
          ])

        // image processor
      , procutil.fork(__filename, [
            'storage-plugin-image'
          , '--port', options.storagePluginImagePort
          , '--storage-url'
          , util.format('http://localhost:%d/', options.poorxyPort)
          ])

        // apk processor
      , procutil.fork(__filename, [
            'storage-plugin-apk'
          , '--port', options.storagePluginApkPort
          , '--storage-url'
          , util.format('http://localhost:%d/', options.poorxyPort)
          ])

        // poorxy
      , procutil.fork(__filename, [
            'poorxy'
          , '--port', options.poorxyPort
          , '--app-url'
          , util.format('http://localhost:%d/', options.appPort)
          , '--auth-url'
          , util.format('http://localhost:%d/', options.authPort)
          , '--websocket-url'
          , util.format('http://localhost:%d/', options.websocketPort)
          , '--storage-url'
          , util.format('http://localhost:%d/', options.storagePort)
          , '--storage-plugin-image-url'
          , util.format('http://localhost:%d/', options.storagePluginImagePort)
          , '--storage-plugin-apk-url'
          , util.format('http://localhost:%d/', options.storagePluginApkPort)
          ])
      ]

      function shutdown() {
        log.info('Shutting down all child processes')
        procs.forEach(function(proc) {
          proc.cancel()
        })
        return Promise.settle(procs)
      }

      process.on('SIGINT', function() {
        log.info('Received SIGINT, waiting for processes to terminate')
      })

      process.on('SIGTERM', function() {
        log.info('Received SIGTERM, waiting for processes to terminate')
      })

      return Promise.all(procs)
        .then(function() {
          process.exit(0)
        })
        .catch(function(err) {
          log.fatal('Child process had an error', err.stack)
          return shutdown()
            .then(function() {
              process.exit(1)
            })
        })
    }

    procutil.fork(__filename, ['migrate'])
      .done(run)
  })

program.parse(process.argv)
