import ast
import uuid
from datetime import datetime
from sqlite3 import Connection
from typing import (
    List,
    Optional,
)
from uuid import UUID

import iso8601
from dbapitemplate import DbApiTemplate
from messagebus import Event
from model import (
    EventPublishDetail,
    AsyncTarget,
    PublicationHistory,
    PublicationAction,
)
from model.repository import EventRepository


def convert_optional_to_date(data):
    if data is None:
        return None
    return iso8601.parse_date(data).replace(tzinfo=None)


class SqliteEventRepository(EventRepository):
    def __init__(self, conn: Connection):
        self.conn = DbApiTemplate(conn)

    def save_event(self, event: EventPublishDetail):
        self.conn.execute(
            sql=self._save_event_query,
            params=(
                str(event.id),
                self._optional_datetime_formatter(event.created_at),
                event.external_code,
                event.event.subject,
                event.event.event_type,
                event.event.data,
                str(event.event.headers),
                self._optional_datetime_formatter(event.next_retry_at),
                event.retry_index,
                event.target,
                event.async_target_reply_subject,
                event.async_target_id_field,
                event.async_target_id,
                event.fifo,
            )
        )

    def get_by_id(self, id: UUID):
        data = self.conn.execute(
            sql=self._get_by_id_query,
            params=(str(id), ),
            mapper=self._event_definition_mapper,
        )
        if len(data) == 0:
            return None

        history = self.conn.execute(
            sql=self._get_history_by_definition_id,
            params=(str(id), ),
            mapper=self._publication_history_mapper,
        )
        data[0].publication_history = history

        return data[0]

    def get_pending(self):

        return self.conn.execute(
            sql=self._get_pending_events_query,
            mapper=self._event_definition_mapper,
        )

    def get_abandoned(self) -> List[EventPublishDetail]:

        return self.conn.execute(
            sql=self._get_abandoned_events_query,
            mapper=self._event_definition_mapper,
        )

    def save(self, event_definition: EventPublishDetail) -> None:
        self.conn.execute(
            sql=self._update_event_definition_query,
            params=(
                self._optional_datetime_formatter(event_definition.acked_at),
                event_definition.retry_index,
                self._optional_datetime_formatter(event_definition.next_retry_at),
                self._optional_datetime_formatter(event_definition.abandoned_at),
                str(event_definition.id),
            )
        )
        for history in event_definition.publication_history:
            if hasattr(history, "_saved"):
                continue

            history._saved = True
            self.conn.execute(
                sql=self._create_publication_history,
                params=(
                    str(event_definition.id),
                    history.created_at,
                    history.action.name,
                )
            )

    def get_by_external_code(self, external_code: str):
        return self.conn.first(
            sql=self._get_by_external_code_query,
            mapper=self._event_definition_mapper,
            params=(external_code, )
        )

    def cleanup_abandoned_events(
        self,
        min_abandoned_date: datetime,
    ) -> None:
        ids = self.conn.execute(
            sql=self._get_cleanup_abandoned_events_ids,
            mapper=self._id_mapper,
            params=(min_abandoned_date.isoformat(),),
        )

        if len(ids) > 0:
            self._clean_events_by_ids(ids=ids)

    def cleanup_acked_events(
        self,
        min_acked_date: datetime,
    ) -> None:
        ids = self.conn.execute(
            sql=self._get_cleanup_acked_event_ids,
            mapper=self._id_mapper,
            params=(min_acked_date.isoformat(),),
        )

        if len(ids) > 0:
            self._clean_events_by_ids(ids=ids)

    def _clean_events_by_ids(self, ids: List[str]):
        delete_ids = f'"{",".join(ids)}"'

        query = self._cleanup_publication_history.format(delete_ids)
        self.conn.execute(
            sql=query,
        )
        query = self._cleanup_pending_events.format(delete_ids)
        self.conn.execute(
            sql=query,
        )

    @staticmethod
    def _id_mapper(row):
        return str(row["Id"])

    @staticmethod
    def _optional_datetime_formatter(v: Optional[datetime]) -> Optional[str]:
        if v is None:
            return v

        return v.isoformat()

    @staticmethod
    def _event_definition_mapper(row):
        async_target = None
        if row["AsyncReplySubject"] is not None:
            async_target = AsyncTarget(
                reply_subject=row["AsyncReplySubject"],
                id_field=row["AsyncIdField"]
            )
        return EventPublishDetail(
            id=uuid.UUID(row["Id"]),
            created_at=iso8601.parse_date(row["CreatedAt"]).replace(tzinfo=None),
            external_code=row["ExternalCode"],
            event=Event(
                subject=row["Subject"],
                event_type=row["EventType"],
                data=row["Data"],
                headers=ast.literal_eval(row["Headers"]),
            ),
            target=row["Target"],
            async_target=async_target,
            fifo=bool(row["Fifo"]),
            retry_index=row["RetryIndex"],
            acked_at=convert_optional_to_date(row["AckedAt"]),
            next_retry_at=convert_optional_to_date(row["NextRetryAt"]),
            abandoned_at=convert_optional_to_date(row["AbandonedAt"]),
        )

    @staticmethod
    def _publication_history_mapper(row):
        return PublicationHistory(
            created_at=iso8601.parse_date(row["CreatedAt"]).replace(tzinfo=None),
            action=PublicationAction(row["Action"])
        )

    _event_definition_columns = """
        Id,
        CreatedAt,
        ExternalCode,
        Subject,
        EventType,
        Data,
        Headers,
        AckedAt,
        RetryIndex,
        NextRetryAt,
        AbandonedAt,
        Target,
        AsyncReplySubject,
        AsyncIdField,
        Fifo
    """

    _save_event_query = """
        INSERT INTO PendingEvent (
            Id,
            CreatedAt,
            ExternalCode,
            Subject,
            EventType,
            Data,
            Headers,
            NextRetryAt,
            RetryIndex,
            Target,
            AsyncReplySubject,
            AsyncIdField,
            AsyncId,
            Fifo
        )
        VALUES
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    """

    _get_by_id_query = f"""
        SELECT {_event_definition_columns}
        FROM
        PendingEvent
        WHERE Id = ?
    """

    _get_by_external_code_query = f"""
            SELECT {_event_definition_columns}
            FROM
            PendingEvent
            WHERE ExternalCode = ?
        """

    _get_pending_events_query = f"""
       SELECT {_event_definition_columns}
       FROM
       PendingEvent
       WHERE AckedAt IS NULL and AbandonedAt is NULL
       ORDER BY CreatedAt
       LIMIT 100
    """

    _get_abandoned_events_query = f"""
           SELECT {_event_definition_columns}
           FROM
           PendingEvent
           WHERE AbandonedAt IS NOT NULL
           ORDER BY CreatedAt DESC
           LIMIT 10
        """

    _update_event_definition_query = """
        UPDATE
        PendingEvent
        set AckedAt = ?,
            RetryIndex = ?,
            NextRetryAt = ?,
            AbandonedAt = ?
        WHERE Id = ?
    """

    _create_publication_history = """
        INSERT INTO PublicationHistory (
            DefinitionId,
            CreatedAt,
            Action
        )
        VALUES
        (?, ?, ?)
    """

    _get_history_by_definition_id = """
        SELECT CreatedAt, Action
        FROM PublicationHistory
        WHERE DefinitionId = ?
    """

    _get_cleanup_abandoned_events_ids = """
        SELECT Id FROM
            PendingEvent
        WHERE AbandonedAt IS NOT NULL AND AbandonedAt < ?
    """

    _get_cleanup_acked_event_ids = """
        SELECT Id FROM
            PendingEvent
        WHERE AckedAt IS NOT NULL AND AckedAt < ?
    """

    _cleanup_publication_history = """
        DELETE FROM
            PublicationHistory
        WHERE DefinitionId IN ({})
    """

    _cleanup_pending_events = """
        DELETE FROM
            PendingEvent
        WHERE Id IN ({})
    """
