'use strict'



Application.Controllers.controller "StatisticsController", ["$scope", "$state", "$rootScope", '$uibModal', "es", "Member", '_t', 'membersPromise', 'statisticsPromise'
, ($scope, $state, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise) ->



  ### PRIVATE STATIC CONSTANTS ###

  ## search window size
  RESULTS_PER_PAGE = 20

  ## keep search context for (delay in minutes) ...
  ES_SCROLL_TIME = 1



  ### PUBLIC SCOPE ###

  ## ui-view transitions optimization: if true, the stats will never be refreshed
  $scope.preventRefresh = false

  ## statistics structure in elasticSearch
  $scope.statistics = statisticsPromise

  ## fablab users list
  $scope.members = membersPromise

  ## statistics data recovered from elasticSearch
  $scope.data = null

  ## when did the search was triggered
  $scope.searchDate = null

  ## id of the elastic search context
  $scope.scrollId = null

  ## total number of results for the current query
  $scope.totalHits = null

  ## configuration of the widget allowing to pick the ages range
  $scope.agePicker =
    show: false
    start: null
    end: null

  ## total CA for the current view
  $scope.sumCA = 0

  ## average users' age for the current view
  $scope.averageAge = 0

  ## total of the stat field for non simple types
  $scope.sumStat = 0

  ## Results of custom aggregations for the current type
  $scope.customAggs = {}

  ## default: results are not sorted
  $scope.sorting =
    ca: 'none'
    date: 'desc'

  ## active tab will be set here
  $scope.selectedIndex = null

  ## type filter binding
  $scope.type =
    selected: null
    active: null

  ## selected custom filter
  $scope.customFilter =
    show: false
    criterion: {}
    value : null
    exclude: false
    datePicker:
      format: Fablab.uibDateFormat
      opened: false # default: datePicker is not shown
      minDate: null
      maxDate: moment().toDate()
      options:
        startingDay: 1 # France: the week starts on monday

  ## available custom filters
  $scope.filters = []

  ## default: we do not open the datepicker menu
  $scope.datePicker =
    show: false

  ## datePicker parameters for interval beginning
  $scope.datePickerStart =
    format: Fablab.uibDateFormat
    opened: false # default: datePicker is not shown
    minDate: null
    maxDate: moment().subtract(1, 'day').toDate()
    selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate()
    options:
      startingDay: Fablab.weekStartingDay

  ## datePicker parameters for interval ending
  $scope.datePickerEnd =
    format: Fablab.uibDateFormat
    opened: false # default: datePicker is not shown
    minDate: null
    maxDate: moment().subtract(1, 'day').toDate()
    selected: moment().subtract(1, 'day').endOf('day').toDate()
    options:
      startingDay: Fablab.weekStartingDay



  ##
  # Callback to open the datepicker (interval start)
  # @param $event {Object} jQuery event object
  ##
  $scope.toggleStartDatePicker = ($event) ->
    toggleDatePicker($event, $scope.datePickerStart)



  ##
  # Callback to open the datepicker (interval end)
  # @param $event {Object} jQuery event object
  ##
  $scope.toggleEndDatePicker = ($event) ->
    toggleDatePicker($event, $scope.datePickerEnd)



  ##
  # Callback to open the datepicker (custom filter)
  # @param $event {Object} jQuery event object
  ##
  $scope.toggleCustomDatePicker = ($event) ->
    toggleDatePicker($event, $scope.customFilter.datePicker)



  ##
  # Callback called when the active tab is changed.
  # recover the current tab and store its value in $scope.selectedIndex
  # @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
  ##
  $scope.setActiveTab = (tab) ->
    $scope.selectedIndex = tab
    $scope.type.selected = tab.types[0]
    $scope.type.active = $scope.type.selected
    $scope.customFilter.criterion = {}
    $scope.customFilter.value = null
    $scope.customFilter.exclude = false
    $scope.sorting.ca = 'none'
    $scope.sorting.date = 'desc'
    buildCustomFiltersList()
    refreshStats()



  ##
  # Returns true if the provided tab must be hidden due to some global or local configuration
  # @param tab {Object} elasticsearch statistic structure (from statistic_indices table)
  ##
  $scope.hiddenTab = (tab) ->
    if tab.table
      if tab.es_type_key == 'subscription' && $rootScope.fablabWithoutPlans
        true
      else if tab.es_type_key == 'space' && $rootScope.fablabWithoutSpaces
        true
      else
        false
    else
      true



  ##
  # Callback to validate the filters and send a new request to elastic
  ##
  $scope.validateFilterChange = ->
    $scope.agePicker.show = false
    $scope.customFilter.show = false
    $scope.type.active = $scope.type.selected
    buildCustomFiltersList()
    refreshStats()



  ##
  # Callback to validate the dates range and refresh the data from elastic
  ##
  $scope.validateDateChange = ->
    $scope.datePicker.show = false
    refreshStats()



  ##
  # Parse the given date and return a user-friendly string
  # @param date {Date} JS date or ant moment.js compatible date string
  ##
  $scope.formatDate = (date) ->
    moment(date).format("LL")



  ##
  # Parse the sex and return a user-friendly string
  # @param sex {string} 'male' | 'female'
  ##
  $scope.formatSex = (sex) ->
    if sex == 'male'
      return _t('man')
    if sex == 'female'
      return _t('woman')



  ##
  # Retrieve the label for the given subtype in the current type
  # @param key {string} statistic subtype key
  ##
  $scope.formatSubtype = (key) ->
    label = ""
    angular.forEach $scope.type.active.subtypes, (subtype) ->
      if subtype.key == key
        label = subtype.label
    label



  ##
  # Helper usable in ng-switch to determine the input type to display for custom filter value
  # @param filter {Object} custom filter criterion
  ##
  $scope.getCustomValueInputType = (filter) ->
    if filter and filter.values
      if typeof(filter.values[0]) == 'string'
        return filter.values[0]
      else if typeof(filter.values[0] == 'object')
        return 'input_select'
    else
      'input_text'



  ##
  # Change the sorting order and refresh the results to match the new order
  # @param filter {Object} any filter
  ##
  $scope.toggleSorting = (filter) ->
    switch $scope.sorting[filter]
      when 'none' then $scope.sorting[filter] = 'asc'
      when 'asc' then $scope.sorting[filter] = 'desc'
      when 'desc' then  $scope.sorting[filter] = 'none'
    refreshStats()



  ##
  # Return the user's name from his given ID
  # @param id {number} user ID
  ##
  $scope.getUserNameFromId = (id) ->
    name = $scope.members[id]
    return (if name then name else "ID "+id)



  ##
  # Run a scroll query to elasticsearch to append the next packet of results to those displayed.
  # If the ES search context has expired when the user ask for more results, we re-run the whole query.
  ##
  $scope.showMoreResults = ->
    # if all results were retrieved, do nothing
    if $scope.data.length >= $scope.totalHits
      return

    if moment($scope.searchDate).add(ES_SCROLL_TIME, 'minutes').isBefore(moment())
      # elastic search context has expired, so we run again the whole query
      refreshStats()
    else
      es.scroll
        "scroll": ES_SCROLL_TIME+'m'
        "body": {scrollId: $scope.scrollId}
      , (error, response) ->
        if (error)
          console.error "Error: something unexpected occurred during elasticSearch scroll query: "+error
        else
          $scope.scrollId = response._scroll_id
          $scope.data = $scope.data.concat(response.hits.hits)



  ##
  # Open a modal dialog asking the user for details about exporting the statistics tables to an excel file
  ##
  $scope.exportToExcel = ->
    options =
      templateUrl: '<%= asset_path "admin/statistics/export.html" %>'
      size: 'sm'
      controller: 'ExportStatisticsController'
      resolve:
        dates: ->
          start: $scope.datePickerStart.selected
          end: $scope.datePickerEnd.selected
        query: ->
          custom = buildCustomFilterQuery()
          buildElasticDataQuery($scope.type.active.key, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
        index: ->
          key: $scope.selectedIndex.es_type_key
        type: ->
          key: $scope.type.active.key

    $uibModal.open options
    .result['finally'](null).then (info)->
      console.log(info)




  ### PRIVATE SCOPE ###

  ##
  # Kind of constructor: these actions will be realized first when the controller is loaded
  ##
  initialize = ->
    # workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
    # which will cause every tabs to reload, one by one, when the view is closed
    $rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
      if fromState.name == 'app.admin.statistics' and Object.keys(fromParams).length == 0
        $scope.preventRefresh = true



  ##
  # Generic function to toggle a bootstrap datePicker
  # @param $event {Object} jQuery event object
  # @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
  ##
  toggleDatePicker = ($event, datePicker) ->
    $event.preventDefault()
    $event.stopPropagation()
    datePicker.opened = !datePicker.opened



  ##
  # Force update the statistics table, querying elasticSearch according to the current config values
  ##
  refreshStats = ->
    if $scope.selectedIndex and !$scope.preventRefresh
      $scope.data = []
      $scope.sumCA = 0
      $scope.averageAge = 0
      $scope.sumStat = 0
      $scope.customAggs = {}
      $scope.totalHits = null
      $scope.searchDate = new Date()
      custom = buildCustomFilterQuery()
      queryElasticStats $scope.selectedIndex.es_type_key, $scope.type.active.key, custom, (res, err)->
        if (err)
          console.error("[statisticsController::refreshStats] Unable to refresh due to "+err)
        else
          $scope.data = res.hits.hits
          $scope.totalHits = res.hits.total
          $scope.sumCA = res.aggregations.total_ca.value
          $scope.averageAge = Math.round(res.aggregations.average_age.value * 100) / 100
          $scope.sumStat = res.aggregations.total_stat.value
          $scope.scrollId = res._scroll_id
          for custom in $scope.type.active.custom_aggregations
            $scope.customAggs[custom.field] = res.aggregations[custom.field].value



  ##
  # Run the elasticSearch query to retreive the /stats/type aggregations
  # @param index {String} elasticSearch document type (account|event|machine|project|subscription|training)
  # @param type {String} statistics type (month|year|booking|hour|user|project)
  # @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
  # @param callback {function} function be to run after results were retrieved, it will receive
  #   two parameters : results {Object}, error {String} (if any)
  ##
  queryElasticStats = (index, type, custom, callback) ->
    # handle invalid callback
    if typeof(callback) != "function"
      console.error('[statisticsController::queryElasticStats] Error: invalid callback provided')
      return

    # run query
    es.search
      "index": "stats"
      "type": index
      "size": RESULTS_PER_PAGE
      "scroll": ES_SCROLL_TIME+'m'
      "stat-type": type
      "custom-query": if custom then JSON.stringify(Object.assign({exclude: custom.exclude}, buildElasticCustomCriterion(custom))) else ''
      "start-date": moment($scope.datePickerStart.selected).format()
      "end-date": moment($scope.datePickerEnd.selected).format()
      "body": buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting)
    , (error, response) ->
      if (error)
        callback({}, "Error: something unexpected occurred during elasticSearch query: "+error)
      else
        callback(response)



  ##
  # Build an object representing the content of the REST-JSON query to elasticSearch,
  # based on the provided parameters for row data recovering.
  # @param type {String} statistics type (month|year|booking|hour|user|project)
  # @param custom {{key:{string}, value:{string}}|null} custom filter property or null to disable this filter
  # @param ageMin {Number|null} filter by age: range lower value OR null to do not filter
  # @param ageMax {Number|null} filter by age: range higher value OR null to do not filter
  # @param intervalBegin {moment} statitics interval beginning (moment.js type)
  # @param intervalEnd {moment} statitics interval ending (moment.js type)
  # @param sortings {Array|null} elasticSearch criteria for sorting the results
  ##
  buildElasticDataQuery = (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) ->
    q =
      "query":
        "bool":
          "must": [
            {
              "term":
                "type": type
            }
            {
              "range":
                "date":
                  "gte": intervalBegin.format()
                  "lte": intervalEnd.format()
            }
          ]
    # optional date range
    if ageMin && ageMax
      q.query.bool.must.push
        "range":
          "age":
            "gte": ageMin
            "lte": ageMax
    # optional criterion
    if custom
      criterion = buildElasticCustomCriterion(custom)
      if (custom.exclude)
        q = "query": {
          "filtered": {
            "query": q.query,
            "filter": {
              "not": {
                "term": criterion.match
              }
            }
          }
        }
      else
        q.query.bool.must.push(criterion)


    if sortings
      q["sort"] = buildElasticSortCriteria(sortings)

    # aggregations (avg age & CA sum)
    q["aggs"] = {
      "total_ca":
        "sum":
          "field": "ca"
      "average_age":
        "avg":
          "field": "age"
      "total_stat":
        "sum":
          "field": "stat"
    }
    q



  ##
  # Build the elasticSearch query DSL to match the selected cutom filter
  # @param custom {Object} if custom is empty or undefined, an empty string will be returned
  # @returns {{match:{*}}|string}
  ##
  buildElasticCustomCriterion = (custom) ->
    if (custom)
      criterion = {
        "match" : {}
      }
      switch $scope.getCustomValueInputType($scope.customFilter.criterion)
        when 'input_date' then criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD')
        when 'input_select' then criterion.match[custom.key] = custom.value.key
        when 'input_list' then criterion.match[custom.key+".name"] = custom.value
        else criterion.match[custom.key] = custom.value
      criterion
    else
      ''



  ##
  # Parse the provided criteria array and return the corresponding elasticSearch syntax
  # @param criteria {Array} array of {key_to_sort:order}
  ##
  buildElasticSortCriteria = (criteria) ->
    crits = []
    angular.forEach criteria, (value, key) ->
      if typeof value != 'undefined' and value != null and value != 'none'
        c = {}
        c[key] = {'order': value}
        crits.push(c)
    crits



  ##
  # Fullfil the list of available options in the custom filter panel. The list will be based on common
  # properties and on index-specific properties (additional_fields)
  ##
  buildCustomFiltersList = ->
    $scope.filters = []

    filters = [
      {key: 'date', label: _t('date'), values: ['input_date']},
      {key: 'userId', label: _t('user_id'), values: ['input_number']},
      {key: 'gender', label: _t('gender'), values: [{key:'male', label:_t('man')}, {key:'female', label:_t('woman')}]},
      {key: 'age', label: _t('age'), values: ['input_number']},
      {key: 'subType', label: _t('type'), values: $scope.type.active.subtypes},
      {key: 'ca', label: _t('revenue'), values: ['input_number']}
    ]

    $scope.filters = filters

    if !$scope.type.active.simple
      f = {key: 'stat', label: $scope.type.active.label, values: ['input_number']}
      $scope.filters.push(f)

    angular.forEach $scope.selectedIndex.additional_fields, (field) ->
      filter = {key: field.key, label: field.label, values:[]}
      switch field.data_type
        when 'index' then filter.values.push('input_number')
        when 'number' then filter.values.push('input_number')
        when 'date' then filter.values.push('input_date')
        when 'list' then filter.values.push('input_list')
        else filter.values.push('input_text')

      $scope.filters.push(filter)



  ##
  # Build and return an object according to the custom filter set by the user, used to request elasticsearch
  # @return {Object|null}
  ##
  buildCustomFilterQuery = ->
    custom = null
    if !angular.isUndefinedOrNull($scope.customFilter.criterion) and
        !angular.isUndefinedOrNull($scope.customFilter.criterion.key) and
        !angular.isUndefinedOrNull($scope.customFilter.value)
      custom = {}
      custom.key = $scope.customFilter.criterion.key
      custom.value = $scope.customFilter.value
      custom.exclude = $scope.customFilter.exclude
    custom



  # init the controller (call at the end !)
  initialize()

]



