Q = require "q"
xpath = require "xpath"
dom = require("xmldom").DOMParser
logger = require "winston"
config = require '../config/config'
utils = require '../utils'
auditing = require '../auditing'
Channels = require('../model/channels')
Channel = Channels.Channel

statsdServer = config.get 'statsd'
application = config.get 'application'
himSourceID = config.get('auditing').auditEvents.auditSourceID
SDC = require 'statsd-client'
os = require 'os'

domain = "#{os.hostname()}.#{application.name}.appMetrics"
sdc = new SDC statsdServer

matchContent = (channel, ctx) ->
  if channel.matchContentRegex
    return matchRegex channel.matchContentRegex, ctx.body
  else if channel.matchContentXpath and channel.matchContentValue
    return matchXpath channel.matchContentXpath, channel.matchContentValue, ctx.body
  else if channel.matchContentJson and channel.matchContentValue
    return matchJsonPath channel.matchContentJson, channel.matchContentValue, ctx.body
  else if channel.matchContentXpath or channel.matchContentJson
    # if only the match expression is given, deny access
    # this is an invalid channel
    logger.error "Channel with name '#{channel.name}' is invalid as it has a content match expression but no value to match"
    return false
  else
    return true

matchRegex = (regexPat, body) ->
  regex = new RegExp regexPat
  return regex.test body.toString()

matchXpath = (xpathStr, val, xml) ->
  doc = new dom().parseFromString(xml.toString())
  xpathVal = xpath.select(xpathStr, doc).toString()
  return val == xpathVal

matchJsonPath = (jsonPath, val, json) ->
  jsonObj = JSON.parse json.toString()
  jsonVal = getJSONValByString jsonObj, jsonPath
  return val == jsonVal.toString()

# taken from http://stackoverflow.com/a/6491621/588776
# readbility improved from the stackoverflow answer
getJSONValByString = (jsonObj, jsonPath) ->
  jsonPath = jsonPath.replace(/\[(\w+)\]/g, '.$1')  # convert indexes to properties
  jsonPath = jsonPath.replace(/^\./, '')            # strip a leading dot
  parts = jsonPath.split('.')
  while parts.length
    part = parts.shift()
    if part of jsonObj
      jsonObj = jsonObj[part]
    else
      return
  return jsonObj

extractContentType = (ctHeader) ->
  index = ctHeader.indexOf ';'
  if index isnt -1
    return ctHeader.substring(0, index).trim()
  else
    return ctHeader.trim()

matchUrlPattern = (channel, ctx) ->
  pat = new RegExp channel.urlPattern
  return pat.test ctx.request.path

matchContentTypes = (channel, ctx) ->
  if channel.matchContentTypes?.length > 0
    if ctx.request.header and ctx.request.header['content-type']
      ct = extractContentType ctx.request.header['content-type']
      if ct in channel.matchContentTypes
        return true
      else
        # deny access to channel if the content type doesnt match
        return false
    else
      # deny access to channel if the content type isnt set
      return false
  else
    return true # don't match on content type if this channel doesn't require it

matchFunctions = [
  matchUrlPattern,
  matchContent,
  matchContentTypes
]

matchChannel = (channel, ctx) ->
  matchFunctions.every (matchFunc) ->
    return matchFunc channel, ctx

findMatchingChannel = (channels, ctx) ->
  return channels.find (channel) ->
    return matchChannel channel, ctx

matchRequest = (ctx, done) ->
  utils.getAllChannelsInPriorityOrder (err, channels) ->
    if err
      ctx.response.status = 500
      logger.error 'Could not fetch OpenHIM channels', err
      return done()

    channels = channels.filter Channels.isChannelEnabled

    match = findMatchingChannel channels, ctx
    done null, match

exports.koaMiddleware = (next) ->
  startTime = new Date() if statsdServer.enabled
  matchReq = Q.denodeify matchRequest
  match = yield matchReq this

  if match?
    logger.info "The channel that matches the request #{this.request.path} is: #{match.name}"
    this.matchingChannel = match
  else
    logger.info "No channel matched the request #{this.request.path}"

  sdc.timing "#{domain}.authorisationMiddleware", startTime if statsdServer.enabled
  yield next

# export private functions for unit testing
# note: you cant spy on these method because of this :(
if process.env.NODE_ENV == "test"
  exports.matchContent = matchContent
  exports.matchRegex = matchRegex
  exports.matchXpath = matchXpath
  exports.matchJsonPath = matchJsonPath
  exports.extractContentType = extractContentType
  exports.matchRequest = matchRequest
