# Copyright 2017 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.

import datetime
import json
import os
import pickle
import Queue
import re
import sys
import time
from threading import Thread

_FINDIT_DIR = os.path.join(
    os.path.dirname(__file__), os.path.pardir, os.path.pardir)
sys.path.insert(1, _FINDIT_DIR)
# Active script for Findit production.
from local_libs import remote_api
remote_api.EnableFinditRemoteApi()

from google.appengine.ext import ndb

from common.findit_http_client import FinditHttpClient
from libs import time_util
from model.wf_analysis import WfAnalysis
from pipeline_utils.appengine_third_party_pipeline_python_src_pipeline \
    import pipeline

_THIRD_PARTY_DIR = os.path.join(
    os.path.dirname(__file__), os.path.pardir, os.path.pardir, 'third_party')
sys.path.insert(0, _THIRD_PARTY_DIR)
import google
google.__path__.insert(0, os.path.join(_THIRD_PARTY_DIR, 'google'))

from handlers import handlers_util
from waterfall import swarming_util

NOW = time_util.GetUTCNow()
START_DATE, END_DATE = datetime.datetime(2017, 1, 1), NOW
THREAD_COUNT = 64

# If more than 1199 entities are requested at once, the ndb query.fetch_page()
# operation will stall.
MAX_ENTITIES_PER_REQUEST = 1199
CLIENT_SINGLETON = FinditHttpClient()


def _BigFetch(query, page_size=MAX_ENTITIES_PER_REQUEST):
  print 'Please wait. Fetching results at a rate of ~400 records per second...'
  cursor = None
  is_more = True
  big_results = []

  while is_more:
    results, cursor, is_more = query.fetch_page(page_size, start_cursor=cursor)
    big_results.extend(results)

  print 'Finished fetching results.'
  return big_results


def _FetchAnalyses(start_date, end_date):
  analyses_query = WfAnalysis.query(WfAnalysis.build_start_time >= start_date,
                                    WfAnalysis.build_start_time < end_date)

  return _BigFetch(analyses_query)


def _GetPickleFilePath():
  findit_tmp_dir = os.environ.get('TMP_DIR', os.getcwd())
  return (os.path.join(findit_tmp_dir, 'analyses.pickle'),
          os.path.join(findit_tmp_dir, 'records.pickle'))


def _SaveAnalyses(analyses, records):
  pickled_analyses = pickle.dumps(analyses)
  pickled_records = pickle.dumps(records)
  analyses_path, records_path = _GetPickleFilePath()
  with open(analyses_path, 'w') as f:
    f.write(pickled_analyses)
  with open(records_path, 'w') as f:
    f.write(pickled_records)


def _LoadAnalyses():
  analyses_path, records_path = _GetPickleFilePath()
  with open(analyses_path, 'r') as f:
    pickled_analyses = f.read()
  with open(records_path, 'r') as f:
    pickled_records = f.read()
  print 'Loading pickled analyses...'
  analyses = pickle.loads(pickled_analyses)
  times = [
      x.build_start_time for x in analyses if hasattr(x, 'build_start_time')
  ]
  records = pickle.loads(pickled_records)
  return analyses, min(times), max(times), records


def _GetTimesFromPipeline(pid):
  result = {}
  suffixes = {}
  status_tree = pipeline.get_status_tree(pid)
  for _, child_status in status_tree.get('pipelines', {}).iteritems():
    cls = child_status['classPath'].split('.')[-1]
    start = child_status.get('startTimeMs')
    end = child_status.get('endTimeMs')
    if start and end:
      if (cls + '.start' + suffixes.get(cls, '')) in result.keys():
        old_suffix = suffixes.get(cls, '.0')
        old_index = int(old_suffix[1:])
        suffixes[cls] = '.' + str(old_index + 1)
      result['pl.' + cls + '.start' + suffixes.get(cls, '')] = (
          _UnknownToDatetime(start))
      result['pl.' + cls + '.end' + suffixes.get(cls, '')] = (
          _UnknownToDatetime(end))
  return result


def _GetTimes(q, r):
  """Obtains times of interest from a WfAnalysis instance.

  This is meant to be run by a worker thread."""
  while True:
    a = q.get()
    result = {
        'wfa.build_start_time': a.build_start_time,
        'wfa.request_time': a.request_time,
        'wfa.start_time': a.start_time,
        'wfa.end_time': a.end_time,
        'wfa.updated_time': a.updated_time,
        'wfa.result_status': a.result_status,
        'wfa.build_failure_type': a.build_failure_type,
    }
    try:
      tryjobs_times = {}
      step_to_tryjobs = handlers_util.GetAllTryJobResults(
          a.master_name, a.builder_name, a.build_number, True)
      for step in step_to_tryjobs.keys():
        this_tryjobs = step_to_tryjobs[step]['try_jobs']
        for job in this_tryjobs:
          if job.get('try_job_url'):
            tryjobs_times.setdefault('try.' + step, {})
            times = _GetTimesFromBuildbot(job['try_job_url'])
            tryjobs_times['try.' + step].update(times)
          if job.get('task_url'):
            tryjobs_times.setdefault('swarm.' + step, {})
            times = _GetTimesFromSwarming(job['task_url'])
            tryjobs_times['swarm.' + step].update(times)

      result.update(tryjobs_times)
      if a.pipeline_status_path:
        pipeline_root = re.search(r'(?<=root\=)[^&]*',
                                  a.pipeline_status_path).group(0)
        result.update(_GetTimesFromPipeline(pipeline_root))
      r.put((a.key, result))
    except Exception, e:
      print 'Problem with ', a.key, e

    q.task_done()