Application.Controllers.controller 'ExportStatisticsController', [ '$scope', '$uibModalInstance', 'Export','dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t'
, ($scope, $uibModalInstance, Export, dates, query, index, type, CSRF, growl, _t) ->

  ## Retrieve Anti-CSRF tokens from cookies
  CSRF.setMetaTags()

  ## Bindings for date range
  $scope.dates = dates

  ## Body of the query to export
  $scope.query = JSON.stringify(query)

  ## API URL where the form will be posted
  $scope.actionUrl = '/stats/'+index.key+'/export'

  ## Key of the current search' statistic type
  $scope.typeKey = type.key

  ## Form action on the above URL
  $scope.method = "post"

  ## Anti-CSRF token to inject into the download form
  $scope.csrfToken = angular.element('meta[name="csrf-token"]')[0].content

  ## Binding of the export type (global / current)
  $scope.export =
    type: 'current'

  ## datePicker parameters for interval beginning
  $scope.exportStart =
    format: Fablab.uibDateFormat
    opened: false # default: datePicker is not shown
    minDate: null
    maxDate: moment().subtract(1, 'day').toDate()
    options:
      startingDay: Fablab.weekStartingDay

  ## datePicker parameters for interval ending
  $scope.exportEnd =
    format: Fablab.uibDateFormat
    opened: false # default: datePicker is not shown
    minDate: null
    maxDate: moment().subtract(1, 'day').toDate()
    options:
      startingDay: Fablab.weekStartingDay



  ##
  # Callback to open the datepicker (interval start)
  # @param $event {Object} jQuery event object
  ##
  $scope.toggleStartDatePicker = ($event) ->
    $scope.exportStart.opened = !$scope.exportStart.opened



  ##
  # Callback to open the datepicker (interval end)
  # @param $event {Object} jQuery event object
  ##
  $scope.toggleEndDatePicker = ($event) ->
    $scope.exportEnd.opened = !$scope.exportEnd.opened



  ##
  # Callback when exchanging the export type between 'global' and 'current view'
  # Adjust the query and the requesting url according to this type.
  ##
  $scope.setRequest = ->
    if $scope.export.type == 'global'
      $scope.actionUrl = '/stats/global/export'
      $scope.query = JSON.stringify(
        "query":
          "bool":
            "must": [
              {
                "range":
                  "date":
                    "gte": moment($scope.dates.start).format()
                    "lte": moment($scope.dates.end).format()
              }
            ]
      )
    else
      $scope.actionUrl = '/stats/'+index.key+'/export'
      $scope.query = JSON.stringify(query)



  ##
  # Callback to close the modal, telling the caller what is exported
  ##
  $scope.exportData = ->
    statusQry = {category: 'statistics', type: $scope.export.type, query: $scope.query}
    unless $scope.export.type == 'global'
      statusQry['type'] = index.key
      statusQry['key'] = type.key

    Export.status(statusQry).then (res) ->
      unless (res.data.exists)
        growl.success _t('export_is_running_you_ll_be_notified_when_its_ready')

    $uibModalInstance.close(statusQry)



  ##
  # Callback to cancel the export and close the modal
  ##
  $scope.cancel = ->
    $uibModalInstance.dismiss('cancel')
]
