#!/usr/bin/env python3

import json
import hashlib
import os
import pprint
import re
import sys
import html
import getopt
import threading
import itertools
import time
import requests
import math
import string
import pygal

from string import Template
from collections import Counter
from urllib.request import urlopen
from urllib import request
from datetime import datetime, timedelta, date

# python -m pip install SomePackage
# python.exe -m pip install --upgrade SomePackage
# python.exe -m pip install --upgrade fx_crash_sig
import fx_crash_sig
from fx_crash_sig.crash_processor import CrashProcessor

# process types
# https://searchfox.org/mozilla-central/source/toolkit/components/crashes/CrashManager.jsm#162

###########################################################
# Usage
###########################################################
# -u (url)      : redash rest endpoint url
# -k (str)      : redash user api key
# -q (query id) : redash api query id
# -c (value)    : redash cache value in minutes (0 is the default)
# -d (name)     : local json cache filename to use (excluding extension)
# -n (name)     : local html output filename to use (excluding extension)
# -c (count)    : number of reports to process, overrides the default
# -p (k=v)      : k=v redash query parameters to pass to the query request.
# -z            : debugging: load and dump the first few records of the local databases. requires -d.
# -s (sig)      : search for a token in reports
# -a (actor)    : IPC actor name to match for ; not passing it will not generate param in query. passing "none" will generate "IS NULL"
# -m            : Maintenance mode
# -l (lower client limit) : set value for ReportLowerClientLimit, filtering out single client crashes (default 2)
# python crashes.py -n nightly -d nightly -u https://sql.telemetry.mozilla.org -k (userapikey) -q 79354 -p process_type=gpu -p version=89 -p channel=nightly

## TODO
## stats statistics when loaded or written
## report struct may not need to os, osver, and arch info anymore since we added stats
## signatures that went away feature
## annotation signature keywords
## click handler should ignore clicks if there's selection in the page
## popup panel layout (Fixed By and Notes) is confusing, and wide when it doesn't need to be.
## Remove reliance on version numbers? Need to get signature headers hooked up, and choose the latest releases for main reports
## build id (nightly / beta)
## linux distro information someplace
## clean up the startup crash icons
## better annotations support
## add dates to annotations

## improve signature header information layout, particular fx version numbers. We can easily expand this down and host info similar to crash stats summary pages.
##  - filter graphing and the list based on clicks on the header data (version, os, arch)

###########################################################
# Globals
###########################################################

# The default symbolication server to use.
SymbolServerUrl = "https://symbolication.services.mozilla.com/symbolicate/v5"
# Max stack depth for symbolication
MaxStackDepth = 50
# Signature list length of the resulting top crashes report
MostCommonLength = 50
# When generating a report, signatures with crash counts
# lower than this value will not be included in the report.
MinCrashCount = 1
# Maximum number of crash reports to include for each signature
# in the final report. Limits the size of the resulting html.
MaxReportCount = 100
# Default redash max_age value in minutes
MaxAge = 43200
# Set to True to target a local json file for testing
LoadLocally = False
LocalJsonFile = "GPU_Raw_Crash_Data_2021_03_19.json"

proc = CrashProcessor(MaxStackDepth, SymbolServerUrl)
pp = pprint.PrettyPrinter(indent=1, width=260)

def symbolicate(ping):
  try:
    return proc.symbolicate(ping)
  except:
    return None

def generateSignature(payload):
  if payload is None:
    return ""
  try:
    return proc.get_signature_from_symbolicated(payload).signature
  except:
    return ""

###########################################################
# Progress indicator
###########################################################

def progress(count, total, status=''):
  bar_len = 60
  filled_len = int(round(bar_len * count / float(total)))

  percents = round(100.0 * count / float(total), 1)
  bar = '=' * filled_len + '-' * (bar_len - filled_len)

  sys.stdout.write('[%s] %s%s ...%s\r' % (bar, percents, '%', status))
  sys.stdout.flush()

class Spinner:
  def __init__(self, message, delay=0.1):
    self.spinner = itertools.cycle(['-', '/', '|', '\\'])
    self.delay = delay
    self.busy = False
    self.spinner_visible = False
    sys.stdout.write(message)

  def write_next(self):
    with self._screen_lock:
      if not self.spinner_visible:
        sys.stdout.write(next(self.spinner))
        self.spinner_visible = True
        sys.stdout.flush()

  def remove_spinner(self, cleanup=False):
    with self._screen_lock:
      if self.spinner_visible:
        sys.stdout.write('\b')
        self.spinner_visible = False
        if cleanup:
          sys.stdout.write(' ')       # overwrite spinner with blank
          sys.stdout.write('\r')      # move to next line
        sys.stdout.flush()

  def spinner_task(self):
    while self.busy:
      self.write_next()
      time.sleep(self.delay)
      self.remove_spinner()

  def __enter__(self):
    if sys.stdout.isatty():
      self._screen_lock = threading.Lock()
      self.busy = True
      self.thread = threading.Thread(target=self.spinner_task)
      self.thread.start()

  def __exit__(self, exception, value, tb):
    if sys.stdout.isatty():
      self.busy = False
      self.remove_spinner(cleanup=True)
    else:
      sys.stdout.write('\r')

def poll_job(s, redash_url, job):
  while job['status'] not in (3,4):
      response = s.get('{}/api/jobs/{}'.format(redash_url, job['id']))
      job = response.json()['job']
      time.sleep(1)

  if job['status'] == 3:
      return job['query_result_id']
    
  return None

###########################################################
# Redash queries
###########################################################

def getRedashQueryResult(redash_url, query_id, api_key, cacheValue, params):
  s = requests.Session()
  s.headers.update({'Authorization': 'Key {}'.format(api_key)})

  # max_age is a redash value that controls cached results. If there is a cached query result
  # newer than this time (in seconds) it will be returned instead of a fresh query.
  # 86400 = 24 hours, 43200 = 12 hours, 0 = refresh query 
  #
  # Note sometimes the redash caching feature gets 'stuck' on an old cache. Side effect is
  # that all reports will eventually be older than 7 days and as such will be filtered out
  # by this script's age checks in processRedashDataset. Crash lists will shrink to zero
  # as a result.
  payload = dict(max_age=cacheValue, parameters=params)

  url = "%s/api/queries/%s/results" % (redash_url, query_id)
  response = s.post(url, data=json.dumps(payload))

  if response.status_code != 200:
    print("\nquery error '%s'" % response)
    pp.pprint(payload)
    raise Exception('Redash query failed.')
  
  #{ 'job': { 'error': '',
  #           'id': '21429857-5fd0-443d-ba4b-fb9cc6d49add',
  #           'query_result_id': None,
  #           'result': None,
  #           'status': 1,
  #           'updated_at': 0}}
  # ...or, we just get back the result

  try:
    result = response.json()['job']
  except KeyError:
    return response.json()

  result_id = poll_job(s, redash_url, response.json()['job'])

  response = s.get('{}/api/queries/{}/results/{}.json'.format(redash_url, query_id, result_id))

  if response.status_code != 200:
      raise Exception('Failed getting results. (Check your redash query for errors.) statuscode=%d' % response.status_code)

  return response.json()

