from datetime import datetime
from decimal import Decimal
import calendar
from enum import IntEnum

import timex

from sqlalchemy import event
from sqlalchemy import and_, or_
from sqlalchemy import literal_column
from sqlalchemy import Column, Table, ForeignKey, Index, UniqueConstraint
from sqlalchemy import Float, Boolean, Text, DateTime, Integer, String
from sqlalchemy import cast, null, case
from sqlalchemy.orm.interfaces import PropComparator
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.dialects.mysql import DECIMAL
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import composite
from sqlalchemy.orm import backref
from sqlalchemy.orm import relationship
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.types import TypeDecorator, DATETIME


class Datatype(IntEnum):
    none = 0
    string = 1
    int = 2
    float = 3
    datetime = 4
    timerange = 5


class StreamState(IntEnum):
    active = 1
    firing = 2
    expiring = 3
    error = 4
    expire_error = 5
    completed = 6
    retry_fire = 7
    retry_expire = 8


class DBException(Exception):
    pass


class InvalidTraitType(DBException):
    pass


def dt_to_decimal(dt):
    t_sec = calendar.timegm(dt.utctimetuple()) + (dt.microsecond/1e6)
    return Decimal("%.6f" % t_sec)


def decimal_to_dt(decimal_timestamp):
    return datetime.utcfromtimestamp(float(decimal_timestamp))


class PreciseTimestamp(TypeDecorator):
    """Represents a timestamp precise to the microsecond."""

    impl = DATETIME

    def load_dialect_impl(self, dialect):
        if dialect.name == 'mysql':
            return dialect.type_descriptor(DECIMAL(precision=20,
                                                   scale=6,
                                                   asdecimal=True))
        return dialect.type_descriptor(DATETIME())

    def process_bind_param(self, value, dialect):
        if value is None:
            return value
        elif dialect.name == 'mysql':
            return dt_to_decimal(value)
        return value

    def process_result_value(self, value, dialect):
        if value is None:
            return value
        elif dialect.name == 'mysql':
            return decimal_to_dt(value)
        return value


class DBTimeRange(object):
    def __init__(self, begin, end):
        self.begin = begin
        self.end = end

    def __composite_values__(self):
        return self.begin, self.end

    def __repr__(self):
        return "DBTimeRange(begin=%r, end=%r)" % (self.begin, self.end)

    def __eq__(self, other):
        return isinstance(other, DBTimeRange) and \
            other.begin == self.begin and \
            other.end == self.end

    def __ne__(self, other):
        return not self.__eq__(other)


class ProxiedDictMixin(object):
    """Adds obj[name] access to a mapped class.

    This class basically proxies dictionary access to an attribute
    called ``_proxied``.  The class which inherits this class
    should have an attribute called ``_proxied`` which points to a dictionary.

    """

    def __len__(self):
        return len(self._proxied)

    def __iter__(self):
        return iter(self._proxied)

    def __getitem__(self, name):
        return self._proxied[name]

    def __contains__(self, name):
        return name in self._proxied

    def __setitem__(self, name, value):
        self._proxied[name] = value

    def __delitem__(self, name):
        del self._proxied[name]


