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()