###########################################################
# HTML and Text Formatting Utilities
###########################################################

def escapeBugLinks(text):
  # convert bug references to links
  # https://bugzilla.mozilla.org/show_bug.cgi?id=1323439
  pattern = "bug ([0-9]*)"
  replacement = "<a href='https://bugzilla.mozilla.org/show_bug.cgi?id=\\1'>Bug \\1</a>"
  result = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
  return result

def createBugLink(id):
  # convert bug references to links
  return "<a href='https://bugzilla.mozilla.org/show_bug.cgi?id=" + str(id) + "'>bug " + str(id) + "</a>"

safe = string.ascii_letters + string.digits + '_-.'

def stripWhitespace(text):
  text = text.strip(' \t\n')
  return text

def stringToHtmlId(s):
    s = ''.join([letter for letter in s if letter in safe])
    return s

def generateSourceLink(frame):
  # examples:
  # https://hg.mozilla.org/mozilla-central/file/2da6d806f45732e169fd8e7ea9a9761fa7fed93d/netwerk/protocol/http/OpaqueResponseUtils.cpp#l208
  # https://crash-stats.mozilla.org/sources/highlight/?url=https://gecko-generated-sources.s3.amazonaws.com/7d3f7c890af...e97be06f948921153/ipc/ipdl/PCompositorManagerParent.cpp&line=200#L-200
  # 'file': 's3:gecko-generated-sources:8276fd848664bea270...8e363bdbc972cdb7eb661c4043de93ce27810b54/ipc/ipdl/PWebGLParent.cpp:',
  # 'file': 'hg:hg.mozilla.org/mozilla-central:dom/canvas/WebGLParent.cpp:52d2c9e672d0a0c50af4d6c93cc0239b9e751d18',
  # 'line': 59,
  srcLineNumer = str()
  srcfileData = str()
  srcUrl = str()
  try:
    srcLineNumber = frame['line']
    srcfileData = frame['file']
    tokenList = srcfileData.split(':')
    if (len(tokenList) != 4):
      print("bad token list " + tokenList)
      return str()
  except:
    return str()

  if tokenList[0].find('s3') == 0:
    srcUrl = 'https://crash-stats.mozilla.org/sources/highlight/?url=https://gecko-generated-sources.s3.amazonaws.com/'
    srcUrl += tokenList[2]
    srcUrl += '&line='
    srcUrl += str(srcLineNumber)
    srcUrl += '#L-'
    srcUrl += str(srcLineNumber)
  elif tokenList[0].find('hg') == 0:
    srcUrl = 'https://'
    srcUrl += tokenList[1]
    srcUrl += '/file/'
    srcUrl += tokenList[3]
    srcUrl += '/'
    srcUrl += tokenList[2]
    srcUrl += '#l' + str(srcLineNumber)
  else:
    #print("Unknown src annoutation source") this happens a lot
    return str()

  return srcUrl

def escape(text):
  return html.escape(text)

###########################################################
# Crash Report Utilities
###########################################################

def processStack(frames):
  # Normalized function names we can consider the same in calculating
  # unique reports. We replace the regex match with the key using sub.
  coelesceFrameDict = {
    'RtlUserThreadStart': '[_]+RtlUserThreadStart'
    }

  # Functions we can replace with the normalized version, filters
  # out odd platform parameter differences.
  coelesceFunctionList = [
    'thread_start<'
    ]

  dataStack = list() # [idx] = { 'frame': '(frame)', 'srcUrl': '(url)' }

  for frame in frames:
    frameIndex = '?'
    try:
      frameIndex = frame['frame'] # zero based frame index
    except KeyError:
      continue
    except TypeError:
      #print("TypeError while indexing frame.");
      continue

    dataStack.insert(frameIndex, { 'index': frameIndex, 'frame': '', 'srcUrl': '', 'module': '' })

    functionCall = ''
    module = 'unknown'
    offset = 'unknown'

    try:
      offset = frame['module_offset']
    except:
      pass
    try:
      module = frame['module']
    except:
      pass


    try:
      functionCall = frame['function']
    except KeyError:
      dataStack[frameIndex]['frame'] = offset
      dataStack[frameIndex]['module'] = module
      continue
    except TypeError:
      print("TypeError while indexing function.");
      dataStack[frameIndex]['frame'] = "(missing function)"
      continue

    for k, v in coelesceFrameDict.items():
      functionCall = re.sub(v, k, functionCall, 1)
      break

    for v in coelesceFunctionList:
      if re.search(v, functionCall) != None:
        normalizedFunction = functionCall
        try:
          normalizedFunction = frame['normalized']
        except KeyError:
          pass
        except TypeError:
          pass
        functionCall = normalizedFunction
        break

    srcUrl = generateSourceLink(frame)

    dataStack[frameIndex]['srcUrl'] = srcUrl
    dataStack[frameIndex]['frame'] = functionCall
    dataStack[frameIndex]['module'] = module

  return dataStack

def generateSignatureHash(signature, os, osVer, arch, fxVer):
  hashData = signature

  # Append any crash meta data to our hashData so it applies to uniqueness.
  # Any variance in this data will cause this signature to be broken out as
  # a separate signature in the final top crash list.
  #hashData += os
  #hashData += osVer
  #hashData += arch

  # The redash queries we are currently using target specific versions, so this
  # doesn't have much of an impact except on beta, where we want to see the effect
  # of beta fixes that get uplifted.
  #hashData += fxVer

  return hashlib.md5(hashData.encode('utf-8')).hexdigest()

###########################################################
# Reports data structure utilities
###########################################################

def getDatasetStats(reports):
  sigCount = len(reports)
  reportCount = 0
  for hash in reports:
    reportCount += len(reports[hash]['reportList'])
  return sigCount, reportCount

