from __future__ import annotations

import json
import sqlite3
import time
import uuid
from typing import Any

from x8._common.sqlite_provider import SQLiteProvider
from x8.core import Context, DataModel, NCall, Operation, Response
from x8.core.exceptions import BadRequestError, ConflictError, NotFoundError
from x8.core.time import Time

from ._models import (
    DEFAULT_SUBSCRIPTION_NAME,
    MessageBatch,
    MessageItem,
    MessageKey,
    MessageProperties,
    MessagePullConfig,
    MessagePutConfig,
    MessageValueType,
    MessagingMode,
    QueueConfig,
    QueueInfo,
    SubscriptionConfig,
    SubscriptionInfo,
    TopicConfig,
    TopicInfo,
)
from ._operation import MessagingOperation
from ._operation_parser import MessagingOperationParser

DEFAULT_TTL = 7 * 24 * 60 * 60
DEFAULT_VISIBILITY_TIMEOUT = 30


class SQLiteBase(SQLiteProvider):
    mode: MessagingMode

    queue: str | None
    topic: str | None
    subscription: str | None

    database: str
    message_table: str
    metadata_table: str
    poll_interval: float
    nparams: dict[str, Any]

    _client: Any
    _client_helper: Any
    _op_converter: OperationConverter
    _result_converter: ResultConverter
    _topic_config_cache: dict[str, TopicConfig] = {}
    _subscription_config_cache: dict[
        str, dict[str, SubscriptionConfig | None]
    ] = {}

    def __init__(
        self,
        mode: MessagingMode = MessagingMode.QUEUE,
        database: str = ":memory:",
        queue: str | None = None,
        topic: str | None = None,
        subscription: str | None = None,
        message_table: str = "message",
        metadata_table: str = "metadata",
        poll_interval: float = 0.5,
        nparams: dict[str, Any] = dict(),
        **kwargs,
    ):
        """Initialize.

        Args:
            mode:
                Messaging mode.
            database:
                SQLite database. Defaults to ":memory:".
            queue:
                Queue name.
            topic:
                Topic name.
            subscription:
                Subscription name.
            message_table:
                SQLite table name for messages.
            metadata_table:
                SQLite table name for metadata.
            poll_interval:
                Poll interval in seconds. Defaults to 0.5.
            nparams:
                Native parameters to SQLite client.
        """
        self.mode = mode
        self.database = database
        self.queue = queue
        self.topic = topic
        self.subscription = subscription
        self.message_table = message_table
        self.metadata_table = metadata_table
        self.poll_interval = poll_interval
        self.nparams = nparams

        self._client = None
        self._client_helper = None
        self._op_converter = OperationConverter(
            self.message_table,
            self.metadata_table,
        )
        self._result_converter = ResultConverter()
        self._topic_config_cache = dict()
        self._subscription_config_cache = dict()
        if self.mode == MessagingMode.QUEUE:
            self.topic = self.queue
            self.subscription = DEFAULT_SUBSCRIPTION_NAME
        super().__init__(**kwargs)

    def __setup__(
        self,
        context: Context | None = None,
    ) -> None:
        if self._client is not None:
            return

        self._init_folder(self.database)
        self._client = sqlite3.connect(
            database=self.database,
            check_same_thread=False,
            **self.nparams,
        )
        self._client_helper = ClientHelper(
            self._client,
            self._op_converter,
            self._result_converter,
            self.mode,
            self.poll_interval,
            self._subscription_config_cache,
        )
        self._create_tables_if_needed()

    def _create_tables_if_needed(self, **kwargs: Any) -> None:
        cursor = self._client.cursor()
        rows = cursor.execute(
            """SELECT name FROM sqlite_master WHERE type='table'
                AND name=?""",
            (self.message_table,),
        ).fetchall()
        if len(rows) == 0:
            cursor.execute(
                f"""
                CREATE TABLE {self.message_table} (
                    id TEXT,
                    topic TEXT,
                    subscription TEXT,
                    value BLOB,
                    metadata TEXT,
                    message_id TEXT,
                    group_id TEXT,
                    content_type TEXT,
                    enqueued_time REAL,
                    delivery_count INTEGER,
                    lock_until_time REAL,
                    lock_token TEXT,
                    PRIMARY KEY (id, topic, subscription)
                )
                """
            )
        rows = cursor.execute(
            """SELECT name FROM sqlite_master WHERE type='table'
                AND name=?""",
            (self.metadata_table,),
        ).fetchall()
        if len(rows) == 0:
            cursor.execute(
                f"""
                CREATE TABLE {self.metadata_table} (
                    topic TEXT,
                    subscription TEXT,
                    config TEXT,
                    PRIMARY KEY (topic, subscription)
                )
                """
            )
        self._client.commit()
        cursor.close()

    def _get_topic_name(self, op_parser: MessagingOperationParser) -> str:
        if self.mode == MessagingMode.PUBSUB:
            topic_name = op_parser.get_topic()
            topic_name = topic_name or self.topic or self.__component__.topic
            if topic_name is None:
                raise BadRequestError(
                    "Topic name is required for this operation.",
                )
        else:
            topic_name = op_parser.get_queue()
            topic_name = topic_name or self.queue or self.__component__.queue
            if topic_name is None:
                raise BadRequestError(
                    "Queue name is required for this operation.",
                )
        return topic_name

    def _get_subscription_name(
        self, op_parser: MessagingOperationParser
    ) -> str:
        subscription_name = op_parser.get_subscription()
        subscription_name = (
            subscription_name
            or self.subscription
            or self.__component__.subscription
        )
        if subscription_name is None:
            raise BadRequestError(
                "Subscription name is required for this operation.",
            )
        return subscription_name

    def __run__(
        self,
        operation: Operation | None = None,
        context: Context | None = None,
        **kwargs,
    ) -> Any:
        self.__setup__(context=context)
        op_parser = MessagingOperationParser(operation)
        ncall, state = self._get_ncall(
            op_parser,
            self._client_helper,
        )
        if ncall is None:
            return super().__run__(
                operation,
                context,
                **kwargs,
            )
        nresult = ncall.invoke()
        result = self._convert_nresult(
            nresult,
            state,
            op_parser,
        )
        return Response(result=result, native=dict(result=nresult, call=ncall))

    def _get_ncall(
        self,
        op_parser: MessagingOperationParser,
        helper: ClientHelper,
    ) -> tuple[NCall | None, dict]:
        call = None
        state: dict = {}
        op_converter = self._op_converter
        # CREATE QUEUE
        if op_parser.op_equals(MessagingOperation.CREATE_QUEUE):
            args = op_converter.convert_create_queue(
                self._get_topic_name(op_parser),
                op_parser.get_queue_config(),
                op_parser.get_where_exists(),
            )
            call = NCall(
                helper.transact,
                args,
                None,
                {sqlite3.IntegrityError: ConflictError},
            )
        # UPDATE QUEUE
        elif op_parser.op_equals(MessagingOperation.UPDATE_QUEUE):
            args = op_converter.convert_update_queue(
                self._get_topic_name(op_parser),
                op_parser.get_queue_config(),
            )
            call = NCall(
                helper.execute,
                args,
                None,
            )
        # DROP QUEUE
        elif op_parser.op_equals(MessagingOperation.DROP_QUEUE):
            args = op_converter.convert_drop_topic(
                self._get_topic_name(op_parser),
                op_parser.get_where_exists(),
            )
            call = NCall(
                helper.execute,
                args,
            )
        # HAS QUEUE
        elif op_parser.op_equals(MessagingOperation.HAS_QUEUE):
            args = op_converter.convert_has_topic(
                self._get_topic_name(op_parser),
            )
            call = NCall(
                helper.execute,
                args,
                None,
            )
        # GET QUEUE
        elif op_parser.op_equals(MessagingOperation.GET_QUEUE):
            args = {
                "queue": self._get_topic_name(op_parser),
            }
            call = NCall(
                helper.get_queue,
                args,
            )
        # LIST QUEUES
        elif op_parser.op_equals(MessagingOperation.LIST_QUEUES):
            args = op_converter.convert_list_topics()
            call = NCall(
                helper.execute,
                args,
            )
        # CREATE TOPIC
        elif op_parser.op_equals(MessagingOperation.CREATE_TOPIC):
            args = op_converter.convert_create_topic(
                self._get_topic_name(op_parser),
                op_parser.get_topic_config(),
                op_parser.get_where_exists(),
            )
            call = NCall(
                helper.execute,
                args,
                None,
                {sqlite3.IntegrityError: ConflictError},
            )
        # UPDATE TOPIC
        elif op_parser.op_equals(MessagingOperation.UPDATE_TOPIC):
            args = op_converter.convert_update_topic(
                self._get_topic_name(op_parser),
                op_parser.get_topic_config(),
            )
            call = NCall(
                helper.execute,
                args,
                None,
            )
        # DROP TOPIC
        elif op_parser.op_equals(MessagingOperation.DROP_TOPIC):
            args = op_converter.convert_drop_topic(
                self._get_topic_name(op_parser),
                op_parser.get_where_exists(),
            )
            call = NCall(
                helper.execute,
                args,
            )
        # HAS TOPIC
        elif op_parser.op_equals(MessagingOperation.HAS_TOPIC):
            args = op_converter.convert_has_topic(
                self._get_topic_name(op_parser),
            )
            call = NCall(
                helper.execute,
                args,
                None,
            )
        # GET TOPIC
        elif op_parser.op_equals(MessagingOperation.GET_TOPIC):
            args = {
                "topic": self._get_topic_name(op_parser),
            }
            call = NCall(
                helper.get_topic,
                args,
            )
        # LIST TOPICS
        elif op_parser.op_equals(MessagingOperation.LIST_TOPICS):
            args = op_converter.convert_list_topics()
            call = NCall(
                helper.execute,
                args,
            )
        # CREATE SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.CREATE_SUBSCRIPTION):
            args = op_converter.convert_create_subscription(
                self._get_topic_name(op_parser),
                self._get_subscription_name(op_parser),
                op_parser.get_subscription_config(),
                op_parser.get_where_exists(),
            )
            call = NCall(
                helper.execute,
                args,
                None,
                {sqlite3.IntegrityError: ConflictError},
            )
        # UPDATE SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.UPDATE_SUBSCRIPTION):
            args = op_converter.convert_update_subscription(
                self._get_topic_name(op_parser),
                self._get_subscription_name(op_parser),
                op_parser.get_subscription_config(),
            )
            call = NCall(
                helper.execute,
                args,
                None,
            )
        # DROP SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.DROP_SUBSCRIPTION):
            args = op_converter.convert_drop_subscription(
                self._get_topic_name(op_parser),
                self._get_subscription_name(op_parser),
                op_parser.get_where_exists(),
            )
            call = NCall(
                helper.execute,
                args,
            )
        # HAS SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.HAS_SUBSCRIPTION):
            args = op_converter.convert_has_subscription(
                self._get_topic_name(op_parser),
                self._get_subscription_name(op_parser),
            )
            call = NCall(
                helper.execute,
                args,
                None,
            )
        # GET SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.GET_SUBSCRIPTION):
            args = {
                "topic": self._get_topic_name(op_parser),
                "subscription": self._get_subscription_name(op_parser),
            }
            call = NCall(
                helper.get_subscription,
                args,
            )
        # LIST SUBSCRIPTIONS
        elif op_parser.op_equals(MessagingOperation.LIST_SUBSCRIPTIONS):
            args = op_converter.convert_list_subscriptions(
                self._get_topic_name(op_parser),
            )
            call = NCall(
                helper.execute,
                args,
            )
        # PUT
        elif op_parser.op_equals(MessagingOperation.PUT):
            args = {
                "topic": self._get_topic_name(op_parser),
                "value": op_parser.get_value(),
                "metadata": op_parser.get_metadata(),
                "properties": op_parser.get_properties(),
                "config": op_parser.get_put_config(),
            }
            call = NCall(
                helper.put,
                args,
            )
        # BATCH
        elif op_parser.op_equals(MessagingOperation.BATCH):
            args = {
                "topic": self._get_topic_name(op_parser),
                "batch": op_parser.get_batch(),
            }
            call = NCall(
                helper.batch,
                args,
            )
        # PULL
        elif op_parser.op_equals(MessagingOperation.PULL):
            args = {
                "topic": self._get_topic_name(op_parser),
                "subscription": self._get_subscription_name(op_parser),
                "config": op_parser.get_pull_config(),
            }
            call = NCall(
                helper.pull,
                args,
            )
        # ACK
        elif op_parser.op_equals(MessagingOperation.ACK):
            args = op_converter.convert_ack(
                self._get_topic_name(op_parser),
                self._get_subscription_name(op_parser),
                op_parser.get_key(),
            )
            call = NCall(
                helper.execute,
                args,
            )
        # NACK
        elif op_parser.op_equals(MessagingOperation.NACK):
            args = op_converter.convert_nack(
                self._get_topic_name(op_parser),
                self._get_subscription_name(op_parser),
                op_parser.get_key(),
            )
            call = NCall(
                helper.execute,
                args,
            )
        # EXTEND
        elif op_parser.op_equals(MessagingOperation.EXTEND):
            args = {
                "topic": self._get_topic_name(op_parser),
                "subscription": self._get_subscription_name(op_parser),
                "key": op_parser.get_key(),
                "timeout": op_parser.get_timeout(),
            }
            call = NCall(
                helper.extend,
                args,
            )
        # PURGE
        elif op_parser.op_equals(MessagingOperation.PURGE):
            args = op_converter.convert_purge(
                self._get_topic_name(op_parser),
                self._get_subscription_name(op_parser),
            )
            call = NCall(
                helper.execute,
                args,
            )
        # CLOSE
        elif op_parser.op_equals(MessagingOperation.CLOSE):
            args = {
                "topic": op_parser.get_topic() or op_parser.get_queue(),
            }
            call = NCall(helper.close, args)
        return call, state

    def _convert_nresult(
        self,
        nresult: Any,
        state: dict,
        op_parser: MessagingOperationParser,
    ) -> Any:
        result: Any = None
        result_converter = self._result_converter
        # CREATE QUEUE
        if op_parser.op_equals(MessagingOperation.CREATE_QUEUE):
            result = None
        # UPDATE QUEUE
        elif op_parser.op_equals(MessagingOperation.UPDATE_QUEUE):
            if nresult == 0:
                raise NotFoundError(
                    "Queue not found.",
                )
            result = None
        # DROP QUEUE
        elif op_parser.op_equals(MessagingOperation.DROP_QUEUE):
            where_exists = op_parser.get_where_exists()
            if nresult == 0 and where_exists is True:
                raise NotFoundError
            result = None
        # HAS QUEUE
        elif op_parser.op_equals(MessagingOperation.HAS_QUEUE):
            return len(nresult) > 0
        # GET QUEUE
        elif op_parser.op_equals(MessagingOperation.GET_QUEUE):
            result = nresult
        # LIST QUEUES
        elif op_parser.op_equals(MessagingOperation.LIST_QUEUES):
            result = result_converter.convert_list_topics(nresult)
        # CREATE TOPIC
        elif op_parser.op_equals(MessagingOperation.CREATE_TOPIC):
            result = None
        # UPDATE TOPIC
        elif op_parser.op_equals(MessagingOperation.UPDATE_TOPIC):
            if nresult == 0:
                raise NotFoundError(
                    "Topic not found.",
                )
            result = None
        # DROP TOPIC
        elif op_parser.op_equals(MessagingOperation.DROP_TOPIC):
            where_exists = op_parser.get_where_exists()
            if nresult == 0 and where_exists is True:
                raise NotFoundError
            result = None
        # HAS TOPIC
        elif op_parser.op_equals(MessagingOperation.HAS_TOPIC):
            return len(nresult) > 0
        # GET TOPIC
        elif op_parser.op_equals(MessagingOperation.GET_TOPIC):
            result = nresult
        # LIST TOPICS
        elif op_parser.op_equals(MessagingOperation.LIST_TOPICS):
            result = result_converter.convert_list_topics(nresult)
        # CREATE SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.CREATE_SUBSCRIPTION):
            result = None
        # UPDATE SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.UPDATE_SUBSCRIPTION):
            if nresult == 0:
                raise NotFoundError(
                    "Subscription not found.",
                )
            result = None
        # DROP SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.DROP_SUBSCRIPTION):
            where_exists = op_parser.get_where_exists()
            if nresult == 0 and where_exists is True:
                raise NotFoundError
            result = None
        # HAS SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.HAS_SUBSCRIPTION):
            return len(nresult) > 0
        # GET SUBSCRIPTION
        elif op_parser.op_equals(MessagingOperation.GET_SUBSCRIPTION):
            result = nresult
        # LIST SUBSCRIPTIONS
        elif op_parser.op_equals(MessagingOperation.LIST_SUBSCRIPTIONS):
            result = result_converter.convert_list_subscriptions(nresult)
        # PUT
        elif op_parser.op_equals(MessagingOperation.PUT):
            result = None
        # BATCH
        elif op_parser.op_equals(MessagingOperation.BATCH):
            result = None
        # PULL
        elif op_parser.op_equals(MessagingOperation.PULL):
            result = nresult
        # ACK
        elif op_parser.op_equals(MessagingOperation.ACK):
            if nresult == 0:
                raise BadRequestError(
                    "Message not found or already acknowledged.",
                )
            result = None
        # NACK
        elif op_parser.op_equals(MessagingOperation.NACK):
            if nresult == 0:
                raise BadRequestError(
                    "Message not found or already acknowledged.",
                )
            result = None
        # EXTEND
        elif op_parser.op_equals(MessagingOperation.EXTEND):
            if nresult == 0:
                raise BadRequestError(
                    "Message not found or already acknowledged.",
                )
            result = MessageItem(key=op_parser.get_key())
        # PURGE
        elif op_parser.op_equals(MessagingOperation.PURGE):
            result = None
        # CLOSE
        elif op_parser.op_equals(MessagingOperation.CLOSE):
            pass
        return result


