# -*- coding: utf-8 -*-
"""
gdcdatamodel.models.submission
----------------------------------

Models for submission TransactionLogs
"""

from datetime import datetime
from json import loads, dumps
from sqlalchemy import func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, deferred

import pytz

from sqlalchemy import (
    Boolean,
    Column,
    DateTime,
    ForeignKey,
    Integer,
    Index,
    Text,
    text,
)

Base = declarative_base()


def datetime_to_unix(dt):
    return (dt - datetime(1970, 1, 1, tzinfo=pytz.utc)).total_seconds()


class TransactionLog(Base):
    __tablename__ = "transaction_logs"

    @declared_attr
    def __table_args__(cls):
        tbl = cls.__tablename__
        return (
            Index("{}_program_idx".format(tbl), "program"),
            Index("{}_project_idx".format(tbl), "project"),
            Index("{}_is_dry_run_idx".format(tbl), "is_dry_run"),
            Index("{}_committed_by_idx".format(tbl), "committed_by"),
            Index("{}_closed_idx".format(tbl), "closed"),
            Index("{}_state_idx".format(tbl), "state"),
            Index("{}_submitter_idx".format(tbl), "submitter"),
            Index("{}_created_datetime_idx".format(tbl), "created_datetime"),
            Index("{}_project_id_idx".format(tbl), cls.program + "-" + cls.project),
        )

    def __repr__(self):
        return "<TransactionLog({}, {})>".format(self.id, self.created_datetime)

    def to_json(self, fields=set()):
        # Source fields
        existing_fields = [c.name for c in self.__table__.c] + ["entities", "documents"]
        custom_fields = {"created_datetime", "entities", "documents"}

        # Pull out child fields
        entity_fields = {f for f in fields if f.startswith("entities.")}
        document_fields = {f for f in fields if f.startswith("documents.")}
        fields = fields - entity_fields - document_fields

        # Reformat child fields
        entity_fields = {f.replace("entities.", "") for f in entity_fields}
        document_fields = {f.replace("documents.", "") for f in document_fields}

        # Default fields
        if not fields:
            fields = {"id", "submitter", "role", "program", "created_datetime"}

        # Check for field existence
        if set(fields) - set(existing_fields):
            raise RuntimeError(
                "Fields do not exist: {}".format(
                    ", ".join((set(fields) - set(existing_fields)))
                )
            )

        # Set standard fields
        doc = {key: getattr(self, key) for key in fields if key not in custom_fields}

        # Add custom fields
        if "entities" in fields or entity_fields:
            doc["entities"] = [n.to_json(entity_fields) for n in self.entities]
        if "documents" in fields or document_fields:
            doc["documents"] = [n.to_json(document_fields) for n in self.documents]
        if "created_datetime" in fields:
            doc["created_datetime"] = self.created_datetime.isoformat("T")

        return doc

    id = Column(
        Integer,
        primary_key=True,
    )

    submitter = Column(
        Text,
    )

    role = Column(
        Text,
        nullable=False,
    )

    program = Column(
        Text,
        nullable=False,
    )

    project = Column(
        Text,
        nullable=False,
    )

    #: Specifies a non-dry_run transaction that repeated this
    #: transaction in an attempt to write to the database
    committed_by = Column(
        Integer,
    )

    #: Was this transaction a dry_run (for validation)
    is_dry_run = Column(
        Boolean,
        nullable=False,
    )

    #: Has this transaction succeeded, errored, failed, etc.
    state = Column(
        Text,
        nullable=False,
    )

    closed = Column(
        Boolean,
        default=False,
        nullable=False,
    )

    @hybrid_property
    def project_id(self):
        return self.program + "-" + self.project

    @project_id.expression
    def project_id(cls):
        return func.concat(cls.program, "-", cls.project)

    created_datetime = Column(
        DateTime(timezone=True),
        nullable=False,
        server_default=text("now()"),
    )

    canonical_json = deferred(
        Column(
            JSONB,
            nullable=False,
            default=JSONB.NULL,
        )
    )


class TransactionSnapshot(Base):
    __tablename__ = "transaction_snapshots"

    def __repr__(self):
        return "<TransactionSnapshot({}, {})>".format(self.id, self.transaction_id)

    def to_json(self, fields=set()):
        fields = set(fields)
        existing_fields = [c.name for c in self.__table__.c]
        if not fields:
            fields = existing_fields
        if set(fields) - set(existing_fields):
            raise RuntimeError(
                "Entity fields do not exist: {}".format(
                    ", ".join((set(fields) - set(existing_fields)))
                )
            )
        doc = {key: getattr(self, key) for key in fields}
        return doc

    id = Column(
        Integer,
        primary_key=True,
    )

    entity_id = Column(
        Text,
        nullable=False,
        index=True,
    )

    transaction_id = Column(
        Integer,
        ForeignKey("transaction_logs.id"),
        index=True,
    )

    action = Column(
        Text,
        nullable=False,
    )

    old_props = Column(
        JSONB,
        nullable=False,
        default=JSONB.NULL,
    )

    new_props = Column(
        JSONB,
        nullable=False,
        default=JSONB.NULL,
    )

    transaction = relationship("TransactionLog", backref="entities")

    __table_args__ = (Index("snapshot_transaction_idx", "transaction_id"),)


class TransactionDocument(Base):
    __tablename__ = "transaction_documents"

    def to_json(self, fields=set()):
        # Source fields
        fields = set(fields)
        existing_fields = {c.name for c in self.__table__.c}

        # Default fields
        if not fields:
            fields = existing_fields

        # Check field existence
        if set(fields) - set(existing_fields):
            raise RuntimeError(
                "Entity fields do not exist: {}".format(
                    ", ".join(fields - existing_fields)
                )
            )

        # Generate doc
        doc = {key: getattr(self, key) for key in fields}
        return doc

    id = Column(
        Integer,
        primary_key=True,
        autoincrement=True,
        nullable=False,
    )

    transaction_id = Column(
        Integer,
        ForeignKey("transaction_logs.id"),
        primary_key=True,
    )

    name = Column(
        Text,
    )

    doc_format = Column(
        Text,
        nullable=False,
    )

    doc = deferred(
        Column(
            Text,
            nullable=False,
        )
    )

    response_json = deferred(
        Column(
            JSONB,
        )
    )

    transaction = relationship("TransactionLog", backref="documents")

    __table_args__ = (Index("document_transaction_idx", "transaction_id"),)

    @property
    def is_json(self):
        if self.doc_format.upper() != "JSON":
            return False
        else:
            return True

    @property
    def is_xml(self):
        if self.doc_format.upper() != "XML":
            return False
        else:
            return True

    @property
    def json(self):
        if not self.is_json:
            return None
        return loads(self.doc)

    @json.setter
    def json(self, doc):
        self.doc_format = "JSON"
        self.doc = dumps(doc)

    @property
    def xml(self):
        if not self.is_xml:
            return None
        return self.doc

    @xml.setter
    def xml(self, doc):
        self.doc_format = "XML"
        self.doc = doc