def processRedashDataset(dbFilename, jsonUrl, queryId, userKey, cacheValue, parameters, crashProcessMax):
  props = list()
  reports = dict()

  totals = {
    'processed': 0,
    'skippedBadSig': 0,
    'alreadyProcessed': 0,
    'outdated': 0
  }
  
  # load up our database of processed crash ids
  # returns an empty dict() if no data is loaded.
  reports, stats = loadReports(dbFilename)

  if LoadLocally:
    with open(LocalJsonFile) as f:
      dataset = json.load(f)
  else:
    with Spinner("loading from redash..."):
      dataset = getRedashQueryResult(jsonUrl, queryId, userKey, cacheValue, parameters)
    print("   done.")

  crashesToProcess = len(dataset["query_result"]["data"]["rows"])
  if  crashesToProcess > crashProcessMax:
    crashesToProcess = crashProcessMax

  print('%04d total reports loaded.' % crashesToProcess)

  for recrow in dataset["query_result"]["data"]["rows"]:
    if totals['processed'] >= crashProcessMax:
      break

    # pull some redash props out of the recrow. You can add these
    # by modifying the sql query.
    operatingSystem = recrow['normalized_os']
    operatingSystemVer = recrow['normalized_os_version']
    firefoxVer = recrow['display_version']
    buildId = recrow['build_id']
    compositor = recrow['compositor']
    arch = recrow['arch']
    oomSize = recrow['oom_size']
    devVendor = recrow['vendor']
    devGen = recrow['gen']
    devChipset = recrow['chipset']
    devDevice = recrow['device']
    drvVer = recrow['driver_version']
    drvDate = recrow['driver_date']
    clientId = recrow['client_id']
    devDesc = recrow['device_description']

    # Load the json crash payload from recrow
    props = json.loads(recrow["payload"])

    # touch up for the crash symbolication package
    props['stackTraces'] = props['stack_traces']

    crashId = props['crash_id']
    crashDate = props['crash_date']
    minidumpHash = props['minidump_sha256_hash']
    crashReason = props['metadata']['moz_crash_reason']
    crashInfo = props['stack_traces']['crash_info']

    startupCrash = False
    if recrow['startup_crash']:
      startupCrash = int(recrow['startup_crash'])

    fissionEnabled = False
    if recrow['fission_enabled']:
      fissionEnabled = int(recrow['fission_enabled'])

    lockdownEnabled = False
    if recrow['lockdown_enabled']:
      lockdownVal = int(recrow['lockdown_enabled'])
      if lockdownVal == 1:
        lockdownEnabled = True

    if crashReason != None:
      crashReason = crashReason.strip('\n')

    # Ignore crashes older than 7 days
    if not checkCrashAge(crashDate):
      totals['processed'] += 1
      totals['outdated'] += 1
      progress(totals['processed'], crashesToProcess)
      continue

    # check if the crash id is processed, if so continue
    ## note, this search has become quite slow. optimize me.
    found = False
    signature = ""
    for sighash in reports: # reports is a dictionary of signature hashes
      for report in reports[sighash]['reportList']: # reportList is a list of dictionaries 
        if report['crashid'] == crashId: # string compare, slow
          found = True
          # if you add a new value to the sql queries, you can update
          # the local json cache we have in memory here. Saves having
          # to delete the file and symbolicate everything again.
          #report['fission'] = fissionEnabled
          #report['lockdown'] = lockdownEnabled
          break

    if found:
      totals['processed'] += 1
      totals['alreadyProcessed'] += 1
      progress(totals['processed'], crashesToProcess)
      continue
  
    # symbolicate and return payload result
    payload = symbolicate({ "normalized_os": operatingSystem, "payload": props })
    signature = generateSignature(payload)

    if skipProcessSignature(signature):
      totals['processed'] += 1
      totals['skippedBadSig'] += 1
      progress(totals['processed'], crashesToProcess)
      continue

    # pull stack information for the crashing thread
    try:
      crashingThreadIndex = payload['crashing_thread']
    except KeyError:
      #print("KeyError on crashing_thread for report");
      continue

    threads = payload['threads']
    
    try:
      frames = threads[crashingThreadIndex]['frames']
    except IndexError:
      print("IndexError while indexing crashing thread");
      continue
    except TypeError:
      print("TypeError while indexing crashing thread");
      continue

    # build up a pretty stack
    stack = processStack(frames)

    # generate a tracking hash 
    hash = generateSignatureHash(signature, operatingSystem, operatingSystemVer, arch, firefoxVer)

    if hash not in reports.keys():
      # Set up this signature's meta data we track in the signature header.
      reports[hash] = {
        'signature':          signature,
        'operatingsystem':    [operatingSystem],
        'osversion':          [operatingSystemVer],
        'firefoxver':         [firefoxVer],
        'arch':               [arch],
        'reportList':         list()
      }

    # Update meta data we track in the report header.
    if operatingSystem not in reports[hash]['operatingsystem']:
      reports[hash]['operatingsystem'].append(operatingSystem)
    if operatingSystemVer not in reports[hash]['osversion']:
      reports[hash]['osversion'].append(operatingSystemVer)
    if firefoxVer not in reports[hash]['firefoxver']:
      reports[hash]['firefoxver'].append(firefoxVer)
    if arch not in reports[hash]['arch']:
      reports[hash]['arch'].append(arch)

    # create our report with per crash meta data
    report = {
      'clientid':           clientId,
      'crashid':            crashId,
      'crashdate':          crashDate,
      'compositor':         compositor,
      'stack':              stack,
      'oomsize':            oomSize,
      'type':               crashInfo['type'],
      'devvendor':          devVendor,
      'devgen':             devGen,
      'devchipset':         devChipset,
      'devdevice':          devDevice,
      'devdescription':     devDesc,
      'driverversion' :     drvVer,
      'driverdate':         drvDate,
      'minidumphash':       minidumpHash,
      'crashreason':        crashReason,
      'startup':            startupCrash,
      'fission':            fissionEnabled,
      'lockdown':           lockdownEnabled,
      # Duplicated but useful if we decide to change the hashing algo
      # and need to reprocess reports.
      'operatingsystem':    operatingSystem,
      'osversion':          operatingSystemVer,
      'firefoxver':         firefoxVer,
      'arch':               arch
    }

    # save this crash in our report list
    reports[hash]['reportList'].append(report)
   
    if hash not in stats.keys():
      stats[hash] = {
        'signature': signature,
        'crashdata': {}
      }

    # check to see if stats has a date entry that matches crashDate
    if crashDate not in stats[hash]['crashdata']:
      stats[hash]['crashdata'][crashDate] = { 'crashids': [], 'clientids':[] }

    if operatingSystem not in stats[hash]['crashdata'][crashDate]:
      stats[hash]['crashdata'][crashDate][operatingSystem] = {}

    if operatingSystemVer not in stats[hash]['crashdata'][crashDate][operatingSystem]:
      stats[hash]['crashdata'][crashDate][operatingSystem][operatingSystemVer] = {}

    if arch not in stats[hash]['crashdata'][crashDate][operatingSystem][operatingSystemVer]:
      stats[hash]['crashdata'][crashDate][operatingSystem][operatingSystemVer][arch] = {}

    if firefoxVer not in stats[hash]['crashdata'][crashDate][operatingSystem][operatingSystemVer][arch]:
      stats[hash]['crashdata'][crashDate][operatingSystem][operatingSystemVer][arch][firefoxVer] = { 'clientcount': 0, 'crashcount': 0 }

    if crashId not in stats[hash]['crashdata'][crashDate]['crashids']:
      stats[hash]['crashdata'][crashDate]['crashids'].append(crashId)
      stats[hash]['crashdata'][crashDate][operatingSystem][operatingSystemVer][arch][firefoxVer]['crashcount'] += 1
      if clientId not in stats[hash]['crashdata'][crashDate]['clientids']:
        stats[hash]['crashdata'][crashDate][operatingSystem][operatingSystemVer][arch][firefoxVer]['clientcount'] += 1
        stats[hash]['crashdata'][crashDate]['clientids'].append(clientId)

    totals['processed'] += 1

    progress(totals['processed'], crashesToProcess)

  print('\n')
  print('%04d - reports processed' % totals['processed'])
  print('%04d - cached results' % totals['alreadyProcessed'])
  print('%04d - reports skipped, bad signature' % totals['skippedBadSig'])
  print('%04d - reports skipped, out dated' % totals['outdated'])

  # Post processing steps

  # Purge signatures from our reports list that are outdated (based
  # on crash date and version). This keeps our crash lists current,
  # especially after a merge. Note this doesn't clear stats, just reports.
  queryFxVersion = parameters['version']
  purgeOldReports(reports, queryFxVersion)

  # purge old crash and client ids from the stats database.
  cleanupStats(reports, stats)

  # calculate unique client id counts for each signature. These are client counts
  # associated with the current redash query, and apply only to a seven day time
  # window. They are stored in the reports database and displayed in the top crash
  # reports. 
  clientCounts = dict()
  needsUpdate = False
  for hash in reports:
    clientCounts[hash] = list()
    for report in reports[hash]['reportList']:
      clientId = report['clientid']
      if clientId not in clientCounts[hash]:
        clientCounts[hash].append(clientId)
    reports[hash]['clientcount'] = len(clientCounts[hash])

  return reports, stats, totals['processed']

