fs = require "fs"
Q = require "q"
Client = require("../model/clients").Client
Keystore = require("../model/keystore").Keystore
logger = require "winston"
utils = require '../utils'
pem = require 'pem'
rootCAs = require('ssl-root-cas/latest').rootCas

config = require '../config/config'
config.tlsClientLookup = config.get('tlsClientLookup')
statsdServer = config.get 'statsd'
application = config.get 'application'
SDC = require 'statsd-client'
os = require 'os'

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

###
# Fetches the trusted certificates, callsback with an array of certs.
###
exports.getTrustedClientCerts = (done) ->
  Keystore.findOne (err, keystore) ->
    done err, null if err
    certs = rootCAs
    if keystore.ca?
      for cert in keystore.ca
        certs.push cert.data

    return done null, certs

###
# Gets server options object for use with a HTTPS node server
#
# mutualTLS is a boolean, when true mutual TLS authentication is enabled
###
exports.getServerOptions = (mutualTLS, done) ->
  Keystore.findOne (err, keystore) ->
    if err
      logger.error "Could not fetch keystore: #{err}"
      return done err

    if keystore?
      options =
        key:  keystore.key
        cert: keystore.cert.data

      #if key has password add it to the options
      if keystore.passphrase
        options.passphrase = keystore.passphrase

    else
      return done(new Error 'Keystore does not exist')

    if mutualTLS
      exports.getTrustedClientCerts (err, certs) ->
        if err
          logger.error "Could not fetch trusted certificates: #{err}"
          return done err, null

        options.ca = certs
        options.requestCert = true
        options.rejectUnauthorized = false  # we test authority ourselves
        done null, options
    else
      done null, options

###
# A promise returning function that lookups up a client via the given cert fingerprint,
# if not found and config.tlsClientLookup.type is 'in-chain' then the function will
# recursively walk up the certificate chain and look for clients with certificates
# higher in the chain.
###
clientLookup = (fingerprint, subjectCN, issuerCN) ->
  logger.debug "Looking up client linked to cert with fingerprint #{fingerprint} with subject #{subjectCN} and issuer #{issuerCN}"
  deferred = Q.defer()

  Client.findOne certFingerprint: fingerprint, (err, result) ->
    deferred.reject err if err

    if result?
      # found a match
      return deferred.resolve result

    if subjectCN is issuerCN
      # top certificate reached
      return deferred.resolve null

    if config.tlsClientLookup.type is 'in-chain'
      # walk further up and cert chain and check
      utils.getKeystore (err, keystore) ->
        deferred.reject err if err
        missedMatches = 0
        # find the isser cert
        if not keystore.ca? || keystore.ca.length < 1
          logger.info "Issuer cn=#{issuerCN} for cn=#{subjectCN} not found in keystore."
          deferred.resolve null
        else
          for cert in keystore.ca
            do (cert) ->
              pem.readCertificateInfo cert.data, (err, info) ->
                if err
                  return deferred.reject err

                if info.commonName is issuerCN
                  promise = clientLookup cert.fingerprint, info.commonName, info.issuer.commonName
                  promise.then (result) -> deferred.resolve result
                else
                  missedMatches++

                if missedMatches is keystore.ca.length
                  logger.info "Issuer cn=#{issuerCN} for cn=#{subjectCN} not found in keystore."
                  deferred.resolve null
    else
      if config.tlsClientLookup.type isnt 'strict'
        logger.warn "tlsClientLookup.type config option does not contain a known value, defaulting to 'strict'. Available options are 'strict' and 'in-chain'."
      deferred.resolve null

  return deferred.promise

if process.env.NODE_ENV == "test"
  exports.clientLookup = clientLookup

###
# Koa middleware for mutual TLS authentication
###
exports.koaMiddleware = (next) ->
  startTime = new Date() if statsdServer.enabled
  if this.authenticated?
    yield next
  else
    if this.req.client.authorized is true
      cert = this.req.connection.getPeerCertificate true
      logger.info "#{cert.subject.CN} is authenticated via TLS."

      # lookup client by cert fingerprint and set them as the authenticated user
      try
        this.authenticated = yield clientLookup cert.fingerprint, cert.subject.CN, cert.issuer.CN
      catch err
        logger.error "Failed to lookup client: #{err}"

      if this.authenticated?
        if this.authenticated.clientID?
          this.header['X-OpenHIM-ClientID'] = this.authenticated.clientID
        sdc.timing "#{domain}.tlsAuthenticationMiddleware", startTime if statsdServer.enabled
        this.authenticationType = 'tls'
        yield next
      else
        this.authenticated = null
        logger.info "Certificate Authentication Failed: the certificate's fingerprint #{cert.fingerprint} did not match any client's certFingerprint attribute, trying next auth mechanism if any..."
        sdc.timing "#{domain}.tlsAuthenticationMiddleware", startTime if statsdServer.enabled
        yield next
    else
      this.authenticated = null
      logger.info "Could NOT authenticate via TLS: #{this.req.client.authorizationError}, trying next auth mechanism if any..."
      sdc.timing "#{domain}.tlsAuthenticationMiddleware", startTime if statsdServer.enabled
      yield next
