"""
This module provides serialization and deserialization functions for Pydantic models that extend BaseModel
"""

import importlib
import json
import uuid
from datetime import date, datetime
from types import UnionType
from typing import Any, Dict, Union, get_args, get_origin
from urllib.parse import parse_qs, urlparse

from pydantic import BaseModel

from amqp.adapter.logging_utils import logging_debug


def serialize_object(obj: BaseModel) -> str:
    """
    Serializes an R2RSerializable object to a string in the format
    "<ClassName>:{key1:value1, key2:value2,...}".  Handles UUIDs and datetimes,
    and nested Pydantic models.

    Args:
        obj: The BaseModel object to serialize.

    Returns:
        A string representation of the object.
    """
    class_name = obj.__class__.__name__
    module_name = obj.__class__.__module__
    obj_dict = {}
    for key, _ in obj.model_dump().items():
        # here we use the original value
        # the one returned by model_dump will convert everything into dict recursively
        value = getattr(obj, key)
        if isinstance(value, uuid.UUID):
            obj_dict[key] = str(value)
        elif isinstance(value, datetime):
            obj_dict[key] = value.isoformat()
        elif isinstance(value, dict):  # Ensure dictionaries are handled
            obj_dict[key] = value
        elif isinstance(value, BaseModel):
            obj_dict[key] = serialize_object(value)  # Recursively serialize
        elif isinstance(value, list):
            serialized_list = []
            for item in value:
                if isinstance(item, BaseModel):
                    serialized_list.append(serialize_object(item))
                else:
                    serialized_list.append(item)
            obj_dict[key] = serialized_list
        else:
            obj_dict[key] = value

    dict_string = json.dumps(obj_dict)
    return f"{module_name}.{class_name}:{dict_string}"


def python_type_to_json(data):
    """
    Convert Python objects (dict, list, tuple, set, datetime, etc.) to a JSON string.
    Handles common non-JSON-serializable types like datetime, date, and sets.

    Args:
        data: Python object to be converted (dict, list, tuple, set, etc.)

    Returns:
        str: JSON string representation of the input data

    Raises:
        TypeError: If the object contains non-serializable types (e.g., custom classes)
    """

    def default_serializer(obj):
        """Custom serializer for non-JSON-serializable objects."""
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()  # Convert datetime/date to ISO string
        elif isinstance(obj, set):
            return list(obj)  # Convert sets to lists
        elif hasattr(obj, "__dict__"):
            return obj.__dict__  # Attempt to serialize custom objects
        else:
            raise TypeError(f"Object of type {type(obj)} is not JSON serializable")

    return json.dumps(data, default=default_serializer, indent=0, ensure_ascii=False)


def try_deserialize_object(data: str) -> BaseModel | str:
    """
    Try deserialize object, if failed, return the input string as-is.
    """
    if not isinstance(data, str):
        return data
    try:
        return deserialize_object(data)
    except Exception as e:
        logging_debug(f"Failed to deserialize object: {e}")
        return data


def deserialize_object(data: str) -> BaseModel:
    """
    Deserializes a string in the format "<ClassName>:{key1:value1, key2:value2,...}"
    back into an BaseModel object.  Handles UUIDs, datetimes, and
    nested Pydantic models.

    Args:
        data: The string to deserialize.

    Returns:
        An R2RSerializable object.
    """
    class_path, dict_str = data.split(":", 1)
    module_path, class_name = class_path.rsplit(".", 1)

    obj_dict: Dict[str, Any] = json.loads(dict_str)

    logging_debug(f"Deserialized object: {class_path}")
    if "[" in class_name:
        class_name = class_name.split("[")[0]
    logging_debug(f"trying object: {class_name}")
    module = importlib.import_module(module_path)
    cls = getattr(module, class_name)
    if cls is None:
        raise ValueError(f"Unknown class: {class_name}")

    # process nested model, dict and list
    for key, value in obj_dict.items():
        key_type: type[Any] = cls.model_fields[key].annotation
        # handle union with none, this will give the true type
        # for example, return `A` if the type is `A | None`
        if get_origin(key_type) is UnionType or get_origin(key_type) is Union:
            union_types = get_args(key_type)
            if len(union_types) > 2:
                # if there are multiple unions, like `A | B | None`
                # then we have no way to figure out which one to use
                raise ValueError(f"Unsupported Union type: {key_type}")
            # return the none-None one as the type for processing
            if union_types[0] is type(None):
                key_type = union_types[1]
            else:
                key_type = union_types[0]
        # skip if the value is None
        if value is None:
            continue

        if key_type == uuid.UUID:
            obj_dict[key] = uuid.UUID(value)
        elif key_type == datetime:
            obj_dict[key] = datetime.fromisoformat(value)
        elif key_type == dict:
            obj_dict[key] = value
        elif issubclass(key_type, BaseModel):
            obj_dict[key] = deserialize_object(value)
        elif key_type == list:
            obj_dict[key] = list(map(try_deserialize_object, value))

    return cls(**obj_dict)


def parse_url_query(url):
    """
    Parses a HTTP GET URL and returns (context_path, query_params_dict)

    Args:
        url: The URL string to parse (e.g., "http://example.com/path?foo=1&bar=2&foo=3")

    Returns:
        tuple: (context_path, query_params_dict)
        - context_path: The path part of the URL (e.g., "/path")
        - query_params_dict: Dictionary of query parameters where values are always lists
                           (e.g., {"foo": ["1", "3"], "bar": ["2"]})
    """
    parsed = urlparse(url)
    query_params = parse_qs(parsed.query)

    # Convert values from lists to single values when there's only one value
    # If you prefer to always keep values as lists, remove this comprehension
    simplified_params = {k: v[0] if len(v) == 1 else v for k, v in query_params.items()}

    return (parsed.path, simplified_params)