class PolymorphicVerticalProperty(object):
    """A name/value pair with polymorphic value storage."""

    ATTRIBUTE_MAP = {Datatype.none: None}
    PY_TYPE_MAP = {unicode: Datatype.string,
                   int: Datatype.int,
                   float: Datatype.float,
                   datetime: Datatype.datetime,
                   DBTimeRange: Datatype.timerange}

    def __init__(self, name, value=None):
        self.name = name
        self.value = value

    @classmethod
    def get_type_value(cls, value):
        if value is None:
            return Datatype.none, None
        if isinstance(value, str):
            value = value.decode('utf8', 'ignore')
        if isinstance(value, timex.Timestamp):
            value = value.timestamp
        if isinstance(value, timex.TimeRange):
            value = DBTimeRange(value.begin, value.end)
        if type(value) in cls.PY_TYPE_MAP:
            return cls.PY_TYPE_MAP[type(value)], value
        return None, value

    @hybrid_property
    def value(self):
        if self.type not in self.ATTRIBUTE_MAP:
            raise InvalidTraitType("Invalid trait type in db for %s: %s" % (self.name, self.type))
        attribute = self.ATTRIBUTE_MAP[self.type]
        if attribute is None:
            return None
        if self.type == Datatype.timerange:
            val = getattr(self, attribute)
            return timex.TimeRange(val.begin, val.end)
        else:
            return getattr(self, attribute)

    @value.setter
    def value(self, value):
        datatype, value = self.get_type_value(value)
        if datatype not in self.ATTRIBUTE_MAP:
            raise InvalidTraitType("Invalid trait type for %s: %s" % (self.name, datatype))
        attribute = self.ATTRIBUTE_MAP[datatype]
        self.type = int(datatype)
        if attribute is not None:
            setattr(self, attribute, value)

    @value.deleter
    def value(self):
        self._set_value(None)

    @value.comparator
    class value(PropComparator):
        """A comparator for .value, builds a polymorphic comparison.

        """
        def __init__(self, cls):
            self.cls = cls

        def __eq__(self, other):
            dtype, value = self.cls.get_type_value(other)
            if dtype is None:
                dtype = Datatype.string
            if dtype == Datatype.none:
                return self.cls.type == int(Datatype.none)
            attr = getattr(self.cls, self.cls.ATTRIBUTE_MAP[dtype])
            return and_(attr == value, self.cls.type == int(dtype))

        def __ne__(self, other):
            dtype, value = self.cls.get_type_value(other)
            if dtype is None:
                dtype = Datatype.string
            if dtype == Datatype.none:
                return self.cls.type != int(Datatype.none)
            attr = getattr(self.cls, self.cls.ATTRIBUTE_MAP[dtype])
            return and_(attr != value, self.cls.type == int(dtype))

    def __repr__(self):
        return '<%s %r=%r>' % (self.__class__.__name__, self.name, self.value)


Base = declarative_base()


class Trait(PolymorphicVerticalProperty, Base):
    __tablename__ = 'trait'
    __table_args__ = (
        Index('ix_trait_t_int', 't_int'),
        Index('ix_trait_t_string', 't_string'),
        Index('ix_trait_t_datetime', 't_datetime'),
        Index('ix_trait_t_float', 't_float'),
    )
    event_id = Column(Integer, ForeignKey('event.id'), primary_key=True)
    name = Column(String(100), primary_key=True)
    type = Column(Integer)

    ATTRIBUTE_MAP = {Datatype.none: None,
                     Datatype.string: 't_string',
                     Datatype.int: 't_int',
                     Datatype.float: 't_float',
                     Datatype.datetime: 't_datetime',}

    t_string = Column(String(255), nullable=True, default=None)
    t_float = Column(Float, nullable=True, default=None)
    t_int = Column(Integer, nullable=True, default=None)
    t_datetime = Column(PreciseTimestamp(),
                      nullable=True, default=None)

    def __repr__(self):
        return "<Trait(%s) %s=%s/%s/%s/%s on %s>" % (self.name,
                                                     self.type,
                                                     self.t_string,
                                                     self.t_float,
                                                     self.t_int,
                                                     self.t_datetime,
                                                     self.event_id)


class EventType(Base):
    """Types of event records."""
    __tablename__ = 'event_type'

    id = Column(Integer, primary_key=True)
    desc = Column(String(255), unique=True)

    def __init__(self, event_type):
        self.desc = event_type

    def __repr__(self):
        return "<EventType: %s>" % self.desc


class Event(ProxiedDictMixin, Base):
    __tablename__ = 'event'
    __table_args__ = (
        Index('ix_event_message_id', 'message_id'),
        Index('ix_event_type_id', 'event_type_id'),
        Index('ix_event_generated', 'generated')
    )
    id = Column(Integer, primary_key=True)
    message_id = Column(String(50), unique=True)
    generated = Column(PreciseTimestamp())

    event_type_id = Column(Integer, ForeignKey('event_type.id'))
    event_type = relationship("EventType", backref=backref('event_type'))

    traits = relationship("Trait",
                    collection_class=attribute_mapped_collection('name'))
    _proxied = association_proxy("traits", "value",
                            creator=lambda name, value: Trait(name=name, value=value))

    @property
    def event_type_string(self):
        return self.event_type.desc

    @property
    def as_dict(self):
        d = dict(self._proxied)
        d['message_id'] = self.message_id
        d['event_type'] = self.event_type_string
        d['timestamp'] = self.generated
        return d

    def __init__(self, message_id, event_type, generated):

         self.message_id = message_id
         self.event_type = event_type
         self.generated = generated

    def __repr__(self):
        return "<Event %s ('Event : %s %s, Generated: %s')>" % (self.id,
                                                              self.message_id,
                                                              self.event_type,
                                                              self.generated)