def checkCrashAge(dateStr):
  try:
    date = datetime.fromisoformat(dateStr)
  except:
    return False
  oldestDate = datetime.today() - timedelta(days=7)
  return (date >= oldestDate)

def getMainVer(version):
  return version.split('.')[0]

def purgeOldReports(reports, fxVersion):
  # Purge obsolete reports.
  # 89.0b7 89.0 90.0.1
  totalReportsDropped = 0
  for hash in reports:
    keepRepList = list()
    origRepLen = len(reports[hash]['reportList'])
    for report in reports[hash]['reportList']:
      reportVer = ''
      try:
        reportVer = getMainVer(report['firefoxver'])
      except:
        pass
      if fxVersion == reportVer:
        keepRepList.append(report)
    totalReportsDropped += (origRepLen - len(keepRepList))
    reports[hash]['reportList'] = keepRepList

  print("Removed %d older reports." % totalReportsDropped)

  # Purge signatures that have no reports
  delSigList = list()
  for hash in reports:
    newRepList = list()
    for report in reports[hash]['reportList']:
      # "crash_date":"2021-03-22"
      dateStr = report['crashdate']
      if checkCrashAge(dateStr):
        newRepList.append(report)
    reports[hash]['reportList'] = newRepList
    if len(newRepList) == 0:
      # add this signature to our purge list
      delSigList.append(hash)

  for hash in reports:
    if len(reports[hash]['reportList']) == 0:
      if hash not in delSigList:
        delSigList.append(hash)

  # purge old signatures that no longer have reports
  # associated with them.
  for hash in delSigList:
    del reports[hash]

  print("Removed %d older signatures from our reports database." % len(delSigList))

def cleanupStats(reports, stats):
  # remove old crash and client ids we no longer have reports for
  clientList = list()
  crashList = list()
  for hash in reports:
    for report in reports[hash]['reportList']:
      clientid = report['clientid']
      crashid = report['crashid']
      if clientid not in clientList:
        clientList.append(clientid)
      if crashid not in crashList:
        crashList.append(crashid)

  purgeClientIdList = list()
  purgeCrashIdList = list()

  for hash in stats:
    for date in stats[hash]['crashdata'].keys():
      for crashid in stats[hash]['crashdata'][date]['crashids']:
        if crashid not in crashList:
          if crashid not in purgeCrashIdList:
            purgeCrashIdList.append(crashid)
      for clientid in stats[hash]['crashdata'][date]['clientids']:
        if clientid not in clientList:
          if clientid not in purgeClientIdList:
            purgeClientIdList.append(clientid)

  for crashid in purgeCrashIdList:
    for hash in stats:
      for date in stats[hash]['crashdata'].keys():
          if crashid in stats[hash]['crashdata'][date]['crashids']:
            stats[hash]['crashdata'][date]['crashids'].remove(crashid)


  for clientid in purgeClientIdList:
    for hash in stats:
      for date in stats[hash]['crashdata'].keys():
          if clientid in stats[hash]['crashdata'][date]['clientids']:
            stats[hash]['crashdata'][date]['clientids'].remove(clientid)
  
  print("Removed %d old client ids and %d old crash ids tracked in stats." % (len(purgeClientIdList), len(purgeCrashIdList)))

  return True

# return true if we should skip processing this signature
def skipProcessSignature(signature):
  if len(signature) == 0:
    return True
  elif signature == 'EMPTY: no crashing thread identified':
    return True
  elif signature == 'EMPTY: no frame data available':
    return True
  elif signature == "<T>":
    print("sig <T>")
    return True

  return False

def isFissionRelated(reports):
  isFission = True
  for report in reports:
    try:
      if report['fission'] == 0:
        isFission = False
    except:
      pass
  return isFission

def isLockdownRelated(reports):
  isLockdown = True
  for report in reports:
    try:
      if report['lockdown'] == 0:
        isLockdown = False
    except:
      pass
  return isLockdown

