import json
import pytz

from datetime import datetime
from timestream.dimensions import TimestreamDimensions
from timestream.util import is_time_key, is_version_key
from typing import List


class TimestreamRecordMulti:
    '''
    A TimestreamRecordMulti represents a single record that can be loaded into Timestream.
    TimestreamRecordMultis are immutable (i.e. after their creation they cannot be updated).
    This immutability is intended to preserve the validity of the TimestreamRecordMulti's
    attributes (which are validated on init)
    '''

    def __init__(
        self,
        measure_values: List[dict],
        dimensions: TimestreamDimensions = None,
        mstime: int = None
    ):
        assert not dimensions or type(dimensions) is TimestreamDimensions, f'Invalid dimensions for TimestreamRecordMulti with type {type(dimensions)}'
        assert not mstime or (type(mstime) is int and mstime > 0), f'Millisecond time must be an integer greater than 0, got {mstime}'

        # Immutable fields
        self.__measure_values = self.parse_values(measure_values)
        self.__dimensions = dimensions or TimestreamDimensions()
        self.__mstime = None if mstime is None else int(mstime)

    @property
    def dimensions(self) -> TimestreamDimensions:
        return self.__dimensions.copy()

    @property
    def mstime(self) -> int:
        return self.__mstime

    @property
    def time(self) -> datetime:
        '''Return the mstime as a UTC datetime'''
        if self.mstime:
            # fromtimestamp returns a naive datetime object that IS adjusted for the local timezone
            return datetime.fromtimestamp(self.mstime/1000).astimezone().astimezone(pytz.utc)
        return None
    
    def parse_values(self, to_parse: List[dict]):
        multi = []
        for i in to_parse:
            for k,v in i.items():
                assert k and type(k) is str, f'Invalid measure name "{k}" with type "{type(k)}"'
                assert not is_time_key(k), f'Invalid measure name "{k}". Keyword "time" cannot be used as a measure name'
                assert not is_version_key(k), f'Invalid measure name "{k}". Keyword "version" cannot be used as a measure name'
                assert type(v) in [int, float], f'Invalid measure value type "{type(v)}", must be INT or FLOAT'
                multi.append({
                    'Name': k,
                    'Value': str(v).strip(),
                    'Type': 'DOUBLE'
                })
        return multi
    
    def loadable(self, include_dimensions: bool = True, omit_dimensions: TimestreamDimensions = None) -> dict:
        '''loadable returns a dictionary that is ready to load into Timestream'''
        loadable = {
            'MeasureValues': self.__measure_values
        }
        if self.mstime is not None:
            loadable['Time'] = str(self.mstime).strip()
        if include_dimensions:
            loadable['Dimensions'] = self.__dimensions.loadable(
                omit_dimensions=omit_dimensions.get_dimension_keys()
            )
        return loadable

    def to_dict(self) -> dict:
        '''to_dict returns a human friendly dictionary representation of this TimestreamRecordMulti'''
        record_dict = {
            'measure_values': self.__measure_values
        }
        if self.time is not None:
            # format the time the way Timestream formats time
            record_dict['time'] = self.time.strftime('%Y-%m-%d %H:%M:%S.%f000')
        record_dict = {**record_dict, **self.dimensions.to_dict()}
        return record_dict

    def __repr__(self) -> str:
        return json.dumps(self.to_dict())

class RejectedTimestreamRecordMulti:
    '''
    RejectedTimestreamRecordMulti is an immutable class representing a TimestreamRecordMulti that got rejected by Timestream during load.
    This class carries the error message that Timestream returned when rejecting this record.
    '''

    def __init__(self, record: TimestreamRecordMulti, error: Exception):
        assert type(record) is TimestreamRecordMulti
        assert error is not None
        self.__record = record
        self.__error = error

    @property
    def record(self) -> TimestreamRecordMulti:
        return self.__record

    @property
    def error(self) -> Exception:
        return self.__error

    def to_dict(self) -> dict:
        return {
            'record': self.record.to_dict(),
            'error': str(self.error)
        }

    def __repr__(self) -> str:
        return json.dumps(self.to_dict())