class ClientHelper:
    client: Any
    op_converter: OperationConverter
    result_converter: ResultConverter
    mode: MessagingMode
    poll_interval: float
    subscription_config_cache: dict[str, dict[str, SubscriptionConfig | None]]

    def __init__(
        self,
        client: Any,
        op_converter: OperationConverter,
        result_converter: ResultConverter,
        mode: MessagingMode,
        poll_interval: float,
        subscription_config_cache: dict[
            str, dict[str, SubscriptionConfig | None]
        ],
    ):
        self.client = client
        self.op_converter = op_converter
        self.result_converter = result_converter
        self.mode = mode
        self.poll_interval = poll_interval
        self.subscription_config_cache = subscription_config_cache

    def _get_subscription_config(
        self,
        topic_name: str,
        subscription_name: str,
    ) -> SubscriptionConfig | None:
        if (
            topic_name in self.subscription_config_cache
            and subscription_name in self.subscription_config_cache[topic_name]
        ):
            return self.subscription_config_cache[topic_name][
                subscription_name
            ]
        args = self.op_converter.convert_get_subscription_config(
            topic_name,
            subscription_name,
        )
        nresult = self.execute(
            args["query"],
            args["params"],
            fetchall=True,
        )
        if nresult is None or len(nresult) == 0:
            raise BadRequestError(
                f"Subscription {subscription_name} not found.",
            )
        if nresult[0][0] is None:
            config = None
        else:
            config = SubscriptionConfig.from_dict(json.loads(nresult[0][0]))
        if topic_name not in self.subscription_config_cache:
            self.subscription_config_cache[topic_name] = {}
        self.subscription_config_cache[topic_name][subscription_name] = config
        return config

    def get_queue(self, queue: str) -> QueueInfo:
        sinfo = self.get_subscription(queue, DEFAULT_SUBSCRIPTION_NAME)
        return QueueInfo(
            name=queue,
            active_message_count=sinfo.active_message_count,
            inflight_message_count=sinfo.inflight_message_count,
            scheduled_message_count=sinfo.scheduled_message_count,
            config=(
                QueueConfig.from_dict(sinfo.config.to_dict())
                if sinfo.config
                else None
            ),
            nref=queue,
        )

    def get_topic(self, topic: str) -> TopicInfo:
        args = self.op_converter.convert_get_topic_config(topic)
        nresult = self.execute(args["query"], args["params"])
        if nresult is None:
            raise NotFoundError
        config_json = nresult[0]
        args = self.op_converter.convert_get_subscription_count(topic)
        subscription_count = self.execute(
            args["query"], args["params"], fetchall=True
        )[0][0]
        return TopicInfo(
            name=topic,
            subscription_count=subscription_count - 1,
            config=(
                TopicConfig.from_dict(json.loads(config_json))
                if config_json
                else None
            ),
            nref=topic,
        )

    def get_subscription(
        self, topic: str, subscription: str
    ) -> SubscriptionInfo:
        args = self.op_converter.convert_get_subscription_config(
            topic, subscription
        )
        nresult = self.execute(args["query"], args["params"])
        if nresult is None:
            raise NotFoundError
        config_json = nresult[0]
        args = self.op_converter.convert_get_active_message_count(
            topic, subscription
        )
        active_message_count = self.execute(
            args["query"], args["params"], fetchall=True
        )[0][0]
        args = self.op_converter.convert_get_inflight_message_count(
            topic, subscription
        )
        inflight_message_count = self.execute(
            args["query"], args["params"], fetchall=True
        )[0][0]
        args = self.op_converter.convert_get_scheduled_message_count(
            topic, subscription
        )
        scheduled_message_count = self.execute(
            args["query"], args["params"], fetchall=True
        )[0][0]
        return SubscriptionInfo(
            name=subscription,
            topic=topic,
            active_message_count=active_message_count,
            inflight_message_count=inflight_message_count,
            scheduled_message_count=scheduled_message_count,
            config=(
                SubscriptionConfig.from_dict(json.loads(config_json))
                if config_json
                else None
            ),
            nref=subscription,
        )

    def put(
        self,
        topic: str,
        value: MessageValueType,
        metadata: dict | None = None,
        properties: MessageProperties | None = None,
        config: MessagePutConfig | None = None,
    ) -> None:
        args = self.op_converter.convert_list_subscriptions(topic)
        nresult = self.client.execute(args["query"], args["params"]).fetchall()
        subscriptions = self.result_converter.convert_list_subscriptions(
            nresult
        )
        if len(subscriptions) == 0:
            if self.mode == MessagingMode.QUEUE:
                raise BadRequestError(
                    f"Queue {topic} not found.",
                )
        if properties and properties.message_id:
            id = properties.message_id
        else:
            id = str(uuid.uuid4())
        enqueue_time = Time.now()
        if config and config.delay:
            enqueue_time += config.delay
        ops: list = []
        for subscription in subscriptions:
            args = self.op_converter.convert_put(
                id,
                topic,
                subscription,
                enqueue_time,
                value,
                metadata,
                properties,
            )
            ops.append(args)
        self.transact(ops=ops)

    def batch(
        self,
        topic: str,
        batch: MessageBatch,
    ) -> None:
        for operation in batch.operations:
            op_parser = MessagingOperationParser(operation)
            self.put(
                topic=topic,
                value=op_parser.get_value(),
                metadata=op_parser.get_metadata(),
                properties=op_parser.get_properties(),
                config=op_parser.get_put_config(),
            )

    def pull(
        self,
        topic: str,
        subscription: str,
        config: MessagePullConfig | None = None,
    ) -> list[MessageItem]:
        subscription_config = self._get_subscription_config(
            topic,
            subscription,
        )
        result: list[MessageItem] = []
        start_time = Time.now()
        while True:
            now = Time.now()
            found = False
            with self.client:
                args = self.op_converter.convert_pull(
                    topic,
                    subscription,
                    subscription_config=subscription_config,
                )
                nresult = self.client.execute(
                    args["query"], args["params"]
                ).fetchone()
                if nresult is not None:
                    lock_token = str(uuid.uuid4())
                    message = self.result_converter.convert_pull(nresult)
                    if message.key is None or message.key.id is None:
                        raise BadRequestError(
                            "Message key is missing.",
                        )
                    lock_duration = (
                        config.visibility_timeout if config else None
                    )
                    lock_duration = lock_duration or (
                        subscription_config.visibility_timeout
                        if subscription_config
                        and subscription_config.visibility_timeout
                        else DEFAULT_VISIBILITY_TIMEOUT
                    )
                    lock_until_time = now + lock_duration
                    args = self.op_converter.convert_lock(
                        message.key.id,
                        topic,
                        subscription,
                        lock_until_time,
                        lock_token,
                        message.key.nref,
                    )
                    updated = self.client.execute(
                        args["query"], args["params"]
                    ).rowcount
                    if updated == 1:
                        message.key.nref = lock_token
                        result.append(message)
                        found = True
            if config and config.max_wait_time:
                if now - start_time > config.max_wait_time:
                    break
            if config and config.max_count:
                if len(result) >= config.max_count:
                    break
            if not config or not config.max_count:
                if len(result) > 0:
                    break
            if not found:
                time.sleep(self.poll_interval)
        return result

    def extend(
        self,
        topic: str,
        subscription: str,
        key: MessageKey,
        timeout: int | None = None,
    ) -> None:
        subscription_config = self._get_subscription_config(
            topic, subscription
        )
        args = self.op_converter.convert_extend(
            topic,
            subscription,
            key,
            timeout,
            subscription_config,
        )
        self.client.execute(args["query"], args["params"])

    def execute(
        self,
        query: str,
        params: tuple | None = None,
        rowcount: bool = False,
        fetchall: bool = False,
    ) -> Any:
        cursor = self.client.cursor()
        try:
            if params:
                cursor.execute(query, params)
            else:
                cursor.execute(query)
            if rowcount:
                return cursor.rowcount
            if fetchall:
                return cursor.fetchall()
            return cursor.fetchone()
        finally:
            self.client.commit()
            cursor.close()

    def transact(self, ops: list) -> Any:
        result = []
        cursor = self.client.cursor()
        try:
            for op in ops:
                if "params" in op:
                    cursor.execute(op["query"], op["params"])
                else:
                    cursor.execute(op["query"])
                if "rowcount" in op and op["rowcount"]:
                    if cursor.rowcount == 0:
                        self.client.rollback()
                        raise ConflictError
                    result.append(cursor.rowcount)
                else:
                    res = cursor.fetchone()
                    if res is None or len(res) == 0:
                        self.client.rollback()
                        raise ConflictError
                    result.append(res)
            self.client.commit()
        finally:
            cursor.close()
        return result

    def close(self, topic: str | None) -> Any:
        if topic is not None:
            return
        self.client.close()