def generateTopReportsList(reports):
  # For certain types of reasons like RustMozCrash, organize
  # the most common for a report list. Otherwise just dump the
  # first MaxReportCount.
  reasonCounter = Counter()
  for report in reports:
    crashReason = report['crashreason']
    reasonCounter[crashReason] += 1
  reportCol = reasonCounter.most_common(MaxReportCount)
  if len(reportCol) < MaxReportCount:
    return reports
  colCount = len(reportCol)
  maxReasonCount = int(math.ceil(MaxReportCount / colCount))
  reportList = list()
  count = 0
  for reason, count in reportCol:
    for report in reports:
      if report['crashreason'] == reason:
         reportList.append(report)
         count += 1
         if count > maxReasonCount:
           break # next reason
  return reportList

def dumpDatabase(reports, annoFilename):
  print("= Reports =======================================================================================")
  pp.pprint(reports)
  print("= Annotations ===================================================================================")
  reports = loadAnnotations(annoFilename)
  pp.pprint(reports)

def doMaintenance(dbFilename):
  exit()
  # load up our database of processed crash ids
  reports, stats = loadReports(dbFilename)

  for hash in reports:
    signature = reports[hash]['signature']
    clientcount = reports[hash]['clientcount']

    operatingSystem = reports[hash]['operatingsystem']
    del reports[hash]['operatingsystem']
    reports[hash]['operatingsystem'] = [operatingSystem]

    operatingSystemVer = reports[hash]['osversion']
    del reports[hash]['osversion']
    reports[hash]['osversion'] = [operatingSystemVer]

    firefoxVer = reports[hash]['firefoxver']
    del reports[hash]['firefoxver']
    reports[hash]['firefoxver'] = [firefoxVer]

    arch = reports[hash]['arch']
    del reports[hash]['arch']
    reports[hash]['arch'] = [arch]

  #dumpDatabase(reports)

  # Caching of reports
  #cacheReports(reports, stats, dbFilename)

###########################################################
# File utilities
###########################################################

# Load the local report database
def loadReports(dbFilename):
  reportsFile = ("%s-reports.json" % dbFilename)
  statsFile = ("%s-stats.json" % dbFilename)
  reports = dict()
  stats = dict()
  try:
    with open(reportsFile) as database:
      reports = json.load(database)
  except FileNotFoundError:
    pass
  try:
    with open(statsFile) as database:
      stats = json.load(database)
  except FileNotFoundError:
    pass
  sigCount, reportCount = getDatasetStats(reports)
  print("Existing database stats: %d signatures, %d reports." % (sigCount, reportCount))
  return reports, stats

# Cache the reports database to a local json file. Speeds
# up symbolication runs across days by avoid re-symbolicating
# reports.
def cacheReports(reports, stats, dbFilename):
  reportsFile = ("%s-reports.json" % dbFilename)
  statsFile = ("%s-stats.json" % dbFilename)
  with open(reportsFile, "w") as database:
      database.write(json.dumps(reports))
  with open(statsFile, "w") as database:
      database.write(json.dumps(stats))
  sigCount, reportCount = getDatasetStats(reports)
  print("Cache database stats: %d signatures, %d reports." % (sigCount, reportCount))

def loadAnnotations(filename):
  file = "%s.json" % filename
  try:
    with open(file) as database:
      annotations = json.load(database)
      print("Loading %s annotations file." % file)
  except FileNotFoundError:
    print("Could not find %s file." % file)
    return dict()
  except json.decoder.JSONDecodeError:
    print("Json error parsing %s" % file)
    return dict()
  return annotations

###########################################################
# HTML Template Utilities
###########################################################

def extractTemplate(token, srcTemplate):
  # This returns the inner template from srcTemplate, minus any
  # identifying tag data.

  # token would be something like 'signature' used
  # in identifying tags like:
  # <!-- start of signature template -->
  # <!-- end of signature template -->
  start = '<!-- start of ' + token + ' template -->'
  end = '<!-- end of ' + token + ' template -->'
  sIndex = srcTemplate.index(start)
  eIndex = srcTemplate.index(end)
  if sIndex == -1 or eIndex == -1:
    raise Exception("Bad HTML template tokens!") 
  template = srcTemplate[sIndex + len(start) : eIndex + len(end)]
  return template

def extractAndTokenizeTemplate(token, srcTemplate, insertToken):
  # This returns the inner template from srcTemplate, minus any
  # identifying tag data, and we also return srcTemplate with
  # $insertToken replacing the block we clipped out.

  start = '<!-- start of ' + token + ' template -->'
  end = '<!-- end of ' + token + ' template -->'
  sIndex = srcTemplate.index(start)
  eIndex = srcTemplate.index(end)
  if sIndex == -1 or eIndex == -1:
    raise Exception("Bad HTML template tokens!") 
  header = srcTemplate[0:sIndex]
  footer = srcTemplate[eIndex + len(end):]
  template = srcTemplate[sIndex + len(start) : eIndex]
  return template, (header + '$' + insertToken + footer)

def dumpTemplates():
  print('mainPage -----')
  print(mainPage)
  print('outerSigTemplate-----')
  print(outerSigTemplate)
  print('outerSigMetaTemplate-----')
  print(outerSigMetaTemplate)
  print('outerReportTemplate-----')
  print(outerReportTemplate)
  print('outerStackTemplate-----')
  print(outerStackTemplate)
  print('innerStackTemplate-----')
  print(innerStackTemplate)
  exit()

###########################################################
### Report generation
###########################################################

def generateSignatureReport(signature):
  reports, stats = loadReports()
  reports = reports[sig]
  if len(reports) == 0:
    print("signature not found in database.")
    exit()

  #for report in reports:
  exit()
    
