# Copyright 2016-2017 Capital One Services, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A cloudwatch log subscriber that records error messages into getsentry.com

Features

- For on premise sentry installations, also supports relaying through
  sqs for final delivery.

- For extant logs supports replaying them through to sentry.

- Supports self-provisioning into lambda with minimal dependency set.

- Supports collecting errors from custodian policy lambda logs or on
  ec2 instance policy logs.

- Can be run as cli against historical logs

- Auto creates projects in sentry

Todo:

- Finish lambda provision / sqs relay

Discussion:

For realtime indexing w/ custodian this is currently setup as a
lambda per account.

 - We need one lambda in the spoke account for all lambda policies
   executing in the spoke account.

 - We need one lambda in the hub account for each spoke account
   that has instance policies executing there.


OrgMode

 - Can operate with a single lambda given a mapping of accounts
   to sentry projects

"""
from __future__ import absolute_import, division, print_function, unicode_literals

import argparse
import base64
from datetime import datetime
from functools import partial
import json
import logging
import os
import time
import uuid
import zlib

# no third-party libs needed in lambda
import boto3
from botocore.exceptions import ClientError
from botocore.vendored import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
from dateutil.parser import parse as parse_date
from six.moves.urllib.parse import urlparse

sqs = logs = config = None

VERSION = "0.1"

log = logging.getLogger("c7n-sentry")


def init():
    """ Lambda globals cache.
    """
    global sqs, logs, config
    if config is None:
        with open('config.json') as fh:
            config = json.load(fh)
    if sqs is None:
        sqs = boto3.client('sqs')
    if logs is None:
        logs = boto3.client('logs')


def process_log_event(event, context):
    """Lambda Entrypoint - Log Subscriber

    Format log events and relay to sentry (direct or sqs)
    """
    init()
    # Grab the actual error log payload
    serialized = event['awslogs'].pop('data')
    data = json.loads(zlib.decompress(
        base64.b64decode(serialized), 16 + zlib.MAX_WBITS))
    msg = get_sentry_message(config, data)
    if msg is None:
        return
    if config['sentry_dsn']:
        # Deliver directly to sentry
        send_sentry_message(config['sentry_dsn'], msg)
    elif config['sentry_sqs']:
        # Delivery indirectly via sqs
        sqs.send_message(
            QueueUrl=config['sentry_sqs'])


def process_sqs(event, context):
    """Lambda Entrypoint - SQS
    """
    init()


def process_log_group(config):
    """CLI - Replay / Index
    """

    from c7n.credentials import SessionFactory
    factory = SessionFactory(
        config.region, config.profile, assume_role=config.role)
    session = factory()
    client = session.client('logs')

    params = dict(logGroupName=config.log_group,
                  filterPattern='Traceback', interleaved=True)
    if config.log_streams:
        params['logStreamNames'] = config.log_streams

    if config.start:
        params['startTime'] = int(time.mktime(
            parse_date(config.start).replace(
                hour=0, minute=0, second=0, microsecond=0).timetuple()) * 1000)
    if config.end:
        params['endTime'] = int(time.mktime(
            parse_date(config.end).replace(
                hour=0, minute=0, second=0, microsecond=0).timetuple()) * 1000)

    settings = dict(account_id=config.account_id,
                    account_name=config.account_name)
    paginator = client.get_paginator('filter_log_events')

    event_count = 0
    log.debug("Querying log events with %s", params)
    for p in paginator.paginate(**params):
        # log.debug("Searched streams\n %s", ", ".join(
        #    [s['logStreamName'] for s in p['searchedLogStreams']]))
        for e in p['events']:
            event_count += 1
            msg = get_sentry_message(
                settings, {'logEvents': [e],
                           'logStream': e['logStreamName'],
                           'logGroup': config.log_group}, client)
            if msg is None:
                continue
            send_sentry_message(config.sentry_dsn, msg)

    if event_count > 0:
        log.info("Processed %s %d error events", config.account_name, event_count)


def send_sentry_message(sentry_dsn, msg):
    # reversed from raven.base along with raven docs
    parsed = urlparse(sentry_dsn)
    key, secret = parsed.netloc.split('@')[0].split(':')
    project_id = parsed.path.strip('/')
    msg['project'] = project_id
    endpoint = "%s://%s/api/%s/store/" % (
        parsed.scheme, parsed.netloc.split('@')[1], project_id)

    client = 'custodian-python-%s' % VERSION
    auth_header_keys = [
        ('sentry_timestamp', time.time()),
        ('sentry_client', client),
        ('sentry_version', '7'),  # try 7?
        ('sentry_key', key),
        ('sentry_secret', secret)]
    auth_header = "Sentry %s" % ', '.join(
        "%s=%s" % (k, v) for k, v in auth_header_keys)
    headers = {
        'User-Agent': client,
        'X-Sentry-Auth': auth_header,
        'Content-Encoding': 'deflate',
        'Content-Type': 'application/octet-stream'}
    encoded = zlib.compress(json.dumps(msg).encode('utf8'))
    result = requests.post(endpoint, data=encoded, headers=headers)
    if result.status_code != 200:
        log.info("Got status code %s" % result.status_code)


def get_sentry_message(config, data, log_client=None, is_lambda=True):
    # Policy name extraction from log group and stream.
    group = data['logGroup']
    stream = data['logStream']

    if group.startswith('/aws/lambda'):
        policy = "-".join(group.split('/')[-1].split('-')[1:])
        module_prefix = "/var/task"
    else:
        policy = stream
        module_prefix = "site-package"

    # Parse the stringified traceback to get a structured exception
    # for sentry.
    try:
        error_msg, error = parse_traceback(
            data['logEvents'][0]['message'], module_prefix)
    except IndexError:
        # error messages without a traceback .. skip
        log.info("no traceback, %s" % data['logEvents'][0]['message'])
        return None

    # WARNING - highly log format dependent :-(
    try:
        _, level, logger, msg_frag = [s.strip() for s in error_msg[
            error_msg.find(','):].split('-', 3)]
        error_msg = " - ".join([level, logger, msg_frag])
    except:
        level, logger = 'ERROR', None

    for f in reversed(error['stacktrace']['frames']):
        culprit = "%s.%s" % (f['module'], f['function'])
        if f['module'].startswith('c7n'):
            break

    breadcrumbs = None
    # Fetch additional logs for context (10s window)
#    if 0:
#        timestamps = [e['timestamp'] for e in data['logEvents']]
#        start = min(timestamps) - 1000 * 10
#        end = max(timestamps) + 1000
#        breadcrumbs = log_client.get_log_events(
#            logGroupName=data['logGroup'],
#            logStreamName=data['logStream'],
#            startTime=start,
#            endTime=end,
#            startFromHead=True)['events'][:5]
#        if data['logEvents'][0] in breadcrumbs:
#            breadcrumbs.remove(data['logEvents'][0])
#    else:

    sentry_msg = {
        'event_id': uuid.uuid4().hex,
        'timestamp': datetime.fromtimestamp(
            data['logEvents'][0]['timestamp'] / 1000).isoformat(),
        'user': {
            'id': config['account_id'],
            'username': config['account_name']},
        'level': level.lower(),
        'culprit': culprit,
        'message': error_msg,
        'platform': 'python',
        'exception': {'values': [error]},
        'tags': {
            'policy': policy,
            'stream': stream,
            'group': group},
    }

    if logger: