# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""URL endpoint for getting information about histogram upload proccess."""
from __future__ import absolute_import

import logging
import uuid

from google.appengine.ext import ndb

from dashboard.api import api_request_handler
from dashboard.common import histogram_helpers
from dashboard.models import histogram
from dashboard.models import upload_completion_token
from tracing.value import histogram as histogram_module
from tracing.value.diagnostics import diagnostic as diagnostic_module
from tracing.value.diagnostics import generic_set
from tracing.value.diagnostics import diagnostic_ref


def _IsValidUuid(val):
  try:
    uuid.UUID(str(val))
    return True
  except ValueError:
    return False


# pylint: disable=abstract-method
class UploadInfoHandler(api_request_handler.ApiRequestHandler):
  """Request handler to get information about upload completion tokens."""

  def _CheckUser(self):
    self._CheckIsInternalUser()

  @ndb.tasklet
  def _GenerateMeasurementInfo(self, measurement, get_dimensions_info=False):
    info = {
        'name': measurement.test_path,
        'state': upload_completion_token.StateToString(measurement.state),
        'monitored': measurement.monitored,
        'lastUpdated': str(measurement.update_time),
    }
    if measurement.error_message is not None:
      info['error_message'] = measurement.error_message
    if get_dimensions_info and measurement.histogram is not None:
      histogram_entity = yield measurement.histogram.get_async()
      attached_histogram = histogram_module.Histogram.FromDict(
          histogram_entity.data)
      info['dimensions'] = []
      for name, diagnostic in attached_histogram.diagnostics.items():
        if name not in histogram_helpers.ADD_HISTOGRAM_RELATED_DIAGNOSTICS:
          continue

        if isinstance(diagnostic, diagnostic_ref.DiagnosticRef):
          original_diagnostic = histogram.SparseDiagnostic.get_by_id(
              diagnostic.guid)
          diagnostic = diagnostic_module.Diagnostic.FromDict(
              original_diagnostic.data)

        # We don't have other diagnostics in
        # histogram_helpers.ADD_HISTOGRAM_RELATED_DIAGNOSTICS if they apper
        # in the future, dimensions format should be changed.
        assert isinstance(diagnostic, generic_set.GenericSet)

        info['dimensions'].append({'name': name, 'value': list(diagnostic)})
    raise ndb.Return(info)

  def _GenerateResponse(self,
                        token,
                        get_measurement_info=False,
                        get_dimensions_info=False):
    measurements = token.GetMeasurements() if get_measurement_info else []
    result = {
        'token': token.key.id(),
        'file': token.temporary_staging_file_path,
        'created': str(token.creation_time),
        'lastUpdated': str(token.update_time),
        'state': upload_completion_token.StateToString(token.state),
    }
    if token.error_message is not None:
      result['error_message'] = token.error_message
    if measurements:
      result['measurements'] = []

    measurement_info_futures = []
    for measurement in measurements:
      measurement_info_futures.append(
          self._GenerateMeasurementInfo(measurement, get_dimensions_info))
    ndb.Future.wait_all(measurement_info_futures)
    for future in measurement_info_futures:
      result['measurements'].append(future.get_result())
    return result

  def Get(self, *args):
    """Returns json, that describes state of the token.

    Can be called by get request to /uploads/<token_id>. Measurements info can
    be requested with GET parameter ?additional_info=measurements. If dimentions
    info is also required use ?additional_info=measurements,dimentions.

    Response is json of the form:
    {
      "token": "...",
      "file": "...",
      "created": "...",
      "lastUpdated": "...",
      "state": "PENDING|PROCESSING|FAILED|COMPLETED",
      "error_message": "...",
      "measurements": [
        {
          "name": "...",
          "state": "PROCESSING|FAILED|COMPLETED",
          "monitored": True|False,
          "lastUpdated": "...",
          "error_message": "...",
          "dimentions": [
            {
              "name": "...",
              "values": ["...", ...]
            },
            ...
          ]
        },
        ...
      ]
    }
    Description of the fields:
      - token: Token id from the request.
      - file: Temporary staging file path, where /add_histogram request data is
        stored during the PENDING stage. For more information look at
        /add_histogram api.
      - created: Date and time of creation.
      - lastUpdated: Date and time of last update.
      - state: Aggregated state of the token and all associated measurements.
      - error_message: If historgam upload failed on token level (during
        /add_histogram) will contain addition information about failure.
        Absent otherwise.
      - measurements: List of json objects, that describes measurements
        associated with the token. If there are no such measurements, the field
        will be absent. This field may be absent if full information is not
        requested.
        - name: The path  of the measurement. It is a fully-qualified path in
          the Dashboard.
        - state: State of the measurement.
        - error_message: If state is FAILED, contains addition information
          about failure. Absent otherwise.
        - monitored: A boolean indicating whether the path is monitored by a
          sheriff configuration.
        - lastUpdated: Date and time of last update.
        - dimentions: List of relevant for /add_histogram api diagnostics,
          associated to the histogram, that is represented by the measurement.
          This field will be present in response only after the histogram has
          been added to Datastore. This field may be absent if full information
          is not requested.
          - name: Name of the diagnostic.
          - values: List of values, stored in the GenericSet diagnostic.

    Meaning of some common error codes:
      - 400: Invalid format of the token id.
      - 403: The user is not authorized to check on the status of an upload.
      - 404: Token could not be found. It is either expired or was never
        created.
    """
    assert len(args) == 1

    token_id = args[0]
    if not _IsValidUuid(token_id):
      logging.error('Upload completion token id is not valid. Token id: %s',
                    token_id)
      raise ValueError

    token = upload_completion_token.Token.get_by_id(token_id)
    if token is None:
      logging.error('Upload completion token not found. Token id: %s', token_id)
      raise api_request_handler.NotFoundError

    get_measurement_info = 'measurements' in self.request.get('additional_info')
    get_dimensions_info = 'dimensions' in self.request.get('additional_info')
    return self._GenerateResponse(token, get_measurement_info,
                                  get_dimensions_info)
