"""
methods to create, serialize and deserialize DataMessage objects.
"""

import base64
import json
import re
from datetime import datetime, timezone
from io import BytesIO
from typing import Any, Dict, List

import amqp.adapter.file_handler
from amqp.adapter.file_handler import FileHandler, StreamingFileHandler
from amqp.adapter.pydantic_serializer import python_type_to_json
from amqp.adapter.serializer import (
    map_as_string,
    map_with_list_as_string,
    parse_map_list_string,
    parse_map_string,
)
from amqp.model.model import DataMessage, DataMessageMagicByte
from amqp.model.snowflake_id import SnowflakeId
from amqp.router.utils import sanitize_as_rabbitmq_name


class DataMessageFactory:
    SNOWFLAKE_ID_BITS = 80
    SEQUENCE_BITS = 12
    MACHINE_ID_BITS = 24
    TIMESTAMP_BITS = SNOWFLAKE_ID_BITS - SEQUENCE_BITS - MACHINE_ID_BITS

    MACHINE_ID_MASK = (1 << MACHINE_ID_BITS) - 1
    SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1

    _instance = None
    snowflake_epoch = datetime(2024, 10, 1, 0, 0, tzinfo=timezone.utc).timestamp()

    def __init__(self, machine_id: int):
        self.machine_id = (machine_id & self.MACHINE_ID_MASK) << self.SEQUENCE_BITS
        self.snowflake_sequence = 0
        self.snowflake_last_timestamp = 0

    @classmethod
    def get_instance(cls, machine_id: int):
        """
        return reusable factory instance
        :param machine_id: a generator ID, should be unique for each component to prevent duplicate IDs
        :return: factory instance
        """
        if cls._instance is None:
            cls._instance = DataMessageFactory(machine_id)
        return cls._instance

    def generate_snowflake_id(self) -> SnowflakeId:
        """
        Generate an unique ID following the SnowflakeID structure, see https://en.wikipedia.org/wiki/Snowflake_ID
        :return: 80-bit wide ID
        """
        timestamp = int(datetime.now(timezone.utc).timestamp() * 1000)

        if timestamp < self.snowflake_last_timestamp:
            raise RuntimeError("Clock moved backwards. Refusing to generate id")

        if timestamp == self.snowflake_last_timestamp:
            self.snowflake_sequence = (self.snowflake_sequence + 1) & self.SEQUENCE_MASK
            if self.snowflake_sequence == 0:
                while timestamp == self.snowflake_last_timestamp:
                    timestamp = int(datetime.now(timezone.utc).timestamp() * 1000)
                self.snowflake_last_timestamp = timestamp
        else:
            self.snowflake_sequence = 0
            self.snowflake_last_timestamp = timestamp

        time = timestamp - 1000 * int(DataMessageFactory.snowflake_epoch)
        return SnowflakeId(
            time >> (64 - self.SEQUENCE_BITS - self.MACHINE_ID_BITS),
            (
                (time << (self.SEQUENCE_BITS + self.MACHINE_ID_BITS))
                | self.machine_id
                | self.snowflake_sequence
            )
            & 0xFFFFFFFFFFFFFFFF,
        )

    def create_request_message(
        self,
        method: str,
        domain: str,
        path: str,
        headers: Dict[str, List[str]],
        message,
    ) -> DataMessage:
        """
        Encapsulate / hide the system level inputs, create the AMQ message from business relevant input
        :param method: a method requested (e.g. HTTP method like GET, POST in case of REST request)
        :param domain: service host name
        :param path: path at the service
        :param headers: HTTP headers
        :param message: payload
        :return: DataMessage object
        """
        serializable_headers = headers.copy() if headers else {}
        payload = base64.b64encode(message.encode("utf-8") if isinstance(message, str) else message)
        trace_info = {}
        return DataMessage(
            DataMessageMagicByte.HTTP_REQUEST.value,
            self.generate_snowflake_id(),
            method,
            domain,
            path,
            serializable_headers,
            trace_info,
            payload,
        )

    def create_rpc_message(
        self,
        fq_method_name: str,
        swarm_id: str,
        args: Dict[str, Any] = None,
    ) -> DataMessage:
        """
        Create a RPC message with the given parameters.
        :param fq_method_name: fully qualified method name (e.g. 'com.example.ServiceClass.method_name')
        :param args: arguments whose serialized form will form the request body
        :return: DataMessage object
        """
        package, class_name, method = fq_method_name.rsplit(".", 2)
        headers = {"Content-Type": ["application/octet-stream"], "Return-Address": [swarm_id]}
        trace_info = {}
        return DataMessage(
            DataMessageMagicByte.RPC_REQUEST.value,
            self.generate_snowflake_id(),
            package,
            class_name,
            method,
            headers,
            trace_info,
            python_type_to_json(args) if args else b"",
        )

    @staticmethod
    def amq_message_routing_key(message: DataMessage) -> str:
        """
        Constructs the message specific routing key.
        :param message: DataMessage input
        :return: routing key (as string) compliant to RabbitMQ allowed character set.
        """
        return sanitize_as_rabbitmq_name(f"{message.domain()}.{message.path()}")

    @staticmethod
    def get_timestamp_from_id(_id: SnowflakeId) -> int:
        """
        Helper method - retrieve the timestamp from the ID
        :param _id: SnowflakeID (includes encoded timestamp)
        :return: the message timestamp
        """
        bits = _id.get_bits()
        low = bits[0] >> (DataMessageFactory.SEQUENCE_BITS + DataMessageFactory.MACHINE_ID_BITS)
        high = bits[1] << (
            64 - DataMessageFactory.SEQUENCE_BITS - DataMessageFactory.MACHINE_ID_BITS
        )
        return (high | low) + DataMessageFactory.snowflake_epoch * 1000

    @staticmethod
    def to_string(message: DataMessage) -> str:
        """
        Human-readable representation.
        :param message: DataMessage input
        :return: as string
        """
        return (
            f"DataMessage{{"
            f"id={message.id()}, "
            f"method='{message.method()}', "
            f"domain='{message.domain()}', "
            f"path='{message.path()}', "
            f"headers={{{map_with_list_as_string(message.headers())}}}, "
            f"traceInfo={{{map_as_string(message.trace_info())}}}, "
            f"base64body={message.base64body().decode()}"
            f"}}"
        )

    @staticmethod
    def serialize(message: DataMessage) -> bytes:
        """
        Converts DataMessage to byte array
        :param message: message to serialize
        :return: messages as bytes
        """
        id_bytes = str(message.id()).encode()
        prolog = (
            f"|m={message.method()}|d={message.domain()}|p={message.path()}|h={{{map_with_list_as_string(message.headers())}}}|t={{{map_as_string(message.trace_info())}}}|b="
        ).encode("utf-8")
        header_length = 1 + len(id_bytes) + len(prolog)
        prolog_len = header_length.to_bytes(2, byteorder="big")
        msg_length = 2 + header_length + len(message.base64body())
        with BytesIO(bytearray(msg_length)) as baos:
            baos.write(prolog_len)
            baos.write(DataMessageMagicByte.HTTP_REQUEST.value.encode("utf-8"))
            baos.write(id_bytes)
            baos.write(prolog)
            baos.write(message.base64body())
            return baos.getvalue()

    @staticmethod
    def from_bytes(input_bytes: bytes) -> DataMessage:
        """
        Builds the DataMessage from its serialized (byte array) form.
        :param input_bytes: serialized message
        :return: DataMessage instance
        """
        prolog_len = (input_bytes[0] << 8) + input_bytes[1]
        metadata = input_bytes[2 : prolog_len + 2].decode()

        pattern = re.compile(
            f"{DataMessageMagicByte.HTTP_REQUEST.value}"
            r"([0-9A-Fa-f]+)\|"
            r"m=([^|]*)\|"
            r"d=([^|]*)\|"
            r"p=([^|]*)\|"
            r"h=(\{[^}]*\})\|"
            r"t=(\{[^}]*\})\|"
            r"b="
        )
        match = pattern.match(metadata)
        if not match:
            raise ValueError(
                f"DataMessage format does not match expected format. "
                f"Input string [{metadata}] does not match pattern: {pattern.pattern}"
            )

        id_str = match.group(1)
        method = match.group(2)
        domain = match.group(3)
        path = match.group(4)
        headers_str = match.group(5)
        trace_info_str = match.group(6)
        body_b64 = input_bytes[prolog_len + 2 :]

        headers = parse_map_list_string(headers_str)
        trace_info = parse_map_string(trace_info_str)

        return DataMessage(
            DataMessageMagicByte.HTTP_REQUEST.value,
            SnowflakeId.from_hex(id_str),
            method,
            domain,
            path,
            headers,
            trace_info,
            body_b64,
        )

    @staticmethod
    async def from_stream(
        stream: bytes,
        rabbitmq_url: str,
        loop,
        file_handler: FileHandler = StreamingFileHandler(),
    ) -> DataMessage | None:
        """
        Builds the DataMessage from its serialized (byte array) form.
        :param stream: serialized message
        :param rabbitmq_url: RabbitMQ connection URL
        :param loop: event loop
        :param file_handler: file handler for downloading the message
        :return: DataMessage instance
        """
        # async implementation
        _message_data = json.loads(stream.decode())
        _req_info = _message_data.get("req-info", "")
        if _req_info:
            (body, eof) = await file_handler.download_buffer(
                loop=loop, rabbitmq_url=rabbitmq_url, queue_name=_req_info
            )
            if eof == amqp.adapter.file_handler.END_OF_FILE:
                return DataMessageFactory.from_bytes(body)
        return None