def generateSparklineJS(sigStats, operatingSystems, operatingSystemVers, firefoxVers, archs, className):
  # generate stats data for crash rate over time graphs
  #   data = [ {name: "Bitcoin", date: "2017-01-01", value: 967.6}, ]
  #"Windows": {
  #  "6.1": {
  #    "x86": {
  #      "91.0a1": {
  #        "clientcount": 1,
  #        "crashcount": 3
  #      }
  #    }
  #  }
  #}

  rawData = dict()
  for dateStr in sigStats['crashdata']:
    for os in operatingSystems:
      for osver in operatingSystemVers:
        for arch in archs:
          for fxver in firefoxVers:
            try:
              stats = sigStats['crashdata'][dateStr][os][osver][arch][fxver]
              rawData[dateStr] = { 'os': os, 'crashcount': stats['crashcount'] }
            except:
              pass # some dates may not apply to a particular combination

  # average data for each os to smooth out the graph
  # {name: "Windows", date: "2021-06-24", value: 84}

  # generate a list of dates
  avgData = dict()
  dates = list(rawData.keys())
  dates.sort()

  # generate an os list [not used]
  osList = list()
  for targetDate in dates:
    os = rawData[targetDate]['os']
    if os not in osList:
      osList.append(os)

  # generate plot data
  plotData = '['
  template = '{name: "$name", date: "$date", value: $value},'

  for targetDate in dates:
    pd = date.fromisoformat(targetDate)
    minDate = pd - timedelta(3)
    maxDate = pd + timedelta(4)
    crashCount = 0
    dataPoints = 0

    for tmpDateStr in dates:
      tmpDate = date.fromisoformat(tmpDateStr)
      if tmpDate >= minDate and tmpDate <= maxDate:
        crashCount += rawData[pd.isoformat()]['crashcount']
        dataPoints += 1

    if dataPoints == 0:
      avgData[targetDate] = 0
    else:
      avgData[targetDate] = crashCount / dataPoints

    #print("date:%s cc=%d dp=%d avg=%f" % (targetDate, crashCount, dataPoints, avgData[targetDate]))
    plotData += Template(template).substitute(name='All', date=targetDate, value=avgData[targetDate])

  plotData += ']'
  #print(plotData)
  template = 'sparkline(document.querySelector("$cname"), $data, sloptions);' ## sloptions defined in template.html
  return Template(template).substitute(data=plotData, cname='.' + className)

# from list of strings, return a comma separated pretty list
def getItemizedHeaderList(theList):
  result = ''
  sl = theList.sort()
  for s in theList:
    result += s + ', '
  return result.strip(' ,')

# currently not in use
def versionListIsExclusiveTo(version, vList):
  # 92.0b6
  # 92.0.1
  # 92.0
  # 94.0a1
  found = False
  for v in vList:
    majorVersionNumber = v.split('.')[0]
    if version == majorVersionNumber:
      found = True

  for v in vList:
    majorVersionNumber = v.split('.')[0]
    if version != majorVersionNumber:
      found = False
  return found

def getFxVersionsFromStatsRec(statsCrashData):
  result = list()
  for date in statsCrashData.values():
    for opsys in date.values():
      if (isinstance(opsys, dict)):
        for osver in opsys.values():
          for arch in osver.values():
            for ver in arch.keys():
              if ver not in result:
                result.append(ver)
  result.sort()
  return result

def getPlatformDataFromStatsRec(statsCrashData):
  osresult = list()
  verresult = list()
  archresult = list()
  for date in statsCrashData.values():
    # accumulate operating system type
    for opsys in date.keys():
      if opsys in ['Windows', 'Linux', 'Mac']: # filter out lists clientids and crashids
        if opsys not in osresult:
          osresult.append(opsys)
        osdict = date[opsys]
        # accumulate os version values
        for osver in osdict.keys():
          if osver not in verresult:
            verresult.append(osver)
          # accumulate arch values
          osverdict = osdict[osver]
          for arch in osverdict.keys():
            if arch not in archresult:
              archresult.append(arch)

  osresult.sort()
  verresult.sort()
  archresult.sort()
  return osresult, verresult, archresult

def getSimpVerList(verList):
  result = list()
  for ver in verList:
    simp = ver.split('.', 1)[0]
    if simp not in result:
      result.append(simp)
  return result

def testIfNewCrash(statsCrashData, version):
  verList = getFxVersionsFromStatsRec(statsCrashData)
  simpList = getSimpVerList(verList)
  if version in simpList and len(simpList) == 1:
    return True
  return False

def prettyBetaVersions(verList):
  verList.sort()
  betaDict = dict()

  for s in verList:
    mver = s.split('.')[0]
    if mver not in betaDict.keys():
      betaDict[mver] = list()

    try:
      bver = s.split('b',1)[1]
    except:
      bver = 'rc' # RC's '94.0'

    betaDict[mver].append(bver)

  result = ''
  for ver in betaDict.keys():
    betaDict[ver].sort()
    result += ver + ' ['
    for beta in betaDict[ver]:
      result += beta + ','
    result = result.strip(',')
    result += '] '
  return result

def getCommaDelimitedList(theList):
  result = ''
  theList.sort()
  for s in theList:
    result += s + ', '
  return result.strip(' ,')

def getPrettyPlatformLists(statsCrashData):
  opsys, ver, arch = getPlatformDataFromStatsRec(statsCrashData)
  return getCommaDelimitedList(opsys), getCommaDelimitedList(ver), getCommaDelimitedList(arch)

def getPrettyFirefoxVersionList(statsCrashData, channel):
  verList = getFxVersionsFromStatsRec(statsCrashData)
  result = ''
  if channel == 'nightly':
    verList.sort()
    for s in verList:
      result += s.split('.')[0] + ', '
  elif channel == 'beta':
    result = prettyBetaVersions(verList)
  else:
    verList.sort()
    for s in verList:
      result += s + ', '

  return result.strip(' ,')

