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

# bruteforce countermeasure
saltRounds = 13

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

            logger.info 'OK clear'

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

  register: (login, password) ->
    new Promise (resolve, reject) =>
      # validate user input
      if login == '' or password == ''
        # no user name or password given
        logger.info "reg: FAIL empty '#{login}' or password"
        reject 'invalid'

      # hash password
      bcrypt.hash password, saltRounds, (err, hash) =>
        if err
          logger.warn "reg: FAIL hash for '#{login}'"
          reject '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}'"
              # reduce attack surface, don't disclose user names
              reject 'db' # user already exists

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

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

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

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

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

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

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

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

  modDeck: (deckID, deckCards) ->
    new Promise (resolve, reject) =>
      stmt = @db.prepare 'UPDATE decks SET json = ? WHERE deck = ?'
      stmt.run [deckCards, deckID], (err) ->
        stmt.finalize()
        if err
          logger.warn "modDeck: FAIL db '#{err.code}' for '#{deckID}'"
          reject 'db'
        else
          logger.debug "modDeck: OK '#{deckID}'"
          resolve deckID

  getDecks: (user) ->
    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 [user], (err, rows) ->
        stmt.finalize()
        if err
          logger.warn "getDeck: FAIL db '#{err.code}' for '#{deckID}'"
          reject 'db'
        else
          logger.debug "getDeck: OK '#{deckID}'"
          resolve (id: row.deck, content: JSON.parse row.json for row, i in rows)

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


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