import asyncio
import logging

import pymysqlreplication

from ..abc.source import Source

#

L = logging.getLogger(__name__)

#


class MySQLBinaryLogSource(Source):
    ConfigDefaults = {
        "server_id": 1,
        "log_file": "",  # name of the first log file
        "log_pos": 4,
        "auto_position": False,  # If server supports GTID, scroll to latest replicated position
        "extract_events": """
			DeleteRowsEvent
			UpdateRowsEvent
			WriteRowsEvent
			""",
    }

    """
	Before you started using it, make sure that
	my.cnf configuration file contains following lines:

	[mysqld]
	server-id		 = 1
	log_bin			 = /var/log/mysql/mysql-bin.log
	expire_logs_days = 10
	max_binlog_size  = 100M
	binlog-format    = row #Very important if you want to receive write, update and delete row events

	Also don't forget to restart the server after changing the config file.

	By default, the it picks the last log file, but you can specify your own, you can find it in /var/log/mysql/ folder, so it
	will process all files, started from specified one.

	The source extracts events from MySQL binary log. By default it extracts only [DeleteRowsEvent, UpdateRowsEvent, WriteRowsEvent],
	but you can extend the list in configuration with [QueryEvent, RotateEvent,StopEvent,FormatDescriptionEvent,XidEvent,
		GtidEvent,BeginLoadQueryEvent,ExecuteLoadQueryEvent,TableMapEvent,HeartbeatLogEvent]

	Examples:
	UpdateRowsEvent event:
		{
			'@timestamp': 1551791161,
			'event_size': 47,
			'log_position': 1251,
			'read_bytes': 13,
			'rows': [
				{'after_values': {'data': 'World', 'data2': 'Hello', 'id': 1}},
				{'before_values': {'data': 'Hello', 'data2': 'World', 'id': 1}}
			],
			'type': 'UpdateRowsEvent'
		}

	DeleteRowsEvent event:
		{
			'@timestamp': 1551791168,
			'event_size': 29,
			'log_position': 1525,
			'read_bytes': 12,
			'rows': [{'values': {'data': 'World', 'data2': 'Hello', 'id': 1}}],
			'type': 'DeleteRowsEvent'
		}

	WriteRowsEvent event:
		{
			'@timestamp': 1551791153,
			'event_size': 29,
			'log_position': 959,
			'read_bytes': 12,
			'rows': [{'values': {'data': 'Hello', 'data2': 'World', 'id': 1}}],
			'type': 'WriteRowsEvent'
		}

	"""

    def __init__(self, app, pipeline, connection, id=None, config=None):
        super().__init__(app, pipeline, id=id, config=config)
        self._connection = pipeline.locate_connection(app, connection)
        self.MySQLSettings = {
            "host": self._connection._host,
            "port": self._connection._port,
            "user": self._connection._user,
            "passwd": self._connection._password,
        }

        self.ServerId = int(self.Config["server_id"])
        self.ProactorService = app.get_service("asab.ProactorService")

        self.ServerId = int(self.Config["server_id"])
        self.LogFile = self.Config["log_file"]
        self.LogPos = int(self.Config["log_pos"])
        self.AutoPosition = self.Config.getboolean("auto_position")
        self.ResumeStream = True

        if self.LogFile == "":
            self.LogFile = None
            self.LogPos = None
            self.ResumeStream = False

        extract_events = []
        lines = self.Config["extract_events"].split("\n")
        for line in lines:
            line_core = "".join(line.split())
            if len(line_core) == 0:
                continue
            extract_events.append(line_core)

        self.ExtractEvents = frozenset(extract_events)
        self.Loop = app.Loop
        self.Queue = asyncio.Queue(loop=self.Loop)
        self.Running = True
        self.Stream = None

    def stream_data(self):
        gtid = None
        if self.AutoPosition:
            cursor = self._connection.acquire_sync_cursor()
            cursor.execute("SELECT @@global.gtid_executed as gtid")
            row = cursor.fetchone()
            if row.get("gtid"):
                gtid = row.get("gtid")
                self.ResumeStream = True
                if self.LogFile is not None:
                    L.warning(
                        "Requested auto_position. Configuration log_file and log_pos is ignored."
                    )
            else:
                L.warning(
                    "MySQL server is not configured for GTID, auto_position cannot be used."
                )

        self.Stream = pymysqlreplication.BinLogStreamReader(
            connection_settings=self.MySQLSettings,
            server_id=self.ServerId,
            log_file=self.LogFile,
            log_pos=self.LogPos,
            resume_stream=self.ResumeStream,
            blocking=True,
            auto_position=gtid,
        )

        for binlogevent in self.Stream:
            if not self.Running:
                return False

            event = {}

            event_type = binlogevent.__class__.__name__

            if event_type not in self.ExtractEvents:
                continue

            # General info
            event["type"] = event_type
            event["@timestamp"] = binlogevent.timestamp
            event["log_position"] = binlogevent.packet.log_pos
            event["event_size"] = binlogevent.event_size
            event["read_bytes"] = binlogevent.packet.read_bytes

            if isinstance(binlogevent, pymysqlreplication.row_event.RowsEvent):
                event["table"] = binlogevent.table
                event["schema"] = binlogevent.schema
                event["primary_key"] = binlogevent.primary_key
                event["rows"] = binlogevent.rows

            # Specific info
            if event_type == "TableMapEvent":
                event["table_id"] = binlogevent.table_id
                event["schema"] = binlogevent.schema
                event["table"] = binlogevent.table
                event["columns"] = binlogevent.column_count

            if event_type == "GtidEvent":
                event["commit"] = binlogevent.commit_flag
                event["GTID_NEXT"] = binlogevent.gtid

            if event_type == "RotateEvent":
                event["position"] = binlogevent.position
                event["next_binlog_file"] = binlogevent.next_binlog

            if event_type == "XidEvent":
                event["transaction_id"] = binlogevent.xid

            if event_type == "HeartbeatLogEvent":
                event["current_binlog"] = binlogevent.ident

            if event_type == "QueryEvent":
                event["schema"] = binlogevent.schema
                event["execution_time"] = binlogevent.execution_time
                event["query"] = binlogevent.query

            if event_type == "BeginLoadQueryEvent":
                event["file_id"] = binlogevent.file_id
                event["block_data"] = binlogevent.block_data

            if event_type == "ExecuteLoadQueryEvent":
                event["slave_proxy_id"] = binlogevent.slave_proxy_id
                event["execution_time"] = binlogevent.execution_time
                event["schema_length"] = binlogevent.schema_length
                event["error_code"] = binlogevent.error_code
                event["status_vars_length"] = binlogevent.status_vars_length
                event["file_id"] = binlogevent.file_id
                event["start_pos"] = binlogevent.start_pos
                event["end_pos"] = binlogevent.end_pos
                event["dup_handling_flags"] = binlogevent.dup_handling_flags

            if event_type == "IntvarEvent":
                event["type"] = binlogevent.type
                event["value"] = binlogevent.value

            self.Queue.put_nowait(({}, event))
        return True

    async def main(self):
        worker = self.ProactorService.execute(self.stream_data)

        try:
            while True:
                await self.Pipeline.ready()
                context, event = await self.Queue.get()

                await self.process(event, context={})
                self.Queue.task_done()

        except asyncio.CancelledError:
            if self.Queue.qsize() > 0:
                self.Running = False
                L.warning(
                    "'{}' stopped with {} events in a queue".format(
                        self.locate_address(), self.Queue.qsize()
                    )
                )

        finally:
            self.Running = False
            worker.result()