class OperationConverter:
    message_table: str
    metadata_table: str

    META_SUBSCRIPTION_NAME = "#"

    def __init__(
        self,
        message_table: str,
        metadata_table: str,
    ):
        self.message_table = message_table
        self.metadata_table = metadata_table

    def convert_get_active_message_count(
        self,
        topic: str,
        subscription: str,
    ) -> dict:
        current_time = Time.now()
        query = f"""
            SELECT COUNT(*)
            FROM {self.message_table}
            WHERE topic = ? AND subscription = ?
            AND lock_until_time < ?
            AND enqueued_time <= ?
            """
        params = (
            topic,
            subscription,
            current_time,
            current_time,
        )
        return {
            "query": query,
            "params": params,
            "fetchall": True,
        }

    def convert_get_inflight_message_count(
        self,
        topic: str,
        subscription: str,
    ) -> dict:
        current_time = Time.now()
        query = f"""
            SELECT COUNT(*)
            FROM {self.message_table}
            WHERE topic = ? AND subscription = ?
            AND lock_until_time >= ?
            """
        params = (
            topic,
            subscription,
            current_time,
        )
        return {
            "query": query,
            "params": params,
            "fetchall": True,
        }

    def convert_get_scheduled_message_count(
        self,
        topic: str,
        subscription: str,
    ) -> dict:
        current_time = Time.now()
        query = f"""
            SELECT COUNT(*)
            FROM {self.message_table}
            WHERE topic = ? AND subscription = ?
            AND enqueued_time > ?
            """
        params = (
            topic,
            subscription,
            current_time,
        )
        return {
            "query": query,
            "params": params,
            "fetchall": True,
        }

    def convert_get_subscription_count(
        self,
        topic: str,
    ) -> dict:
        query = f"""
            SELECT COUNT(*)
            FROM {self.metadata_table}
            WHERE topic = ?
            """
        params = (topic,)
        return {
            "query": query,
            "params": params,
            "fetchall": True,
        }

    def convert_get_topic_config(
        self,
        topic: str,
    ) -> dict:
        query = f"""
            SELECT config
            FROM {self.metadata_table}
            WHERE topic = ? AND subscription = ?
            """
        params = (topic, OperationConverter.META_SUBSCRIPTION_NAME)
        return {
            "query": query,
            "params": params,
        }

    def convert_get_subscription_config(
        self,
        topic: str,
        subscription: str,
    ) -> dict:
        query = f"""
            SELECT config
            FROM {self.metadata_table}
            WHERE topic = ? AND subscription = ?
            """
        params = (topic, subscription)
        return {
            "query": query,
            "params": params,
        }

    def convert_create_queue(
        self,
        queue: str,
        config: QueueConfig | None,
        where_exists: bool | None,
    ) -> dict:
        ops: list = []
        args = self.convert_create_topic(
            queue,
            None,
            where_exists,
        )
        ops.append(args)
        args = self.convert_create_subscription(
            queue,
            DEFAULT_SUBSCRIPTION_NAME,
            config,
            where_exists,
        )
        ops.append(args)
        return {"ops": ops}

    def convert_update_queue(
        self,
        queue: str,
        config: QueueConfig | None,
    ) -> dict:
        return self.convert_update_subscription(
            queue,
            DEFAULT_SUBSCRIPTION_NAME,
            config,
        )

    def convert_create_topic(
        self,
        topic: str,
        config: QueueConfig | TopicConfig | None,
        where_exists: bool | None,
    ) -> dict:
        if where_exists is False:
            query = f"""
                INSERT INTO {self.metadata_table}
                (topic, subscription, config)
                VALUES (?, ?, ?)
                """
        else:
            query = f"""
                INSERT INTO {self.metadata_table}
                (topic, subscription, config)
                VALUES (?, ?, ?)
                ON CONFLICT (topic, subscription)
                DO UPDATE SET
                config = EXCLUDED.config
                """
        params = (
            topic,
            OperationConverter.META_SUBSCRIPTION_NAME,
            config.to_json() if config else None,
        )
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_update_topic(
        self,
        topic: str,
        config: TopicConfig | None,
    ) -> dict:
        query = f"""
            UPDATE {self.metadata_table}
            SET config = ?
            WHERE topic = ? AND subscription = ?
            """
        params = (
            config.to_json() if config else None,
            topic,
            OperationConverter.META_SUBSCRIPTION_NAME,
        )
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_drop_topic(
        self,
        topic: str,
        where_exists: bool | None,
    ) -> dict:
        query = f"""
            DELETE FROM {self.metadata_table}
            WHERE topic = ?
            """
        params = (topic,)
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_has_topic(
        self,
        topic: str,
    ) -> dict:
        query = f"""
            SELECT topic
            FROM {self.metadata_table}
            WHERE topic = ?
            """
        params = (topic,)
        return {
            "query": query,
            "params": params,
            "fetchall": True,
        }

    def convert_list_topics(self) -> dict:
        query = f"""
            SELECT topic
            FROM {self.metadata_table}
            WHERE subscription = ?
            """
        params = (OperationConverter.META_SUBSCRIPTION_NAME,)
        return {
            "query": query,
            "params": params,
            "fetchall": True,
        }

    def convert_create_subscription(
        self,
        topic: str,
        subscription: str,
        config: SubscriptionConfig | QueueConfig | None,
        where_exists: bool | None,
    ) -> dict:
        if where_exists is False:
            query = f"""
                INSERT INTO {self.metadata_table}
                (topic, subscription, config)
                VALUES (?, ?, ?)
                """
        else:
            query = f"""
                INSERT INTO {self.metadata_table}
                (topic, subscription, config)
                VALUES (?, ?, ?)
                ON CONFLICT (topic, subscription)
                DO UPDATE SET
                config = EXCLUDED.config
                """
        params = (
            topic,
            subscription,
            config.to_json() if config else None,
        )
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_update_subscription(
        self,
        topic: str,
        subscription: str,
        config: SubscriptionConfig | QueueConfig | None,
    ) -> dict:
        query = f"""
            UPDATE {self.metadata_table}
            SET config = ?
            WHERE topic = ? AND subscription = ?
            """
        params = (
            config.to_json() if config else None,
            topic,
            subscription,
        )
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_drop_subscription(
        self,
        topic: str,
        subscription: str,
        where_exists: bool | None,
    ) -> dict:
        query = f"""
            DELETE FROM {self.metadata_table}
            WHERE topic = ? AND subscription = ?
            """
        params = (topic, subscription)
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_has_subscription(
        self,
        topic: str,
        subscription: str,
    ) -> dict:
        query = f"""
            SELECT topic, subscription
            FROM {self.metadata_table}
            WHERE topic = ? AND subscription = ?
            """
        params = (topic, subscription)
        return {
            "query": query,
            "params": params,
            "fetchall": True,
        }

    def convert_list_subscriptions(self, topic: str) -> dict:
        query = f"""
            SELECT subscription
            FROM {self.metadata_table}
            WHERE topic = ?
            AND subscription != ?
            """
        params = (
            topic,
            OperationConverter.META_SUBSCRIPTION_NAME,
        )
        return {
            "query": query,
            "params": params,
            "fetchall": True,
        }

    def convert_put(
        self,
        id: str,
        topic: str,
        subscription: str,
        enqueue_time: float,
        value: MessageValueType,
        metadata: dict | None,
        properties: MessageProperties | None,
    ) -> dict:
        message_id = None
        content_type = None
        group_id = None
        body: bytes | None = None
        if properties is not None:
            message_id = properties.message_id
            content_type = properties.content_type
            group_id = properties.group_id
        if isinstance(value, str):
            body = value.encode("utf-8")
            content_type = "text/plain"
        elif isinstance(value, bytes):
            body = value
            content_type = "application/octet-stream"
        elif isinstance(value, dict):
            body = json.dumps(value).encode("utf-8")
            content_type = "application/json"
        elif isinstance(value, DataModel):
            body = value.to_json().encode("utf-8")
            content_type = "application/json"
        else:
            raise BadRequestError("Message type not supported")
        query = f"""
            INSERT INTO {self.message_table}
            (id, topic, subscription, value, metadata,
            message_id, group_id, content_type,
            enqueued_time, delivery_count,
            lock_until_time, lock_token)
            VALUES (?, ?, ?, ?, ?,
            ?, ?, ?, ?, ?, ?, ?)
            """
        params = (
            id,
            topic,
            subscription,
            body,
            json.dumps(metadata) if metadata else None,
            message_id,
            group_id,
            content_type,
            enqueue_time,
            0,
            0,
            "",
        )
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_pull(
        self,
        topic: str,
        subscription: str,
        subscription_config: SubscriptionConfig | None = None,
    ) -> dict:
        ttl = (
            subscription_config.ttl
            if subscription_config and subscription_config.ttl
            else DEFAULT_TTL
        )
        current_time = Time.now()
        query = f"""
            SELECT id, topic, subscription, value, metadata,
            message_id, group_id, content_type,
            enqueued_time, delivery_count,
            lock_until_time, lock_token
            FROM {self.message_table}
            WHERE topic = ? AND subscription = ?
            AND lock_until_time <= ?
            AND enqueued_time <= ?
            AND enqueued_time >= ?
            ORDER BY enqueued_time
            LIMIT 1
            """
        params = (
            topic,
            subscription,
            current_time,
            current_time,
            current_time - ttl,
        )
        return {
            "query": query,
            "params": params,
        }

    def convert_lock(
        self,
        id: str,
        topic: str,
        subscription: str,
        lock_until_time: float,
        lock_token: str,
        previous_lock_token: str | None,
    ) -> dict:
        query = f"""
            UPDATE {self.message_table}
            SET lock_until_time = ?, lock_token = ?,
            delivery_count = delivery_count + 1
            WHERE topic = ? AND subscription = ?
            AND id = ? AND lock_token = ?
            """
        params = (
            lock_until_time,
            lock_token,
            topic,
            subscription,
            id,
            previous_lock_token,
        )
        return {
            "query": query,
            "params": params,
        }

    def convert_ack(
        self,
        topic: str,
        subscription: str,
        key: MessageKey,
    ) -> dict:
        query = f"""
            DELETE FROM {self.message_table}
            WHERE topic = ? AND subscription = ?
            AND lock_token = ?
            """
        params = (topic, subscription, key.nref)
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_nack(
        self,
        topic: str,
        subscription: str,
        key: MessageKey,
    ) -> dict:
        query = f"""
            UPDATE {self.message_table}
            SET lock_until_time = ?, lock_token = ?
            WHERE topic = ? AND subscription = ?
            AND lock_token = ?
            """
        params = (0, "", topic, subscription, key.nref)
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_extend(
        self,
        topic: str,
        subscription: str,
        key: MessageKey,
        timeout: int | None,
        subscription_config: SubscriptionConfig | None = None,
    ) -> dict:
        lock_duration = (
            subscription_config.visibility_timeout
            if subscription_config and subscription_config.visibility_timeout
            else DEFAULT_VISIBILITY_TIMEOUT
        )
        t = timeout or lock_duration
        t = Time.now() + t
        query = f"""
            UPDATE {self.message_table}
            SET lock_until_time = ?
            WHERE topic = ? AND subscription = ?
            AND lock_token = ?
            """
        params = (t, topic, subscription, key.nref)
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }

    def convert_purge(
        self,
        topic: str,
        subscription: str,
    ) -> dict:
        query = f"""
            DELETE FROM {self.message_table}
            WHERE topic = ? AND subscription = ?
            """
        params = (topic, subscription)
        return {
            "query": query,
            "params": params,
            "rowcount": True,
        }


