import json
import os
import threading
from datetime import (
    datetime,
    timedelta,
)
from logging import Logger
from typing import (
    Dict,
    List,
)

from msgbus import MBEasyContext
from mwhelper import BaseRepository
from persistence import (
    Connection,
    Row,
)

from services.model import (
    Configs,
    ConfiguredAlert,
    Event,
)


lock_db = threading.Lock()


class AlertsRepository(BaseRepository):

    def __init__(
        self,
        mb_context,  # type: MBEasyContext
        configs,     # type: Configs
        logger,      # type: Logger
    ):
        # type: (...) -> None

        self.service_name = "AlertsPersistence"
        self.configs = configs
        self.logger = logger
        self.alerts_db_exported_xml = "alerts.db.xml"

        super(
            AlertsRepository,
            self,
        ).__init__(mb_context)

    def get_configured_alerts(
        self,
    ):
        # type: () -> List[ConfiguredAlert]

        def inner_func(
            conn,  # type: Connection
        ):
            # type: (...) -> List[ConfiguredAlert]

            query = """
                SELECT Id,
                    AlertJson
                FROM alerts.ConfiguredAlerts
            """
            configured_alerts = []
            for row in conn.select(sql=query):
                obj = self._create_configured_alert(row)
                configured_alerts.append(obj)

            return configured_alerts

        result = self.execute_with_connection(
            method=inner_func,
            service_name=self.service_name
        )

        return result

    def update_configured_alerts(
        self,
        configured_alerts,  # type: List[Dict]
    ):
        # type: (...) -> None

        def inner_func(
            conn,  # type: Connection
        ):
            # type: (...) -> None

            try:
                conn.query("DELETE FROM alerts.ConfiguredAlerts;")
                self._update_configured_alerts(conn, configured_alerts)
            except Exception:  # noqa
                self.logger.exception("Error updating configured alerts")
                raise
            else:
                self.logger.info("Configured alerts updated successfully")

        self.execute_with_transaction(
            method=inner_func,
            service_name=self.service_name
        )

    def get_events(
        self,
        range_days=None,  # type: int
    ):
        # type: (...) -> List[Event]

        def inner_func(
            conn,  # type: Connection
        ):
            # type: (...) -> List[Event]

            query = """
                SELECT 
                    e.Id,
                    e.AlertId,
                    e.NotificationJson,
                    e.Timestamp,
                    e.SentStatus,
                    s.TypeDescr
                FROM alerts.Events e
                JOIN alerts.StatusType s ON s.TypeId = e.SentStatus
            """

            if range_days is not None:
                query += """
                    WHERE e.Timestamp <= '{}';
                """
                query = query.format(str(datetime.utcnow() - timedelta(days=range_days)))

            events = []
            for row in conn.select(sql=query):
                obj = self._create_event(row)
                events.append(obj)

            return events

        result = self.execute_with_connection(
            method=inner_func,
            service_name=self.service_name,
        )

        return result

    def get_events_with_sent_status(
        self,
        sent_status,   # type: int
    ):
        # type: (...) -> List[Dict]

        def inner_func(
            conn,  # type: Connection
        ):
            # type: (...) -> List[Dict]

            query = """
                SELECT Id, NotificationJson, Timestamp, SentStatus
                FROM alerts.Events
                WHERE SentStatus='{}';"""
            events = []
            for row in conn.select(sql=query.format(sent_status)):
                events.append({
                    'id': row.get_entry('Id'),
                    'notification_json': row.get_entry('NotificationJson'),
                    'time_stamp': row.get_entry('Timestamp'),
                    'sent_status': row.get_entry('SentStatus'),
                })

            return events

        result = self.execute_with_connection(
            method=inner_func,
            service_name=self.service_name,
        )

        return result

    def set_sent_status(
        self,
        event_id,      # type: str
        sent_status,   # type: int
    ):
        # type: (...) -> None

        def inner_func(
            conn,  # type: Connection
        ):
            # type: (...) -> None

            query = """
                UPDATE alerts.Events
                set SentStatus='{}' 
                WHERE Id='{}';"""

            conn.query(sql=query.format(
                sent_status,
                event_id,
            ))

        self.execute_with_connection(
            method=inner_func,
            service_name=self.service_name,
        )

    def _update_notification_json(
        self,
        event_id,           # type: str
        notification_json,  # type: dict
    ):
        # type: (...) -> None

        notification_json.update({
            "id": event_id,
        })

        def inner_func(
            conn,  # type: Connection
        ):
            # type: (...) -> None

            query = """
                UPDATE alerts.Events
                set NotificationJson='{}' 
                WHERE Id='{}';"""

            conn.query(sql=query.format(
                json.dumps(notification_json),
                event_id,
            ))

        self.execute_with_connection(
            method=inner_func,
            service_name=self.service_name,
        )

    def insert_event(
        self,
        alert_id,           # type: str
        notification_json,  # type: dict
        timestamp,          # type: str
        sent_status=0,      # type: int
    ):
        # type: (...) -> None

        with lock_db:
            def inner_func(
                conn,  # type: Connection
            ):
                # type: (...) -> str

                query = """
                    INSERT INTO alerts.Events (AlertId, NotificationJson, Timestamp, SentStatus)
                    VALUES ('{}', '{}', '{}', '{}');
                """
                conn.query(
                    query.format(
                        alert_id,
                        json.dumps(notification_json),
                        timestamp,
                        sent_status
                    )
                )

                query = """
                    SELECT MAX(id)
                    FROM Events;
                """
                cursor = conn.select(sql=query)
                row_id = cursor.get_row(0).get_entry(0)

                return row_id

            event_id = self.execute_with_connection(
                method=inner_func,
                service_name=self.service_name,
            )

            self._update_notification_json(
                event_id=event_id,
                notification_json=notification_json,
            )

    def delete_outdated_events(
        self,
        range_days,  # type: int
    ):
        # type: (...) -> None

        def inner_func(
            conn,  # type: Connection
        ):
            # type: (...) -> bool

            try:
                query = """
                    DELETE FROM alerts.Events
                    WHERE Timestamp <= '{}';
                """
                query = query.format(str(datetime.utcnow() - timedelta(days=range_days)))
                conn.query(sql=query)

            except Exception as _:
                return False

            return True

        result = self.execute_with_connection(
            method=inner_func,
            service_name=self.service_name,
        )

        if not result:
            raise Exception("Error deleting outdated events")

    def vacuum_database(
        self,
    ):
        # type: (...) -> None

        with lock_db:
            db_info = {}

            def inner_func(
                conn,  # type: Connection
            ):
                # type: (...) -> bool

                try:
                    db_info["db_file"] = self.get_db_file(
                        conn=conn,
                        db_name_to_find="alerts",
                    )
                    db_info["old_file_size"] = self.get_file_size(file_name=db_info.get("db_file"))
                    query = """
                        VACUUM alerts;
                    """
                    conn.query(sql=query)
                except Exception as _:
                    return False

                return True

            result = self.execute_with_connection(
                method=inner_func,
                service_name=self.service_name,
            )

            if not result:
                raise Exception("Error doing vacuum on alerts db")

            db_info["new_file_size"] = self.get_file_size(file_name=db_info.get("db_file"))
            self.logger.info("Alerts db file sizes. OLD: {} bytes; NEW: {} bytes".format(
                db_info.get("old_file_size"),
                db_info.get("new_file_size"),
            ))
            self.logger.info("Finished [vacuum_alerts_database]")

    @staticmethod
    def _update_configured_alerts(
        conn,               # type: Connection
        configured_alerts,  # type: List[Dict]
    ):
        # type: (...) -> None

        for index, alert in enumerate(configured_alerts, 1):
            query = """
                INSERT INTO alerts.ConfiguredAlerts (Id, AlertJson)
                VALUES ('{}', '{}')
            """
            conn.query(
                query.format(
                    index,
                    json.dumps(alert)
                )
            )

    @staticmethod
    def get_db_file(
        conn,
        db_name_to_find,
    ):

        db_file = None
        pragma_cursor = conn.select("PRAGMA database_list;")
        for pragma_row in pragma_cursor:
            db_name, db_file = map(pragma_row.get_entry, (1, 2,))
            if db_name == db_name_to_find:
                break
        return db_file

    @staticmethod
    def get_file_size(
        file_name,  # type: str
    ):
        # type: (...) -> int

        return os.path.getsize(file_name)

    @staticmethod
    def _create_configured_alert(
        row,  # type: Row
    ):
        # type: (...) -> ConfiguredAlert

        obj = ConfiguredAlert(
            alert_id=row.get_entry("Id"),
            alert_json=json.loads(row.get_entry("AlertJson")),
        )
        return obj

    @staticmethod
    def _create_event(
        row,  # type: Row
    ):
        # type: (...) -> Event

        obj = Event(
            event_id=row.get_entry("Id"),
            alert_id=row.get_entry("AlertId"),
            notification_json=row.get_entry("NotificationJson"),
            timestamp=row.get_entry("Timestamp"),
            sent_status=row.get_entry("SentStatus"),
            status_desc=row.get_entry("TypeDescr"),
        )
        return obj
