'use strict';

var Utils = require('../../utils')
  , AbstractQuery = require('../abstract/query')
  , DataTypes = require('../../data-types')
  , hstore = require('./hstore')
  , range = require('./range')
  , QueryTypes = require('../../query-types')
  , Promise = require('../../promise')
  , sequelizeErrors = require('../../errors.js');

var parseDialectSpecificFields,
    dialectSpecificTypes;

parseDialectSpecificFields = {
  // Parses hstore fields if the model has any hstore fields.
  // This cannot be done in the 'pg' lib because hstore is a UDT.
  hstore: function (value, options) {
    if (value === null) return null;

    return DataTypes.ARRAY.is(options.dataType, DataTypes.HSTORE) ?
      this.lib.types.arrayParser.create(value, hstore.parse).parse() : hstore.parse(value);
  },
  range:  function (value, options) {
    if (value === null) return null;

    return DataTypes.ARRAY.is(options.dataType, DataTypes.RANGE) ?
      this.lib.types.arrayParser.create(value, function (v) {
        return range.parse(v, options.dataType.type);
      }).parse() : range.parse(value, options.dataType);
  }
};

dialectSpecificTypes = Utils._.keys(parseDialectSpecificFields);

function dialectSpecificFieldDatatypeMap (options, prefix) {
  if (!Utils._.isArray(options.types))
    return [];

  prefix = prefix || '';

  var fields = {};
  if (options.callee) {
    if (options.as)
      prefix += options.as + '.';

    Utils._.each(options.types, function (type) {
      Utils._.each(options.callee['_' + type + 'Attributes'], function (attrName) {
        fields[prefix + attrName] = {
          dataType: options.callee.attributes[attrName].type || null,
          type:     type
        };
      });
    });
  }

  Utils._.each(options.include, function (include) {
    fields = Utils._.merge(fields, dialectSpecificFieldDatatypeMap({
      callee:  include.model,
      as:      include.as,
      include: include.include,
      types:   options.types
    }, prefix));
  });

  return fields;
}

var Query = function(client, sequelize, options) {
  this.client = client;
  this.sequelize = sequelize;
  this.instance = options.instance;
  this.model = options.model;
  this.options = Utils._.extend({
    logging: console.log,
    plain: false,
    raw: false
  }, options || {});

  this.checkLoggingOption();
};
Utils.inherit(Query, AbstractQuery);

Query.prototype.parseDialectSpecificFields = parseDialectSpecificFields;