def _GetTimesFromBuildbot(buildbot_url):
  # TODO(robertocn): Use existing code that talks to milo.
  # Convert the buildbot url to request to milo.
  milo_url = ('https://luci-milo.appspot.com/prpc/'
              'milo.Buildbot/GetBuildbotBuildJSON')
  # http://build.chromium.org/p/<master>/builders/<builder>/builds/<number>
  master, _, builder, _, buildnum = str(buildbot_url).split('/')[4:]
  data = json.dumps({
      'master': master,
      'builder': builder,
      'buildNum': buildnum,
      'exclude_deprecated': True,
  })

  _, response = CLIENT_SINGLETON.Post(
      milo_url, data, headers={'Content-Type': 'application/json'})

  response_json = response[response.index('{'):]
  response_data = json.loads(response_json)
  result = {}
  for step in response_data['steps']:
    if 'name' in step and 'times' in step:
      # Times from buildbot seem to be in PST, naively converting to UTC
      start = _UnknownToDatetime(step['times'][0])
      end = _UnknownToDatetime(step['times'][1])
      if start:
        result[step['name'] + '.start'] = start + datetime.timedelta(hours=8)
      if end:
        result[step['name'] + '.end'] = end + datetime.timedelta(hours=8)
  return result


def _GetTimesFromSwarming(url):
  results = {}
  task_id = url.split('/')[-1]
  task_result, _ = swarming_util.GetSwarmingTaskResultById(
      task_id, CLIENT_SINGLETON)
  for k in task_result.keys():
    if k.endswith('_ts'):
      results[k] = _UnknownToDatetime(task_result[k])
  return results


# TODO: Instead of guessing, make the right conversions when retrieving data,
# including timezone adjustment (i.e. make everything UTC)
def _UnknownToDatetime(unknown):
  if isinstance(unknown, datetime.datetime):
    return unknown
  if isinstance(unknown, int) and unknown < 1000000000:  # Timestamp in 2001
    return unknown
  if isinstance(unknown, basestring):
    for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
      try:
        return datetime.datetime.strptime(unknown, fmt)
      except ValueError:
        pass
  # This is only used to guess whether the time given is in seconds or ms
  epoch = time.mktime(datetime.datetime(year=2008, month=9, day=2).timetuple())
  try:
    ts = float(unknown)
    if ts > epoch:
      if ts > epoch * 1000:
        ts = float(ts) / 1000
    return datetime.datetime.fromtimestamp(ts)
  except (TypeError, ValueError):
    # Couldn't cast it, return the original value.
    pass
  return unknown


def _PrependKeys(prefix, d):
  new_d = {}
  for k in d.keys():
    new_d[prefix + k] = d[k]
  return new_d


def _Denest(d):
  """Converts {'a': {'b': 'c'}} into {'a.b': 'c'}."""
  if isinstance(d, dict):
    removals = []
    insertions = {}
    for k, v in d.iteritems():
      if isinstance(v, dict):
        insertions.update(_PrependKeys(k + '.', v))
        removals.append(k)
    for k in removals:
      del (d[k])
    d.update(insertions)
  return d


def main():
  # TODO: add options to limit the date range to fetch
  # TODO: add options to display summary of fetched info.

  remote_api.EnableFinditRemoteApi()

  try:
    all_analyses, stored_start, stored_end, time_records = _LoadAnalyses()
  except IOError:
    all_analyses = _FetchAnalyses(START_DATE, END_DATE)
    time_records = {}
    _SaveAnalyses(all_analyses, time_records)
  else:
    if stored_start > START_DATE:
      all_analyses = _FetchAnalyses(START_DATE, stored_start) + all_analyses
    if END_DATE > stored_end:
      all_analyses += _FetchAnalyses(stored_end, END_DATE)
    _SaveAnalyses(all_analyses, time_records)
  saved_count = len(time_records)

  q = Queue.Queue()
  r = Queue.Queue()

  for _ in range(THREAD_COUNT):
    t = Thread(target=_GetTimes, args=(q, r))
    t.daemon = True
    t.start()

  for a in all_analyses:
    if a.key not in time_records.keys():
      q.put(a)

  while not (q.empty() and r.empty()):
    key, record = r.get()
    time_records[key] = _Denest(record)
    print len(time_records)
    if saved_count + THREAD_COUNT < len(time_records):
      _SaveAnalyses(all_analyses, time_records)
      saved_count = len(time_records)
      print 'Saved %d results' % saved_count
  q.join()


if __name__ == '__main__':
  main()
