import logging
from datetime import datetime, timedelta
import time
from typing import List
from operator import itemgetter
import pytz

from influxdb_client.client.write_api import WriteType, WriteOptions
import boto3
from botocore.exceptions import ClientError
import ujson

from algo.config import config
from algo.data.connection import DataConnection
from algo.data.common import CompletePricePoint

log = logging.getLogger(__name__)

DATA_SENDING_INTERVAL = timedelta(milliseconds=5000)


class InfluxDataReporter(DataConnection):  # pylint: disable=too-few-public-methods
    def __init__(self, url: str, token: str, org: str):
        super().__init__(url, token, org)
        self._write_api = self._client.write_api(write_options=WriteOptions(WriteType.batching))
        self._last_sent_time = datetime.now()
        self._message_stack = []

    def report(self, price: CompletePricePoint):
        self._message_stack.append(price)
        if datetime.now() - self._last_sent_time > DATA_SENDING_INTERVAL:
            self._send_data()

    @staticmethod
    def _to_influx_data(price: CompletePricePoint):
        return {
            "measurement": 'price',
            "tags": {
                "instrument": price.instrument.value,
            },
            "time": price.time,
            "fields": {
                "ask": price.ask,
                "bid": price.bid,
            }
        }

    def _send_data(self):
        self._last_sent_time = datetime.now()
        start_time = time.time()
        messages_count = len(self._message_stack)
        json_body = [self._to_influx_data(d) for d in self._message_stack]
        self._message_stack.clear()
        self._write_api.write(bucket='tsdata', record=json_body)
        log.info('influx-data-sent', extra={
            'count': messages_count,
            'time_to_send_sec': time.time() - start_time
        })


class InfluxStrategyOutput(InfluxDataReporter):

    def report_point(self, data_stream: str, time_mark: datetime, data: dict):
        self._message_stack.append({
            "measurement": data_stream,
            "time": time_mark,
            "fields": data
        })
        if len(self._message_stack) > 1000:
            # self._write_api.write(
            #     bucket='tsdata',
            #     record=self._message_stack
            # )
            self._message_stack.clear()
            log.info('strategy-output 1k records sent', extra={
                'data_stream': data_stream
            })

    # this one has a space to lower current limit is 5 requests per second


ALREADY_EXISTS_ERROR_CODE = 'ResourceAlreadyExistsException'
INVALID_SEQUENCE_TOKEN_ERROR_CODE = 'InvalidSequenceTokenException'


class CloudWatchDataReporter:

    def _init_connection(self, log_group_name, log_stream_name):
        api = boto3.client('logs')

        try:
            api.create_log_group(logGroupName=log_group_name)
        except ClientError as exc:
            log.info(
                'error creating log group',
                extra={
                    'e': exc, 't': type(exc),
                    'error-details': exc.response
                }
            )
            if exc.response['Error']['Code'] != ALREADY_EXISTS_ERROR_CODE:
                raise

        try:
            api.create_log_stream(
                logGroupName=log_group_name,
                logStreamName=log_stream_name
            )
        except ClientError as exc:
            log.error(
                'error creating log stream',
                extra={
                    'e': exc, 't': type(exc),
                    'error-details': exc.response
                }
            )
            if exc.response['Error']['Code'] != ALREADY_EXISTS_ERROR_CODE:
                raise
        return api

    def __init__(self, log_group_name, log_stream_name) -> None:
        self._log_group_name = log_group_name
        self._log_stream_name = log_stream_name

        self._price_stack: List[CompletePricePoint] = []
        self._last_report_time = datetime.now()
        self._last_sequence_token = None
        self._api = self._init_connection(log_group_name, log_stream_name)

    def report(self, price: CompletePricePoint):
        self._price_stack.append(price)
        self._send_data()

    def _init_sequence_token(self):
        if self._last_sequence_token is None:
            streams = self._api.describe_log_streams(
                logGroupName=self._log_group_name, logStreamNamePrefix=self._log_stream_name
            )
            # uploadSequenceToken will be absent on first run for new stream
            self._last_sequence_token = streams['logStreams'][0].get(
                'uploadSequenceToken'
            )

    def _send_data(self):
        self._init_sequence_token()
        # TBD: need to catch potential errors and reconnect
        if datetime.now() - self._last_report_time > DATA_SENDING_INTERVAL:
            messages_count = len(self._price_stack)
            sending_start_time = time.time()
            log.info(
                'sending-cloudtrail-message',
                extra={
                    'dt': datetime.now() - self._last_report_time,
                    'data-count': messages_count
                }
            )
            self._last_report_time = datetime.now()
            records_batch = [{
                'timestamp': int(p.time.astimezone(pytz.utc).timestamp() * 1000),
                'message': ujson.dumps(p.dict)
            } for p in self._price_stack]

            log_events_args = {
                'logGroupName': self._log_group_name,
                'logStreamName': self._log_stream_name,
                'logEvents': sorted(records_batch, key=itemgetter('timestamp'), reverse=False),
            }
            self._price_stack.clear()

            if self._last_sequence_token is not None:
                log_events_args['sequenceToken'] = self._last_sequence_token

            self._send_data_failsafe(**log_events_args)
            log.info('cloud-trail-message-sent', extra={
                'count': messages_count,
                'time_to_send_sec': time.time() - sending_start_time
            })

    def _send_data_failsafe(self, **log_events_args):
        keep_trying = True
        while keep_trying:
            try:
                resp = self._api.put_log_events(**log_events_args)
                self._last_sequence_token = resp.get('nextSequenceToken', None)
                log.debug('put_log_event_response', extra={'response': resp})
                keep_trying = False
            except ClientError as exc:
                log.error(
                    'error sending data',
                    extra={
                        'e': exc, 't': type(exc),
                        'error-details': exc.response
                    }
                )
                # InvalidSequenceTokenException sometimes just happens(no good idea why)
                if exc.response['Error']['Code'] == INVALID_SEQUENCE_TOKEN_ERROR_CODE:
                    log.info('correct-sequence-token',
                             extra={
                                 'token': exc.response['expectedSequenceToken']}
                             )
                    log_events_args['sequenceToken'] = exc.response['expectedSequenceToken']
                    keep_trying = True
                else:
                    log.critical(
                        'unexpected error sending data to cloudwatch',
                        extra={
                            'e': exc, 't': type(exc),
                            'error-details': exc.response
                        })
                    raise


def get_influx_strategy_output() -> InfluxStrategyOutput:
    return InfluxStrategyOutput(
        'http://192.168.1.137:8086', None, 'some-org'
    )


def get_influx_data_reporter() -> InfluxDataReporter:
    return InfluxDataReporter(
        config.influx_url,
        config.influx_token,
        config.influx_org
    )


def get_cloud_watch_reporter() -> CloudWatchDataReporter:
    return CloudWatchDataReporter(
        config.aws_cloud_watch_log_group_name,
        config.aws_cloud_watch_log_stream_name
    )
