# node libraries
bcrypt = (require 'bcrypt')
logger = (require 'logging').default 'db'
path = (require 'path')
sqlite3 = (require 'sqlite3').verbose()

# bruteforce countermeasure
saltRounds = 13

messages =
  empty:    'Empty user name or password'
  hash:     'Failed to process your data, try again later'
  exists:   'User name is already taken'
  noexists: 'Wrong user name or password'
  password: 'Wrong user name or password'
  db:       'Failed to access the database, try again later'

class FFTCGDB
  constructor: (filename, truncate) ->
    @filename = filename

    @db = new sqlite3.Database @filename, (err) =>
      if err
        logger.error err.message

      else
        logger.info "OK open '#{@filename}'"

        @db.run 'PRAGMA foreign_keys = ON;', (err) =>
          logger.error err.message if err

          if truncate == true
            @db.run 'DROP TABLE IF EXISTS users;', (err) =>
              logger.error err.message if err
              @db.run '''
                CREATE TABLE users (
                  user integer PRIMARY KEY,
                  login text NOT NULL COLLATE NOCASE,
                  pwdhash text NOT NULL,
                  settings text,
                  UNIQUE(login)
                );
                ''', (err) =>
                  logger.error err.message if err

                  @db.run 'DROP TABLE IF EXISTS decks;', (err) =>
                    logger.error err.message if err
                    @db.run '''
                      CREATE TABLE decks (
                        deck integer PRIMARY KEY,
                        user integer NOT NULL,
                        json text,
                        FOREIGN KEY (user) REFERENCES users (user)
                          ON DELETE CASCADE
                      );
                      ''', (err) =>
                        logger.error err.message if err

                        @db.run '''INSERT INTO users VALUES(1,'jmm','$2b$13$jgDdHHDWqq1RV6PXxf7aOO6AbxqY6tbxIADyIO0FeXt2BlKQCCMzS',NULL);'''
                        @db.run '''INSERT INTO decks VALUES(1,1,'{"name":"Antipode Bomb Version 6.0","note":"As Seen In Tournament: The North American Water Cup","cards":[{"count":1,"serial":"1-192"},{"count":2,"serial":"7-132"},{"count":2,"serial":"8-037"},{"count":2,"serial":"8-139"},{"count":1,"serial":"5-036"},{"count":3,"serial":"4-048"},{"count":1,"serial":"2-026"},{"count":3,"serial":"8-043"},{"count":3,"serial":"4-021"},{"count":3,"serial":"3-033"},{"count":1,"serial":"8-014"},{"count":2,"serial":"8-006"},{"count":1,"serial":"8-042"},{"count":1,"serial":"6-027"},{"count":3,"serial":"5-019"},{"count":2,"serial":"2-019"},{"count":2,"serial":"5-032"},{"count":3,"serial":"4-026"},{"count":3,"serial":"1-057"},{"count":1,"serial":"1-048"},{"count":2,"serial":"8-036"},{"count":3,"serial":"8-005"},{"count":3,"serial":"2-005"},{"count":1,"serial":"7-017"},{"count":1,"serial":"8-007"}]}');'''

            logger.info 'OK clear'

  close: ->
    logger.info 'shutting down'
    new Promise (resolve, reject) =>
      @db.close (err) ->
        if err
          logger.error "FAIL '#{err.message}'"
          reject null
        else
          logger.warn  "OK close '#{@filename}'"
          resolve null

  validate: (login, password) ->
    defined = (value) -> value? and value isnt ''

    new Promise (resolve, reject) =>
      if (defined login) and (defined password)
        # both are defined
        resolve null
      else
        # no user name or password given
        logger.info "validate: FAIL empty '#{login}' or password"
        reject null

  register: (login, password) ->
    new Promise (resolve, reject) =>
      # validate user input
      @validate login, password
      .then =>
        # hash password
        bcrypt.hash password, saltRounds, (err, hash) =>
          if err
            logger.warn "reg: FAIL hash for '#{login}'"
            reject messages.hash

          else
            # try creating row in users table
            stmt = @db.prepare 'INSERT INTO users (login, pwdhash) VALUES (?, ?)'
            stmt.run [login, hash], (err) ->
              stmt.finalize()
              if err
                logger.warn "reg: FAIL db '#{err.code}' for '#{login}'"
                # user already exists
                reject messages.exists

              else
                logger.info "reg: OK '#{login}'"
                # registration successful
                resolve null

      .catch ->
        reject messages.empty

  login: (login, password) ->
    new Promise (resolve, reject) =>
      # validate user input
      @validate login, password
      .then =>
        # get users table row
        stmt = @db.prepare 'SELECT * FROM users WHERE login = ?'
        stmt.get [login], (err, row) =>
          stmt.finalize()
          if err
            logger.warn "login: FAIL db '#{err.code}' for '#{login}'"
            reject messages.db

          else if not row
            # hash the password for timing attack reasons
            bcrypt.hash password, saltRounds, (err, hash) ->
              logger.debug "login: FAIL nonexistent '#{login}'"
              reject messages.noexists # user doesnt exist

          else
            bcrypt.compare password, row.pwdhash, (err, res) ->
              if err
                logger.warn "login: FAIL hash for '#{login}'"
                reject messages.hash

              if res == true
                logger.debug "login: OK '#{row.login}'"
                # login successful
                resolve row.user

              else
                logger.debug "login: FAIL password for '#{login}'"
                reject messages.password # login failed

      .catch ->
        reject messages.empty

  getUser: (userID) ->
    new Promise (resolve, reject) =>
      # get users table row
      stmt = @db.prepare 'SELECT * FROM users WHERE user = ?'
      stmt.get [userID], (err, row) =>
        stmt.finalize()
        if err
          logger.warn "get: FAIL db '#{err.code}' for '#{userID}'"
          reject messages.db

        else if not row
          logger.debug "get: FAIL nonexistent '#{userID}'"
          reject messages.noexists # user doesnt exist

        else
          resolve
            user: row.user
            login: row.login
            settings: row.settings

  addDeck: (userID, deckCards) ->
    new Promise (resolve, reject) =>
      # try creating row in decks table
      stmt = @db.prepare 'INSERT INTO decks (user, json) VALUES (?, ?)'
      stmt.run [userID, JSON.stringify deckCards], (err) ->
        stmt.finalize()
        if err
          logger.warn "addDeck: FAIL db '#{err.code}' for '#{userID}'"
          reject messages.db

        else
          logger.debug "addDeck: OK '#{@lastID}'"
          resolve @lastID

  modDeck: (userID, deckID, deckCards) ->
    new Promise (resolve, reject) =>
      stmt = @db.prepare 'UPDATE decks SET json = ? WHERE deck = ? AND user = ?'
      stmt.run [(JSON.stringify deckCards), deckID, userID], (err) ->
        stmt.finalize()
        if err
          logger.warn "modDeck: FAIL db '#{err.code}' for '#{deckID}'"
          reject messages.db
        else if @changes == 0
          logger.warn "no changes for input (#{userID}, #{deckID}, #{JSON.stringify deckCards})!"
          reject messages.db
        else
          resolve deckID

  getDecks: (userID) ->
    new Promise (resolve, reject) =>
      stmt = @db.prepare 'SELECT decks.deck, decks.json FROM decks INNER JOIN users ON decks.user = users.user WHERE users.user = ?'
      stmt.all [userID], (err, rows) ->
        stmt.finalize()
        if err
          logger.warn "getDecks: FAIL db '#{err.code}' for '#{userID}'"
          reject messages.db
        else
          logger.debug "getDecks: OK '#{userID}'"
          resolve (id: row.deck, content: JSON.parse row.json for row, i in rows)

  delDeck: (userID, deckID) ->
    new Promise (resolve, reject) =>
      stmt = @db.prepare 'DELETE FROM decks WHERE deck = ? AND user = ?'
      stmt.run [deckID, userID], (err) ->
        stmt.finalize()
        if err
          logger.warn "delDeck: FAIL db '#{err.code}' for '#{deckID}'"
          reject messages.db
        else
          logger.debug "delDeck: OK '#{deckID}'"
          resolve deckID


module.exports = new FFTCGDB path.resolve(__dirname, 'fftcg.db'), true