class ResultConverter:
    def __init__(self):
        pass

    def _convert_value(
        self,
        value: bytes,
        content_type: str | None,
    ) -> MessageValueType:
        if content_type == "text/plain":
            return value.decode("utf-8")
        elif content_type == "application/octet-stream":
            return value
        elif content_type == "application/json":
            return json.loads(value.decode("utf-8"))
        return value

    def convert_list_topics(self, nresult: Any) -> list[str]:
        topics: list[str] = []
        for row in nresult:
            topics.append(row[0])
        return topics

    def convert_list_subscriptions(self, nresult: Any) -> list[str]:
        subscriptions: list[str] = []
        for row in nresult:
            subscriptions.append(row[0])
        return subscriptions

    def convert_pull(
        self,
        nresult: Any,
    ) -> MessageItem:
        id = nresult[0]
        value = nresult[3]
        metadata = nresult[4]
        message_id = nresult[5]
        group_id = nresult[6]
        content_type = nresult[7]
        enqueued_time = nresult[8]
        delivery_count = nresult[9]
        lock_token = nresult[11]

        message = MessageItem(
            key=MessageKey(id=id, nref=lock_token),
            value=self._convert_value(
                value,
                content_type,
            ),
            metadata=json.loads(metadata) if metadata else None,
            properties=MessageProperties(
                message_id=message_id,
                group_id=group_id,
                content_type=content_type,
                enqueued_time=enqueued_time,
                delivery_count=delivery_count + 1,
            ),
        )
        return message
