errors = require '../commons/errors'
Payment = require './../models/Payment'
Prepaid = require '../models/Prepaid'
Product = require '../models/Product'
User = require '../models/User'
Handler = require '../commons/Handler'
{handlers} = require '../commons/mapping'
mongoose = require 'mongoose'
log = require 'winston'
sendwithus = require '../sendwithus'
slack = require '../slack'
config = require '../../server_config'
request = require 'request'
async = require 'async'
apple_utils = require '../lib/apple_utils'


PaymentHandler = class PaymentHandler extends Handler
  modelClass: Payment
  editableProperties: []
  postEditableProperties: ['purchased']
  jsonSchema: require '../../app/schemas/models/payment.schema'

  get: (req, res) ->
    return res.send([]) unless req.user
    q = Payment.find({recipient:req.user._id})
    q.exec((err, payments) ->
      return @sendDatabaseError(res, err) if err
      res.send(payments)
    )

  getByRelationship: (req, res, args...) ->
    relationship = args[1]
    return @getSchoolSalesAPI(req, res) if relationship is 'school_sales'
    super arguments...

  logPaymentError: (req, msg) ->
    log.warn "Payment Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'"

  makeNewInstance: (req) ->
    payment = super(req)
    payment.set 'purchaser', req.user._id
    payment.set 'recipient', req.user._id
    payment.set 'created', new Date().toISOString()
    payment

  getSchoolSalesAPI: (req, res, code) ->
    throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin()

    Payment.find({}, {amount: 1, created: 1, description: 1, prepaidID: 1, productID: 1, purchaser: 1, service: 1}).lean().exec (err, payments) =>
      return @sendDatabaseError(res, err) if err
      userIDs = [];
      schoolSales = []
      prepaidIDs = []
      prepaidPaymentMap = {}
      for payment in payments
        continue unless payment.amount > 0
        unless created = payment.created
          paymentID = mongoose.Types.ObjectId(payment._id)
          created = paymentID.getTimestamp()
        description = payment.description ? ''
        if prepaidID = payment.prepaidID
          unless prepaidPaymentMap[prepaidID.valueOf()]
            prepaidPaymentMap[prepaidID.valueOf()] = {_id: payment._id, amount: payment.amount, created: created, description: description, userID: payment.purchaser, prepaidID: prepaidID}
            prepaidIDs.push(prepaidID)
            userIDs.push(payment.purchaser)
        else if payment.productID is 'custom' or payment.service is 'external' or payment.service is 'invoice'
          schoolSales.push({_id: payment._id, amount: payment.amount, created: created, description: description, userID: payment.purchaser})
          userIDs.push(payment.purchaser)

      Prepaid.find({$and: [{_id: {$in: prepaidIDs}}, {type: 'course'}]}, {_id: 1}).lean().exec (err, prepaids) =>
        return @sendDatabaseError(res, err) if err
        for prepaid in prepaids
          schoolSales.push(prepaidPaymentMap[prepaid._id])

        User.find({_id: {$in: userIDs}}).lean().exec (err, users) =>
          return @sendDatabaseError(res, err) if err
          userMap = {}
          for user in users
            userMap[user._id] = user
          for schoolSale in schoolSales
            schoolSale.user = userMap[schoolSale.userID]

          @sendSuccess(res, schoolSales)

  post: (req, res, pathName) ->
    if pathName is 'check-stripe-charges'
      return @checkStripeCharges(req, res)

    if (not req.user) or req.user.isAnonymous()
      return @sendForbiddenError(res)

    appleReceipt = req.body.apple?.rawReceipt
    appleTransactionID = req.body.apple?.transactionID
    appleLocalPrice = req.body.apple?.localPrice
    stripeToken = req.body.stripe?.token
    stripeTimestamp = parseInt(req.body.stripe?.timestamp)
    productID = req.body.productID

    if pathName is 'custom'
      return @handleStripePaymentPost(req, res, stripeTimestamp, 'custom', stripeToken)

    if not (appleReceipt or (stripeTimestamp and productID))
      @logPaymentError(req, "Missing data. Apple? #{!!appleReceipt}. Stripe timestamp? #{!!stripeTimestamp}. Product id? #{!!productID}.")
      return @sendBadInputError(res, 'Need either apple.rawReceipt or stripe.timestamp and productID')

    if stripeTimestamp and not productID
      @logPaymentError(req, 'Missing stripe productID')
      return @sendBadInputError(res, 'Need productID if paying with Stripe.')

    if stripeTimestamp and (not stripeToken) and (not req.user.get('stripe')?.customerID)
      @logPaymentError(req, 'Missing stripe token')
      return @sendBadInputError(res, 'Need stripe.token if new customer.')

    if appleReceipt
      if not appleTransactionID
        @logPaymentError(req, 'Missing apple transaction id')
        return @sendBadInputError(res, 'Apple purchase? Need to specify which transaction.')
      @handleApplePaymentPost(req, res, appleReceipt, appleTransactionID, appleLocalPrice)
    else
      @handleStripePaymentPost(req, res, stripeTimestamp, productID, stripeToken)

  #- Apple payments

  handleApplePaymentPost: (req, res, receipt, transactionID, localPrice) ->
    #- verify receipt with Apple

    apple_utils.verifyReceipt(receipt, (err, body) =>
      if err or not body?.receipt?.in_app or (not body?.bundle_id is 'com.codecombat.CodeCombat')
        console.warn 'apple receipt error?', err, body
        @logPaymentError(req, 'Unable to verify apple receipt')
        @sendBadInputError(res, 'Unable to verify Apple receipt.')
        return

      transaction = _.find body.receipt.in_app, { transaction_id: transactionID }
      unless transaction
        @logPaymentError(req, 'Missing transaction given id.')
        return @sendBadInputError(res, 'Invalid transactionID.')

      #- Check existence
      transactionID = transaction.transaction_id
      criteria = { 'ios.transactionID': transactionID }
      Payment.findOne(criteria).exec((err, payment) =>

        if payment
          unless payment.get('recipient').equals(req.user._id)
            @logPaymentError(req, 'Cross user apple payment.')
            return @sendForbiddenError(res)

          @recalculateGemsFor(req.user, (err) =>
            if err
              @logPaymentError(req, 'Apple recalc db error.'+err)
              return @sendDatabaseError(res, err)
            @sendSuccess(res, @formatEntity(req, payment))
          )
          return

        payment = @makeNewInstance(req)
        payment.set 'service', 'ios'
        Product.findOne({name: transaction.product_id}).exec (err, product) =>
          return @sendDatabaseError(res, err) if err
          return @sendNotFoundError(res) if not product
          payment.set 'amount', product.get('amount')
          payment.set 'gems', product.get('gems')
          payment.set 'ios', {
            transactionID: transactionID
            rawReceipt: receipt
            localPrice: localPrice
          }

          validation = @validateDocumentInput(payment.toObject())
          if validation.valid is false
            @logPaymentError(req, 'Invalid apple payment object.')
            return @sendBadInputError(res, validation.errors)

          payment.save((err) =>
            if err
              @logPaymentError(req, 'Apple payment save error.'+err)
              return @sendDatabaseError(res, err)
            @incrementGemsFor(req.user, product.get('gems'), (err) =>
              if err
                @logPaymentError(req, 'Apple incr db error.'+err)
                return @sendDatabaseError(res, err)
              @sendPaymentSlackMessage user: req.user, payment: payment
              @sendCreated(res, @formatEntity(req, payment))
            )
          )
      )
    )

  #- Stripe payments

  handleStripePaymentPost: (req, res, timestamp, productID, token) ->

    # First, make sure we save the payment info as a Customer object, if we haven't already.
    if token
      customerID = req.user.get('stripe')?.customerID

      if customerID
        # old customer, new token. Save it.
        stripe.customers.update customerID, { card: token }, (err, customer) =>
          @beginStripePayment(req, res, timestamp, productID)

      else
        newCustomer = {
          card: token
          email: req.user.get('email')
          metadata: { id: req.user._id + '', slug: req.user.get('slug') }
        }

        stripe.customers.create newCustomer, (err, customer) =>
          if err
            @logPaymentError(req, 'Stripe customer creation error. '+err)
            return @sendDatabaseError(res, err)

          stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
          stripeInfo.customerID = customer.id
          req.user.set('stripe', stripeInfo)
          req.user.save (err) =>
            if err
              @logPaymentError(req, 'Stripe customer id save db error. '+err)
              return @sendDatabaseError(res, err)
            @beginStripePayment(req, res, timestamp, productID)

    else
      @beginStripePayment(req, res, timestamp, productID)


  beginStripePayment: (req, res, timestamp, productID) ->

    async.parallel([
      ((callback) ->
        criteria = { recipient: req.user._id, 'stripe.timestamp': timestamp }
        Payment.findOne(criteria).exec((err, payment) =>
          callback(err, payment)
        )
      ),
      ((callback) ->
        stripe.charges.list({customer: req.user.get('stripe')?.customerID}, (err, recentCharges) =>
          return callback(err) if err
          charge = _.find recentCharges.data, (c) -> c.metadata.timestamp is timestamp
          callback(null, charge)
        )
      ),
      ((callback) ->
        Product.findOne({name: productID}).exec (err, product) =>
          callback(err, product)
      )
    ],

      ((err, results) =>
        if err
          @logPaymentError(req, 'Stripe async load db error. '+err)
          return @sendDatabaseError(res, err)
        [payment, charge, product] = results

        if not product
          return @sendNotFoundError(res, 'could not find product with id '+productID)

        if not (payment or charge)
          # Proceed normally from the beginning
          @chargeStripe(req, res, product)

        else if charge and not payment
          # Initialized Payment. Start from charging.
          @recordStripeCharge(req, res, charge)

        else
          return @sendSuccess(res, @formatEntity(req, payment)) if product.get('name') is 'custom'

          # Charged Stripe and recorded it. Recalculate gems to make sure credited the purchase.
          @recalculateGemsFor(req.user, (err) =>
              if err
                @logPaymentError(req, 'Stripe recalc db error. '+err)
                return @sendDatabaseError(res, err)
              @sendPaymentSlackMessage user: req.user, payment: payment
              @sendSuccess(res, @formatEntity(req, payment))
          )
      )
    )

  chargeStripe: (req, res, product) ->
    amount = parseInt product.get('amount') ? req.body.amount
    return @sendError(res, 400, "Invalid amount.") if isNaN(amount)

    stripe.charges.create({
      amount: amount
      currency: 'usd'
      customer: req.user.get('stripe')?.customerID
      metadata: {
        productID: product.get('name')
        userID: req.user._id + ''
        gems: product.get('gems')
        timestamp: parseInt(req.body.stripe?.timestamp)
        description: req.body.description
      }
      receipt_email: req.user.get('email')
      statement_descriptor: 'CODECOMBAT.COM'
    }).then(
      # success case
      ((charge) => @recordStripeCharge(req, res, charge)),

      # error case
      ((err) =>
        if err.type in ['StripeCardError', 'StripeInvalidRequestError']
          @sendError(res, 402, err.message)
        else
          @logPaymentError(req, 'Stripe charge error. '+err)
          @sendDatabaseError(res, 'Error charging card, please retry.'))
    )

  recordStripeCharge: (req, res, charge) ->
    return @sendError(res, 500, 'Fake db error for testing.') if req.body.breakAfterCharging
    payment = @makeNewInstance(req)
    payment.set 'service', 'stripe'
    payment.set 'productID', charge.metadata.productID
    payment.set 'amount', parseInt(charge.amount)
    payment.set 'gems', parseInt(charge.metadata.gems) if charge.metadata.gems
    payment.set 'description', charge.metadata.description if charge.metadata.description
    payment.set 'stripe', {
      customerID: charge.customer
      timestamp: parseInt(charge.metadata.timestamp)
      chargeID: charge.id
    }

    validation = @validateDocumentInput(payment.toObject())
    if validation.valid is false
      @logPaymentError(req, 'Invalid stripe payment object.')
      return @sendBadInputError(res, validation.errors)
    payment.save((err) =>
      return @sendDatabaseError(res, err) if err
      return @sendCreated(res, @formatEntity(req, payment)) if payment.productID is 'custom'

      # Credit gems
      @incrementGemsFor(req.user, parseInt(charge.metadata.gems), (err) =>
        if err
          @logPaymentError(req, 'Stripe incr db error. '+err)
          return @sendDatabaseError(res, err)
        @sendPaymentSlackMessage user: req.user, payment: payment
        @sendCreated(res, @formatEntity(req, payment))
      )
    )

  #- Confirm all Stripe charges are recorded on our server

  checkStripeCharges: (req, res) ->
    return @sendSuccess(res) unless customerID = req.user.get('stripe')?.customerID
    async.parallel([
        ((callback) ->
          criteria = { recipient: req.user._id, 'stripe.invoiceID': { $exists: false }, 'ios.transactionID': { $exists: false } }
          Payment.find(criteria).limit(100).sort({_id:-1}).exec((err, payments) =>
            callback(err, payments)
          )
        ),
        ((callback) ->
          stripe.charges.list({customer: customerID, limit: 100}, (err, recentCharges) =>
            return callback(err) if err
            callback(null, recentCharges.data)
          )
        )
      ],

      ((err, results) =>
        if err
          @logPaymentError(req, 'Stripe async load db error. '+err)
          return @sendDatabaseError(res, err)

        [payments, charges] = results
        recordedChargeIDs = (p.get('stripe').chargeID for p in payments)
        for charge in charges
          continue unless charge.paid
          continue if charge.invoice # filter out subscription charges
          if charge.id not in recordedChargeIDs
            return @recordStripeCharge(req, res, charge)

        @sendSuccess(res)
      )
    )

  #- Incrementing/recalculating gems

  incrementGemsFor: (user, gems, done) ->
    if not gems
      return done()
      
    purchased = _.clone(user.get('purchased'))
    if not purchased?.gems
      purchased ?= {}
      purchased.gems = gems
      user.set('purchased', purchased)
      user.save((err) -> done(err))

    else
      user.update({$inc: {'purchased.gems': gems}}, {}, (err) -> done(err))

  recalculateGemsFor: (user, done, saveIfUnchanged=true) ->

    Payment.find({recipient: user._id}).select('gems').exec((err, payments) ->
      gems = _.reduce payments, ((sum, p) -> sum + p.get('gems')), 0
      purchased = _.clone(user.get('purchased'))
      purchased ?= {}
      if (purchased.gems or 0) isnt gems
        log.debug "Updating #{user.get('_id')} gems from #{purchased.gems} to #{gems} from #{payments.length} payments; #{user.get('email')} #{user.get('name')}"
      else unless saveIfUnchanged
        log.debug "#{user.get('_id')} already had #{purchased.gems} #{gems} from #{payments.length} payments; #{user.get('email')} #{user.get('name')}"
        return done()
      purchased.gems = gems
      user.set('purchased', purchased)
      user.save((err) -> done(err))
    )

  sendPaymentSlackMessage: (options) ->
    try
      message = "#{options.user?.get('emailLower')} paid #{options.payment?.get('amount')} for #{options.payment.get('description') or '???, no payment description!'}"
      slack.sendSlackMessage message, ['tower']
    catch e
      log.error "Couldn't send Slack message on payment because of error: #{e}"

module.exports = new PaymentHandler()