def generateTopCrashReport(reports, stats, totalCrashesProcessed, parameters, ipcActorName, outputFilename, annoFilename, reportLowerClientLimit):
  processType = parameters['process_type']
  channel = parameters['channel']
  queryFxVersion = parameters['version']

  templateFile = open("template.html", "r")
  template = templateFile.read()
  templateFile.close()

  # <!-- start of crash template -->
  # <!-- end of crash template -->
  innerTemplate, mainPage = extractAndTokenizeTemplate('crash', template, 'main')
  annotationTemplate, mainPage = extractAndTokenizeTemplate('annotation', mainPage, 'annotations')
  annotationReport, annotationTemplate = extractAndTokenizeTemplate('annotation report', annotationTemplate, 'annreports')

  # <!-- start of signature template -->
  # <!-- end of signature template -->
  innerSigTemplate, outerSigTemplate = extractAndTokenizeTemplate('signature', innerTemplate, 'signature')

  # Main inner block
  # <!-- start of signature meta template -->
  # <!-- end of signature meta template -->
  innerSigMetaTemplate, outerSigMetaTemplate = extractAndTokenizeTemplate('signature meta', innerSigTemplate, 'reports')

  # Report meta plus stack info
  # <!-- start of report template -->
  # <!-- end of report template -->
  innerReportTemplate, outerReportTemplate = extractAndTokenizeTemplate('report', innerSigMetaTemplate, 'report')

  # <!-- start of stackline template -->
  # <!-- end of stackline template -->
  innerStackTemplate, outerStackTemplate = extractAndTokenizeTemplate('stackline', innerReportTemplate, 'stackline')

  outerStackTemplate = stripWhitespace(outerStackTemplate)
  innerStackTemplate = stripWhitespace(innerStackTemplate)
  outerReportTemplate = stripWhitespace(outerReportTemplate)
  outerSigMetaTemplate = stripWhitespace(outerSigMetaTemplate)
  outerSigTemplate = stripWhitespace(outerSigTemplate)
  annotationTemplate = stripWhitespace(annotationTemplate)
  annotationReport = stripWhitespace(annotationReport)
  # mainPage = stripWhitespace(mainPage) # mucks with js
  annDb = loadAnnotations(annoFilename)

  #resultFile = open(("%s.html" % outputFilename), "w", encoding="utf-8")
  resultFile = open(("%s.html" % outputFilename), "w", errors="replace")

  signatureHtml = str()
  sigMetaHtml = str()
  annotationsHtml = str()
  signatureIndex = 0

  sigCount, reportCount = getDatasetStats(reports)

  # generate a top crash list
  sigCounter = Counter()
  for hash in reports:
    if reports[hash]['clientcount'] < reportLowerClientLimit:
      continue
    sigCounter[hash] = len(reports[hash]['reportList'])

  collection = sigCounter.most_common(MostCommonLength)
  
  sparklineJS = ''

  for hash, crashcount in collection:
    try:
      sigRecord = reports[hash] # reports data vs. stats
    except KeyError:
      continue

    signature = sigRecord['signature']
    statsCrashData = stats[hash]['crashdata']

    prettyOperatingSystems, prettyOperatingSystemVers, prettyArchs = getPrettyPlatformLists(statsCrashData)
    prettyFirefoxVers = getPrettyFirefoxVersionList(statsCrashData, channel)

    operatingSystemsList, operatingSystemVersList, archsList = getPlatformDataFromStatsRec(statsCrashData)
    firefoxVersList = getFxVersionsFromStatsRec(statsCrashData)

    crashcount = len(sigRecord['reportList'])
    percent = (crashcount / reportCount)*100.0

    if crashcount < MinCrashCount: # Skip small crash count reports
      continue

    isNewCrash = False
    newIcon = 'noicon'
    if testIfNewCrash(statsCrashData, queryFxVersion):
      isNewCrash = True
      newIcon = 'icon'

    signatureIndex += 1

    crashStatsHashQuery = 'https://crash-stats.mozilla.org/search/?'
    crashStatsQuery = 'https://crash-stats.mozilla.org/search/?signature=~%s&product=Firefox&_facets=signature&process_type=%s' % (signature, processType)

    # sort reports in this signature based on common crash reasons, so the most common
    # is at the top of the list.
    reportsToReport = generateTopReportsList(reports[hash]['reportList'])

    #fissionIcon = 'noicon'
    #if isFissionRelated(reports[hash]['reportList']):
    #  fissionIcon = 'icon'
    #if crashcount < 10 and fissionIcon == 'icon':
    #  fissionIcon = 'grayicon'

    lockdownIcon = 'noicon'
    if isLockdownRelated(reports[hash]['reportList']):
      lockdownIcon = 'icon'

    reportHtml = str()
    idx = 0
    hashTotal= 0
    oomIcon = 'noicon'
    for report in reportsToReport:
      idx = idx + 1
      if idx > MaxReportCount:
        break
      oombytes = report['oomsize']


      if report['oomsize'] is not None:
        oomIcon = 'icon'
      else:
        oombytes = ''

      crashReason = report['crashreason']
      if (crashReason == None):
        crashReason = ''

      crashType = report['type']
      crashType = crashType.replace('EXCEPTION_', '')

      appendAmp = False
      if hashTotal < 30: # This is all crash stats can hande (414 Request-URI Too Large)
        try:
          crashStatsHashQuery += 'minidump_sha256_hash=~' + report['minidumphash']
          hashTotal += 1
          appendAmp = True
        except:
          pass

      # Redash meta data dump for a particular crash id
      infoLink = "https://sql.telemetry.mozilla.org/queries/{query_id}?p_channel={channel}&p_process_type={process_type}&p_version={version}&p_crash_id={crash_id}".format(query_id=79462, channel=channel, process_type=processType, version=queryFxVersion, crash_id=report['crashid'])

      startupStyle = 'noicon'
      if report['startup'] != 0:
        startupStyle = 'icon'

      stackHtml = str()
      for frameData in report['stack']:
        # [idx] = { 'index': n, 'frame': '(frame)', 'srcUrl': '(url)', 'module': '(module)' }
        frameIndex = frameData['index']
        frame = frameData['frame']
        srcUrl = frameData['srcUrl']
        moduleName = frameData['module']

        linkStyle = 'inline-block'
        srcLink = srcUrl
        if len(srcUrl) == 0:
          linkStyle = 'none'
          srcLink = ''

        stackHtml += Template(innerStackTemplate).substitute(frameindex=frameIndex,
                                                              frame=escape(frame),
                                                              srcurl=srcLink,
                                                              module=moduleName,
                                                              style=linkStyle)

      compositor = report['compositor']
      if compositor == 'webrender_software_d3d11':
        compositor = 'd3d11'
      elif compositor == 'webrender':
        compositor = 'webrender'
      elif compositor == 'webrender_software':
        compositor = 'swiggle'
      elif compositor == 'none':
        compositor = ''

      reportHtml += Template(outerStackTemplate).substitute(expandostack=('st'+str(signatureIndex)+'-'+str(idx)),
                                                            rindex=idx,
                                                            type=crashType,
                                                            oomsize=oombytes,
                                                            devvendor=report['devvendor'],
                                                            devgen=report['devgen'],
                                                            devchipset=report['devchipset'],
                                                            description=report['devdescription'],
                                                            drvver=report['driverversion'],
                                                            drvdate=report['driverdate'],
                                                            compositor=compositor,
                                                            reason=crashReason,
                                                            infolink=infoLink,
                                                            startupiconclass=startupStyle,
                                                            stackline=stackHtml)
      if appendAmp:
        crashStatsHashQuery += '&'

    # class="svg-$expandosig"
    sparklineJS += generateSparklineJS(stats[hash], operatingSystemsList, operatingSystemVersList, firefoxVersList, archsList, 'svg-'+stringToHtmlId(hash)) + '\n'

    # svg element
    sigHtml = Template(outerReportTemplate).substitute(expandosig=stringToHtmlId(hash),
                                                       os=prettyOperatingSystems,
                                                       fxver=prettyFirefoxVers,
                                                       osver=prettyOperatingSystemVers,
                                                       arch=prettyArchs,
                                                       report=reportHtml)

    crashStatsHashQuery = crashStatsHashQuery.rstrip('&')

    searchIconClass = 'icon'
    if hashTotal == 0:
      crashStatsHashQuery = ''
      searchIconClass = 'lticon'

    # ann$expandosig - view signature meta parameter
    annIconClass = 'lticon'
    if signature in annDb:
      record = annDb[signature]
      # record['annotations'] { date: 'date', 'annotation': 'notes' }
      sigAnnotations = str()
      # record['fixedby'] (list of tables, { date: 'date', 'version': 87, 'bug': 1234567 }
      for fb in record['fixedby']:
        sigAnnotations += Template(annotationReport).substitute(annotations=escape(fb['annotation']),
                                                                fixedbybug=createBugLink(str(fb['bug'])),
                                                                fixedbyversion=fb['version'])
      for annotation in record['annotations']:
        annotation = escape(annotation['annotation'])
        annotation = escapeBugLinks(annotation)
        sigAnnotations += Template(annotationReport).substitute(annotations=annotation, fixedbybug='', fixedbyversion='')
      annotationsHtml += Template(annotationTemplate).substitute(expandosig=('sig'+str(signatureIndex)),
                                                                 annreports=sigAnnotations)
      annIconClass = 'icon'

    sigMetaHtml += Template(outerSigMetaTemplate).substitute(rank=signatureIndex,
                                                             percent=("%.00f%%" % percent),
                                                             # expandosig=('sig'+str(signatureIndex)),
                                                             expandosig=stringToHtmlId(hash),
                                                             annexpandosig=('sig'+str(signatureIndex)),
                                                             signature=(html.escape(signature)),
                                                             newicon=newIcon,
                                                             fissionicon='noicon',
                                                             lockicon=lockdownIcon,
                                                             oomicon=oomIcon,
                                                             iconclass=searchIconClass,
                                                             anniconclass=annIconClass,
                                                             cslink=crashStatsHashQuery,
                                                             cssearchlink=crashStatsQuery,
                                                             clientcount=sigRecord['clientcount'],
                                                             count=crashcount,
                                                             reports=sigHtml)

  if ipcActorName:
      ipcActorHdr = '<div class="header-elements">IPC Actor - {}</div>'.format(ipcActorName)
  else:
      ipcActorHdr = ""
  signatureHtml += Template(outerSigTemplate).substitute(channel=channel,
                                                         version=queryFxVersion,
                                                         process=processType,
                                                         sigcount=sigCount,
                                                         ipcActorHdr=ipcActorHdr,
                                                         repcount=reportCount,
                                                         sparkline=sparklineJS,
                                                         signature=sigMetaHtml)

  # Add processed date to the footer
  dateTime = datetime.now().isoformat()
  processHead = "{}".format(processType.capitalize())
  if ipcActorName:
      processHead += " ({})".format(ipcActorName)
  resultFile.write(Template(mainPage).substitute(main=signatureHtml,
                                                 annotations=annotationsHtml,
                                                 process=processHead,
                                                 processeddate=dateTime))
  resultFile.close()