stream_event_table = Table('streamevent', Base.metadata,
        Column('stream_id', Integer, ForeignKey('stream.id'), primary_key=True),
        Column('event_id', Integer,
                ForeignKey('event.id'),
                primary_key=True)
        )


class Stream(ProxiedDictMixin, Base):
    __tablename__ = 'stream'

    __table_args__ = (
        Index('ix_stream_name', 'name'),
        Index('ix_stream_state', 'state'),
        Index('ix_stream_expire_timestamp', 'expire_timestamp'),
        Index('ix_stream_fire_timestamp', 'fire_timestamp')
    )
    id = Column(Integer, primary_key=True)
    first_event = Column(PreciseTimestamp(), nullable=False)
    last_event = Column(PreciseTimestamp(), nullable=False)
    expire_timestamp = Column(PreciseTimestamp())
    fire_timestamp = Column(PreciseTimestamp())
    name = Column(String(255), nullable=False)
    state = Column(Integer, default=StreamState.active, nullable=False)
    state_serial_no = Column(Integer, default=0, nullable=False)

    distinguished_by = relationship("DistinguishingTrait",
                    cascade="save-update, merge, delete, delete-orphan",
                    collection_class=attribute_mapped_collection('name'))
    _proxied = association_proxy("distinguished_by", "value",
                    creator=lambda name, value: DistinguishingTrait(name=name, value=value))

    events = relationship(Event, secondary=stream_event_table,
                          order_by=Event.generated)

    @property
    def distinguished_by_dict(self):
        return dict(self._proxied)

    @property
    def as_dict(self):
        return {'name': self.name,
                'id': self.id,
                'state': StreamState(self.state).name,
                'first_event': self.first_event,
                'last_event': self.last_event,
                'fire_timestamp': self.fire_timestamp,
                'expire_timestamp': self.expire_timestamp,
                'distinguishing_traits': self.distinguished_by_dict}



    def __init__(self, name, first_event, last_event=None, expire_timestamp=None,
                fire_timestamp=None, state=None, state_serial_no=None):
        self.name = name
        self.first_event = first_event
        if last_event is None:
            last_event = first_event
        self.last_event = last_event
        self.expire_timestamp = expire_timestamp
        self.fire_timestamp = fire_timestamp
        if state is None:
            state = StreamState.active
        self.state = int(state)
        if state_serial_no is None:
            state_serial_no = 0
        self.state_serial_no = state_serial_no


class DistinguishingTrait(PolymorphicVerticalProperty, Base):
    __tablename__ = 'dist_trait'
    __table_args__ = (
        Index('ix_dist_trait_dt_int', 'dt_int'),
        Index('ix_dist_trait_dt_float', 'dt_float'),
        Index('ix_dist_trait_dt_string', 'dt_string'),
        Index('ix_dist_trait_dt_datetime', 'dt_datetime'),
        Index('ix_dist_trait_dt_timerange_begin', 'dt_timerange_begin'),
        Index('ix_dist_trait_dt_timerange_end', 'dt_timerange_end'),
    )
    stream_id = Column(Integer, ForeignKey('stream.id'), primary_key=True)
    name = Column(String(100), primary_key=True)
    type = Column(Integer)


    ATTRIBUTE_MAP = {Datatype.none:     None,
                     Datatype.string:   'dt_string',
                     Datatype.int:      'dt_int',
                     Datatype.float:    'dt_float',
                     Datatype.datetime: 'dt_datetime',
                     Datatype.timerange:'dt_timerange',
                    }

    dt_string = Column(String(255), nullable=True, default=None)
    dt_float = Column(Float, nullable=True, default=None)
    dt_int = Column(Integer, nullable=True, default=None)
    dt_datetime = Column(PreciseTimestamp(),
                      nullable=True, default=None)
    dt_timerange_begin = Column(PreciseTimestamp(), nullable=True, default=None)
    dt_timerange_end = Column(PreciseTimestamp(), nullable=True, default=None)

    dt_timerange = composite(DBTimeRange, dt_timerange_begin, dt_timerange_end)

    @property
    def as_dict(self):
        return {self.name: self.value}

    def __repr__(self):
        return "<DistinguishingTrait(%s) %s=%s/%s/%s/%s/(%s to %s) on %s>" % (self.name,
                                                             self.type,
                                                             self.dt_string,
                                                             self.dt_float,
                                                             self.dt_int,
                                                             self.dt_datetime,
                                                             self.dt_timerange_begin,
                                                             self.dt_timerange_end,
                                                             self.stream_id)