Query.prototype.run = function(sql) {
  this.sql = sql;

  var self = this
    , receivedError = false
    , query = this.client.query(sql)
    , rows = [];

  this.sequelize.log('Executing (' + (this.client.uuid || 'default') + '): ' + this.sql, this.options);

  var promise = new Promise(function(resolve, reject) {
    query.on('row', function(row) {
      rows.push(row);
    });

    query.on('error', function(err) {
      receivedError = true;
      err.sql = sql;
      reject(self.formatError(err));
    });

    query.on('end', function(result) {
      if (receivedError) {
        return;
      }

      resolve([rows, sql, result]);
    });
  }).spread(function(rows, sql, result) {
    var results = rows
      , isTableNameQuery = (sql.indexOf('SELECT table_name FROM information_schema.tables') === 0)
      , isRelNameQuery = (sql.indexOf('SELECT relname FROM pg_class WHERE oid IN') === 0)
      , dialectSpecificFields
      , isDialectSpecificField = Utils._.memoize(function (key) { return dialectSpecificFields.hasOwnProperty(key); });

    if (isTableNameQuery || isRelNameQuery) {
      if (isRelNameQuery) {
        results = rows.map(function(row) {
          return {
            name: row.relname,
            tableName: row.relname.split('_')[0]
          };
        });
      } else {
        results = rows.map(function(row) { return Utils._.values(row); });
      }
      return results;
    }

    if (rows[0] && rows[0].sequelize_caught_exception !== undefined) {
      if (rows[0].sequelize_caught_exception !== null) {
        var err = self.formatError({
          code: '23505',
          detail: rows[0].sequelize_caught_exception
        });
        throw err;
      } else {
        rows = rows.map(function (row) {
          delete row.sequelize_caught_exception;
          return row;
        });
      }
    }

    if (self.isShowIndexesQuery()) {
      results.forEach(function (result) {
        var attributes = /ON .*? (?:USING .*?\s)?\((.*)\)/gi.exec(result.definition)[1].split(',')
          , field

          , attribute
          , columns;

        // Map column index in table to column name
        columns = Utils._.zipObject(
          result.column_indexes,
          self.sequelize.queryInterface.QueryGenerator.fromArray(result.column_names)
        );
        delete result.column_indexes;
        delete result.column_names;

        // Indkey is the order of attributes in the index, specified by a string of attribute indexes
        result.fields = result.indkey.split(' ').map(function (indKey, index) {
          field = columns[indKey];
          attribute = attributes[index];
          return {
            attribute: field,
            collate: attribute.match(/COLLATE "(.*?)"/) ? /COLLATE "(.*?)"/.exec(attribute)[1] : undefined,
            order: attribute.indexOf('DESC') !== -1 ? 'DESC' : attribute.indexOf('ASC') !== -1 ? 'ASC': undefined,
            length: undefined
          };
        });
        delete result.columns;
      });
      return results;
    } else if (self.isForeignKeysQuery()) {
      result = [];
      rows.forEach(function(row) {
        var defParts;
        if (row.condef !== undefined && (defParts = row.condef.match(/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?/))) {
          row.id = row.constraint_name;
          row.table = defParts[2];
          row.from = defParts[1];
          row.to = defParts[3];
          var i;
          for (i=5;i<=8;i+=3) {
            if (/(UPDATE|DELETE)/.test(defParts[i])) {
              row['on_'+defParts[i].toLowerCase()] = defParts[i+1];
            }
          }
        }
        result.push(row);
      });
      return result;
    } else if (self.isSelectQuery()) {
      // Postgres will treat tables as case-insensitive, so fix the case
      // of the returned values to match attributes
      if (self.options.raw === false && self.sequelize.options.quoteIdentifiers === false) {
        var attrsMap = Utils._.reduce(self.model.attributes, function(m, v, k) { m[k.toLowerCase()] = k; return m; }, {});
        rows.forEach(function(row) {
          Utils._.keys(row).forEach(function(key) {
            var targetAttr = attrsMap[key];
            if (typeof targetAttr === 'string' && targetAttr !== key) {
              row[targetAttr] = row[key];
              delete row[key];
            }
          });
        });
      }

      if (!!self.model && rows.length > 0) {
        dialectSpecificFields = dialectSpecificFieldDatatypeMap({
          callee:  self.model,
          include: self.options.include,
          types:   dialectSpecificTypes
        });

        // check whether custom fields exist in responses model
        if (!Utils._.isEmpty(dialectSpecificFields)) {
          rows.forEach(function (row, rowId, rows) {
            Utils._.each(row, function (value, key) {
              if (isDialectSpecificField(key))
                rows[rowId][key] =
                parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
            });
          });
        }
      }

      return self.handleSelectQuery(rows);
    } else if (QueryTypes.DESCRIBE === self.options.type) {
      result = {};

      rows.forEach(function(_result) {
        result[_result.Field] = {
          type: _result.Type.toUpperCase(),
          allowNull: (_result.Null === 'YES'),
          defaultValue: _result.Default,
          special: (!!_result.special ? self.sequelize.queryInterface.QueryGenerator.fromArray(_result.special) : [])
        };

        if (result[_result.Field].type === 'BOOLEAN') {
          result[_result.Field].defaultValue = { 'false': false, 'true': true }[result[_result.Field].defaultValue];

          if (result[_result.Field].defaultValue === undefined) {
            result[_result.Field].defaultValue = null;
          }
        }

        if (typeof result[_result.Field].defaultValue === 'string') {
          result[_result.Field].defaultValue = result[_result.Field].defaultValue.replace(/'/g, '');

          if (result[_result.Field].defaultValue.indexOf('::') > -1) {
            var split = result[_result.Field].defaultValue.split('::');
            if (split[1].toLowerCase() !== 'regclass)') {
              result[_result.Field].defaultValue = split[0];
            }
          }
        }
      });

      return result;
    } else if (self.isShowOrDescribeQuery()) {
      return results;
    } else if (QueryTypes.BULKUPDATE === self.options.type) {
      if (!self.options.returning) {
        return parseInt(result.rowCount, 10);
      }

      if (!!self.model && rows.length > 0) {
        dialectSpecificFields = dialectSpecificFieldDatatypeMap({
          callee:  self.model,
          types:   dialectSpecificTypes
        });

        // check whether custom fields exist in responses model
        if (!Utils._.isEmpty(dialectSpecificFields)) {
          rows.forEach(function (row, rowId, rows) {
            Utils._.each(row, function (value, key) {
              if (isDialectSpecificField(key))
                rows[rowId][key] =
                parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
            });
          });
        }
      }

      return self.handleSelectQuery(rows);
    } else if (QueryTypes.BULKDELETE === self.options.type) {
      return parseInt(result.rowCount, 10);
    } else if (self.isUpsertQuery()) {
      return rows[0].sequelize_upsert;
    } else if (self.isInsertQuery() || self.isUpdateQuery()) {
      if (self.instance && self.instance.dataValues) {
        if (self.model) {
          dialectSpecificFields = dialectSpecificFieldDatatypeMap({
            callee:  self.model,
            types:   dialectSpecificTypes
          });

          // check whether custom fields exist in responses model
          if (!Utils._.isEmpty(dialectSpecificFields)) {
            Utils._.each(rows[0], function (value, key) {
              if (isDialectSpecificField(key))
                rows[0][key] =
                parseDialectSpecificFields[dialectSpecificFields[key].type](value, { dataType: dialectSpecificFields[key].dataType });
            });
          }
        }

        for (var key in rows[0]) {
          if (rows[0].hasOwnProperty(key)) {
            var record = rows[0][key];

            var attr = Utils._.find(self.model.rawAttributes, function (attribute) {
              return attribute.fieldName === key || attribute.field === key;
            });

            self.instance.dataValues[attr && attr.fieldName || key] = record;
          }
        }
      }

      return self.instance || (rows && ((self.options.plain && rows[0]) || rows)) || undefined;
    } else if (self.isVersionQuery()) {
      return results[0].version;
    } else if (self.isRawQuery()) {
      return [rows, result];
    } else {
      return results;
    }
  });

  return promise;
};

Query.prototype.formatError = function (err) {
  var match
    , table
    , index
    , fields
    , errors
    , message
    , self = this;

  var code = err.code || err.sqlState
    , errMessage = err.message || err.messagePrimary
    , errDetail = err.detail || err.messageDetail;

  switch (code) {
    case '23503':
      index = errMessage.match(/violates foreign key constraint \"(.+?)\"/);
      index = index ? index[1] : undefined;
      table = errMessage.match(/on table \"(.+?)\"/);
      table = table ? table[1] : undefined;

      return new sequelizeErrors.ForeignKeyConstraintError({
        fields: null,
        index: index,
        table: table,
        parent: err
      });
    case '23505':
      // there are multiple different formats of error messages for this error code
      // this regex should check at least two
      match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);

      if (match) {
        fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '));
        errors = [];
        message = 'Validation error';

        Utils._.forOwn(fields, function(value, field) {
          errors.push(new sequelizeErrors.ValidationErrorItem(
            self.getUniqueConstraintErrorMessage(field),
            'unique violation', field, value));
        });

        if (this.model && this.model.uniqueKeys) {
          Utils._.forOwn(this.model.uniqueKeys, function(constraint) {
            if (Utils._.isEqual(constraint.fields, Object.keys(fields)) && !!constraint.msg) {
              message = constraint.msg;
              return false;
            }
          });
        }

        return new sequelizeErrors.UniqueConstraintError({
          message: message,
          errors: errors,
          parent: err,
          fields: fields
        });
      } else {
        return new sequelizeErrors.UniqueConstraintError({
          message: errMessage,
          parent: err
        });
      }

      break;
    case '23P01':
      match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);

      if (match) {
        fields = Utils._.zipObject(match[1].split(', '), match[2].split(', '));
      }
      message = 'Exclusion constraint error';

      return new sequelizeErrors.ExclusionConstraintError({
        message: message,
        constraint: err.constraint,
        fields: fields,
        table: err.table,
        parent: err
      });

    default:
      return new sequelizeErrors.DatabaseError(err);
  }
};

Query.prototype.isForeignKeysQuery = function() {
  return /SELECT conname as constraint_name, pg_catalog\.pg_get_constraintdef\(r\.oid, true\) as condef FROM pg_catalog\.pg_constraint r WHERE r\.conrelid = \(SELECT oid FROM pg_class WHERE relname = '.*' LIMIT 1\) AND r\.contype = 'f' ORDER BY 1;/.test(this.sql);
};

Query.prototype.getInsertIdField = function() {
  return 'id';
};

module.exports = Query;