###########################################################
# Process crashes and stacks
###########################################################

def main():
  # Maximum number of raw crashes to process. This matches
  # the limit value of re:dash queries. Reduce for testing
  # purposes.
  CrashProcessMax = 7500

  # When generating a report, signatures with client counts
  # lower than this value will not be included in the report.
  ReportLowerClientLimit = 2 # filter out single client crashes

  queryId = ''
  userKey = ''
  targetSignature = ''

  dbFilename = "crashreports" #.json
  annoFilename = "annotations"
  cacheValue = MaxAge
  parameters = dict()
  ipcActor = None

  options, remainder = getopt.getopt(sys.argv[1:], 'c:u:n:d:c:k:q:p:a:s:zml:')
  for o, a in options:
    if o == '-u':
      jsonUrl = a
      print("data source url: %s" %  jsonUrl)
    elif o == '-n':
      outputFilename = a
      print("output filename: %s.html" %  outputFilename)
    elif o == '-c':
      cacheValue = int(a)
    elif o == '-d':
      dbFilename = a
      print("local cache file: %s.json" %  dbFilename)
    elif o == '-c':
      CrashProcessMax = int(a)
    elif o == '-q':
      queryId = a
      print("query id: %s" %  queryId)
    elif o == '-k':
      userKey = a
      print("user key: ({}) [CLI]".format(len(userKey)))
    elif o == '-s':
      targetSignature = a
      print("target signature: %s" %  targetSignature)
    elif o == '-m':
      print("calling maintenance function.")
      doMaintenance(dbFilename)
      exit()
    elif o == '-p':
      param = a.split('=')
      parameters[param[0]] = param[1]
    elif o == '-a':
      ipcActor = a
      print("IPC actor: %s" % ipcActor)
    elif o == '-z':
      reports, stats = loadReports(dbFilename)
      dumpDatabase(reports)
      exit()
    elif o == '-l':
      ReportLowerClientLimit = int(a)
      print("ReportLowerClientLimit: %d" % ReportLowerClientLimit)

  if len(userKey) == 0:
    userKey = os.getenv("REDASH_API_KEY")
    if userKey:
      print("user key: ({}) [ENV]".format(len(userKey)))
    else:
      print("No user key; use -k or REDASH_API_KEY")
      exit()

  if ipcActor is not None:
    if ipcActor == "none":
      parameters["utility_actor_name_op"] = "IS NULL"
    else:
      parameters["utility_actor_name_op"] = 'LIKE "%{}%"'.format(ipcActor)

  if len(userKey) == 0:
    print("missing user api key.")
    exit()
  elif len(queryId) == 0:
    print("missing query id.")
    exit()

  print("redash cache time: %d" %  cacheValue)

  parameters['crashcount'] = str(CrashProcessMax)

  if len(targetSignature) > 0:
    print("analyzing '%s'" % targetSignature)
    generateSignatureReport(targetSignature)
    exit()

  # Pull fresh data from redash and process it
  reports, stats, totalCrashesProcessed = processRedashDataset(dbFilename, jsonUrl, queryId, userKey, cacheValue, parameters, CrashProcessMax)

  # Caching of reports
  cacheReports(reports, stats, dbFilename)

  generateTopCrashReport(reports, stats, totalCrashesProcessed, parameters, ipcActor, outputFilename, annoFilename, ReportLowerClientLimit)

  exit()

if __name__ == "__main__":
  main()
