Grid = require 'gridfs-stream'
fs = require 'fs'
request = require 'request'
mongoose = require 'mongoose'
errors = require '../commons/errors'
config = require '../../server_config'

module.exports.setup = (app) ->
  app.all '/file*', (req, res) ->
    return fileGet(req, res) if req.route.method is 'get'
    return filePost(req, res) if req.route.method is 'post'
    return fileDelete(req, res) if req.route.method is 'delete'
    return errors.badMethod(res, ['GET', 'POST', 'DELETE'])

fileDelete = (req, res) ->
  return errors.forbidden(res) unless req.user
  query = parsePathIntoQuery(req.path)
  return errors.badInput(res) if not query.filename
  Grid.gfs.collection('media').findOne query, (err, filedata) =>
    return errors.notFound(res) if not filedata
    return errors.forbidden(res) unless userCanEditFile(req.user, filedata)
    Grid.gfs.remove {_id: filedata._id, root: 'media'}, (err) ->
      return errors.serverError(res) if err
      return res.end()

fileGet = (req, res) ->
  query = parsePathIntoQuery(req.path)

  if not query.filename # it's a folder, return folder contents
    Grid.gfs.collection('media').find query, (err, cursor) ->
      return errors.serverError(res) if err
      results = cursor.toArray (err, results) ->
        return errors.serverError(res) if err
        res.setHeader('Content-Type', 'text/json')
        res.send(results)
        res.end()

  else # it's a single file
    Grid.gfs.collection('media').findOne query, (err, filedata) =>
      return errors.notFound(res) if not filedata
      readstream = Grid.gfs.createReadStream({_id: filedata._id, root: 'media'})
      if req.headers['if-modified-since'] is filedata.uploadDate
        res.status(304)
        return res.end()

      res.setHeader('Content-Type', filedata.contentType)
      res.setHeader('Last-Modified', filedata.uploadDate)
      res.setHeader('Cache-Control', 'public')
      readstream.pipe(res)
      handleStreamEnd(res, res)

parsePathIntoQuery = (path) ->
  path = path[6..]
  path = decodeURI path
  try
    objectId = mongoose.Types.ObjectId(path)
    query = objectId
  catch e
    path = path.split('/')
    filename = path[path.length-1]
    path = path[...path.length-1].join('/')
    query =
      'metadata.path': path
    query.filename = filename if filename

  query

postFileSchema =
  type: 'object'
  properties:
    # source
    url: { type: 'string', description: 'The url to download the file from.' }
    postName: { type: 'string', description: 'The input field this file was sent on.' }
    b64png: { type: 'string', description: 'Raw png data to upload.' }

    # options
    force: { type: 'string', 'default': '', description: 'Whether to overwrite existing files (as opposed to throwing an error).' }

    # metadata
    filename: { type: 'string', description: 'What the file will be named in the system.' }
    mimetype: { type: 'string' }
    name: { type: 'string', description: 'Human readable and searchable string.' }
    description: { type: 'string' }
    path: { type: 'string', description: 'What "folder" this file goes into.' }

  required: ['filename', 'mimetype', 'path']

filePost = (req, res) ->
  return errors.forbidden(res) unless req.user
  options = req.body
  tv4 = require('tv4').tv4
  valid = tv4.validate(options, postFileSchema)
  hasSource = options.url or options.postName or options.b64png
  # TODO : give tv4.error  to badInput
  return errors.badInput(res) if (not valid) or (not hasSource)
  return saveURL(req, res) if options.url
  return saveFile(req, res) if options.postName
  return savePNG(req, res) if options.b64png

saveURL = (req, res) ->
  options = createPostOptions(req)
  checkExistence options, req, res, req.body.force, (err) ->
    return errors.serverError(res) if err
    writestream = Grid.gfs.createWriteStream(options)
    request(req.body.url).pipe(writestream)
    handleStreamEnd(res, writestream)

saveFile = (req, res) ->
  options = createPostOptions(req)
  checkExistence options, req, res, req.body.force, (err) ->
    return if err
    writestream = Grid.gfs.createWriteStream(options)
    f = req.files[req.body.postName]
    fileStream = fs.createReadStream(f.path)
    fileStream.pipe(writestream)
    handleStreamEnd(res, writestream)

savePNG = (req, res) ->
  options = createPostOptions(req)
  checkExistence options, req, res, req.body.force, (err) ->
    return if err
    writestream = Grid.gfs.createWriteStream(options)
    img = new Buffer(req.body.b64png, 'base64')
    streamBuffers = require 'stream-buffers'
    myReadableStreamBuffer = new streamBuffers.ReadableStreamBuffer({frequency: 10, chunkSize: 2048})
    myReadableStreamBuffer.put(img)
    myReadableStreamBuffer.pipe(writestream)
    handleStreamEnd(res, writestream)

userCanEditFile = (user=null, file=null) ->
  # no user means 'anyone'. No file means 'any file'
  return false unless user
  return true if user.isAdmin() or user.isArtisan()
  return false unless file
  return true if file.metadata.creator is user.id
  return false

checkExistence = (options, req, res, force, done) ->
  q = {
    filename: options.filename
    'metadata.path': options.metadata.path
  }
  Grid.gfs.collection('media').find(q).toArray (err, files) ->
    file = files[0]
    if file and ((not userCanEditFile(req.user, file) or (not force)))
      errors.conflict(res, {canForce: userCanEditFile(req.user, file)})
      done(true)
    else if file
      fullPath = "/file/#{options.metadata.path}/#{options.filename}"
      clearCloudFlareCacheForFile(fullPath)
      q = { _id: file._id }
      q.root = 'media'
      Grid.gfs.remove q, (err) ->
        if err
          errors.serverError(res)
          return done(true)
        done()
    else
      done()

handleStreamEnd = (res, stream) ->
  stream.on 'close', (f) ->
    res.send(f)
    res.end()

  stream.on 'error', ->
    return errors.serverError(res)

CHUNK_SIZE = 1024*256

createPostOptions = (req) ->
  unless req.body.name
    name = req.body.filename.split('.')[0]
    req.body.name = _.str.humanize(name)

  path = req.body.path or ''
  path = path[1...] if path and path[0] is '/'
  path = path[...path.length-2] if path and path[path.length-1] is '/'

  options =
    mode: 'w'
    filename: req.body.filename
    chunk_size: CHUNK_SIZE
    root: 'media'
    content_type: req.body.mimetype
    metadata:
      name: req.body.name
      path: path
      creator: ''+req.user._id
  options.metadata.description = req.body.description if req.body.description?

  options

clearCloudFlareCacheForFile = (path='/file') ->
  unless config.cloudflare.token
    console.log 'skipping clearing cloud cache, not configured'
    return

  request = require 'request'
  r = request.post 'https://www.cloudflare.com/api_json.html', (err, httpResponse, body) ->
    if (err)
      console.error('CloudFlare file cache clear failed:', body)

  form = r.form()
  form.append 'tkn', config.cloudflare.token
  form.append 'email', 'scott@codecombat.com'
  form.append 'z', 'codecombat.com'
  form.append 'a', 'zone_file_purge'
  form.append 'url', "http://codecombat.com#{path}"
