#!/usr/bin/env node
// -*- mode: js -*-
// Copyright 2011 Mark Cavage.  All rights reserved.

var path = require('path');

var dashdash = require('dashdash');

var ldap = require('../lib/index');
var Logger = require('bunyan');


///--- Globals

dashdash.addOptionType({
  name: 'ldap.Filter',
  takesArg: true,
  helpArg: 'LDAP_FILTER',
  parseArg: function (option, optstr, arg) {
    return ldap.parseFilter(arg);
  }
});

dashdash.addOptionType({
  name: 'ldap.scope',
  takesArg: true,
  helpArg: 'SCOPE',
  parseArg: function (option, optstr, arg) {
    if (!/^base|one|sub$/.test(arg)) {
      throw new TypeError('Scope must be <base|one|sub>');
    }
    return arg;
  }
});

dashdash.addOptionType({
  name: 'ldap.outputFormat',
  takesArg: true,
  helpArg: 'FORMAT',
  parseArg: function (option, optstr, arg) {
    var formats = ['json', 'jsonl', 'jsona'];
    if (formats.indexOf(arg) === -1) {
      throw new TypeError('Must be valid format type');
    }
    return arg;
  }
});


var opts = [
  {
    names: ['base', 'b'],
    type: 'string',
    help: 'Base DN of search',
    helpArg: 'BASE_DN',
    default: ''
  },
  {
    names: ['scope', 's'],
    type: 'ldap.scope',
    help: 'Search scope <base|sub|one>',
    helpArg: 'SCOPE',
    default: 'sub'
  },
  {
    names: ['timeout', 't'],
    type: 'integer',
    help: 'Search timeout',
    helpArg: 'SECS'
  },
  {
    names: ['persistent', 'p'],
    type: 'bool',
    help: 'Enable persistent search control',
    default: false
  },
  {
    names: ['paged', 'g'],
    type: 'number',
    help: 'Enable paged search result control',
    helpArg: 'PAGE_SIZE'
  },
  {
    names: ['control', 'c'],
    type: 'arrayOfString',
    help: 'Send addition control OID',
    helpArg: 'OID',
    default: []
  },
  { group: 'General Options' },
  {
    names: ['help', 'h'],
    type: 'bool',
    help: 'Print this help and exit.'
  },
  {
    names: ['debug', 'd'],
    type: 'integer',
    help: 'Set debug level <0-2>',
    helpArg: 'LEVEL'
  },
  { group: 'Output Options' },
  {
    names: ['format', 'o'],
    type: 'ldap.outputFormat',
    helpWrap: false,
    help: ('Specify and output format. One of:\n' +
            '  json:  JSON objects (default)\n' +
            '  jsonl: Line-delimited JSON\n' +
            '  jsona: Array of JSON objects\n'),
    default: 'json'
  },
  { group: 'Connection Options' },
  {
    names: ['url', 'u'],
    type: 'string',
    help: 'LDAP server URL',
    helpArg: 'URL',
    default: 'ldap://127.0.0.1:389'
  },
  {
    names: ['binddn', 'D'],
    type: 'string',
    help: 'Bind DN',
    helpArg: 'BIND_DN',
    default: ''
  },
  {
    names: ['password', 'w'],
    type: 'string',
    help: 'Bind password',
    helpArg: 'PASSWD',
    default: ''
  },
  {
    names: ['insecure', 'i'],
    type: 'bool',
    env: 'LDAPJS_TLS_INSECURE',
    help: 'Disable SSL certificate verification',
    default: false
  }
];
var parser = dashdash.createParser({options: opts});



///--- Helpers

function usage(code, message) {
  var msg = (message ? message + '\n' : '') +
    'Usage: ' + path.basename(process.argv[1]) +
      ' [OPTIONS] FILTER [ATTRIBUTES...]\n\n' +
    parser.help({includeEnv: true});

  process.stderr.write(msg + '\n');
  process.exit(code);
}


function perror(err) {
  if (parsed.debug) {
    process.stderr.write(err.stack + '\n');
  } else {
    process.stderr.write(err.message + '\n');
  }
  process.exit(1);
}


function EntryFormatter(fp, format) {
  this.format = format;
  this.started = false;
  this.ended = false;
  this.fp = fp;
}

EntryFormatter.prototype.write = function write(entry) {
  switch (this.format) {
  case 'json':
    this.fp.write(JSON.stringify(entry.object, null, 2) + '\n');
    break;
  case 'jsonl':
    this.fp.write(JSON.stringify(entry.object) + '\n');
    break;
  case 'jsona':
    this.fp.write((this.started) ? ',\n' : '[\n');
    this.started = true;
    // pretty-print with indent
    this.fp.write(
        JSON.stringify(entry.object, null, 2)
          .split('\n')
          .map(function (line) { return '  ' + line; })
          .join('\n'));
    break;
  default:
    throw new Error('invalid output format');
  }
};

EntryFormatter.prototype.end = function end() {
  if (this.ended) {
    return;
  }
  this.ended = true;
  if (this.format === 'jsona') {
    this.fp.write('\n]\n');
  }
};


///--- Mainline

var parsed;

process.stdout.on('error', function (err) {
  if (err.code === 'EPIPE') {
    process.exit(0);
  } else {
    throw err;
  }
});

try {
  parsed = parser.parse(process.argv);
} catch (e) {
  usage(1, e.toString());
}

if (parsed.help)
  usage(0);
if (parsed._args.length < 1)
  usage(1, 'filter required');

try {
  ldap.parseFilter(parsed._args[0]);
} catch (e) {
  usage(1, e.message);
}

var logLevel = 'info';

if (parsed.debug)
  logLevel = (parsed.debug > 1 ? 'trace' : 'debug');

var formatter = new EntryFormatter(process.stdout, parsed.format);

var log = new Logger({
  name: 'ldapjs',
  component: 'client',
  stream: process.stderr,
  level: logLevel
});

var client = ldap.createClient({
  url: parsed.url,
  log: log,
  strictDN: false,
  timeout: parsed.timeout || false,
  tlsOptions: {
    rejectUnauthorized: !parsed.insecure
  }
});

client.on('error', function (err) {
  perror(err);
});

client.on('timeout', function (req) {
  process.stderr.write('Timeout reached\n');
  process.exit(1);
});

client.bind(parsed.binddn, parsed.password, function (err, res) {
  if (err)
    perror(err);

  var controls = [];
  parsed.control.forEach(function (c) {
    controls.push(new ldap.Control({
      type: c,
      criticality: true
    }));
  });
  if (parsed.persistent) {
    var pCtrl = new ldap.PersistentSearchControl({
      type: '2.16.840.1.113730.3.4.3',
      value: {
        changeTypes: 15,
        changesOnly: false,
        returnECs: true
      }
    });
    controls.push(pCtrl);
  }
  var req = {
    scope: parsed.scope || 'sub',
    filter: parsed._args[0],
    attributes: parsed._args.length > 1 ? parsed._args.slice(1) : []
  };
  if (parsed.paged) {
    req.paged = {
      pageSize: parsed.paged
    };
  }
  client.search(parsed.base, req, controls, function (err, res) {
    if (err)
      perror(err);

    res.on('searchEntry', function (entry) {
      formatter.write(entry);
    });
    res.on('error', function (err) {
      formatter.end();
      perror(err);
    });
    res.on('end', function (res) {
      formatter.end();
      if (res.status !== 0) {
        process.stderr.write(ldap.getMessage(res.status) + '\n');
      }
      client.unbind(function () {
        return;
      });
    });
  });
});