# SPDX-License-Identifier: Apache-2.0.
# Copyright (c) 2024 - 2025 Waldiez and contributors.

# pyright: reportUnnecessaryIsInstance=false,reportUnknownVariableType=false
# pyright: reportUnknownArgumentType=false,reportArgumentType=false
# pyright: reportReturnType=false

"""User input data models and validation."""

import json
import os
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

from pydantic import BaseModel, Field, field_validator
from typing_extensions import Annotated

from ..utils import detect_media_type
from .constants import CONTENT_MAPPING, ContentMappingEntry, MediaContent
from .content.audio import AudioMediaContent
from .content.base import (
    AudioContent,
    FileContent,
    ImageContent,
    VideoContent,
)
from .content.file import FileMediaContent
from .content.image import ImageMediaContent, ImageUrlMediaContent
from .content.text import TextMediaContent
from .content.video import VideoMediaContent


class UserInputData(BaseModel):
    """User's input data model."""

    content: Annotated[
        MediaContent | list[MediaContent],
        Field(
            description="The content of the input data.",
            title="Content",
        ),
    ]

    def __str__(self) -> str:
        """Get the string representation of the content."""
        return self.to_string()

    def __repr__(self) -> str:
        """Get the string representation of the UserInputData."""
        return f"UserInputData(content={self.to_string()})"

    def to_string(
        self,
        uploads_root: Path | None = None,
        base_name: str | None = None,
    ) -> str:
        """Convert the content to a string.

        Parameters
        ----------
        uploads_root : Path | None
            The root directory for storing images, optional.
        base_name : str | None
            The base name for the image file, optional.

        Returns
        -------
        str
            The string representation of the content.
        """
        if isinstance(self.content, list):
            return " ".join(
                [
                    item.to_string(uploads_root, base_name)
                    for item in self.content
                ]
            )
        return self.content.to_string(uploads_root, base_name)

    @classmethod
    def content_from_string(cls, value: str) -> MediaContent:
        """Convert a string to the appropriate MediaContent type.

        Parameters
        ----------
        value : str
            The input string

        Returns
        -------
        MediaContent
            the appropriate MediaContent.
        """
        try:
            parsed = json.loads(value)
        except json.JSONDecodeError:
            return TextMediaContent(type="text", text=value)
        if isinstance(parsed, str):
            try:
                parsed = json.loads(parsed)
            except json.JSONDecodeError:
                return TextMediaContent(type="text", text=parsed)
            if isinstance(parsed, str):  # pragma: no cover
                return TextMediaContent(type="text", text=parsed)
        return cls.validate_content(parsed)

    @classmethod
    def content_from_dict(cls, value: dict[str, Any]) -> MediaContent:
        """Convert a dictionary to the appropriate MediaContent type.

        Parameters
        ----------
        value : dict[str, Any]
            The input dictionary

        Returns
        -------
        MediaContent
            the appropriate MediaContent.

        Raises
        ------
        ValueError
            If the content type is not supported or
            if any required field is missing.
        """
        content_type = detect_media_type(value)

        # Get the mapping for the detected content type
        if content_type not in CONTENT_MAPPING:
            raise ValueError(f"Unsupported content type: {content_type}")

        # Get the mapping for the content type
        mapping = CONTENT_MAPPING[content_type]
        return cls._build_media_content(value, mapping, content_type)

    @classmethod
    def _build_media_content(
        cls,
        value: dict[str, Any],
        mapping: ContentMappingEntry,
        content_type: str,
    ) -> MediaContent:
        """Try to construct the appropriate MediaContent from the mapping."""
        for field in mapping["fields"]:
            if field not in value:
                continue

            raw_val = value[field]

            if field == mapping["required_field"]:
                # if we have direct mapping to the required field,
                # let's try to instantiate the class directly
                try:
                    return mapping["cls"](**{field: raw_val})
                except ValueError:
                    pass  # let's try to convert it

            converted = cls._convert_simple_content(
                raw_val, mapping["required_field"]
            )
            if converted is not None:  # pragma: no branch
                return mapping["cls"](
                    **{mapping["required_field"]: converted}  # type: ignore
                )

        msg = (
            "Missing required field for content type "
            f"'{content_type}' in: {value}"
        )
        raise ValueError(msg)

    @staticmethod
    def _convert_simple_content(
        raw_val: Any, target_field: str
    ) -> ImageContent | VideoContent | AudioContent | FileContent | None:
        """Convert a simple string to the appropriate content, if applicable."""
        if not isinstance(raw_val, str):  # pragma: no cover
            return None

        if target_field in ("image_url", "image"):
            return ImageContent(url=raw_val)
        if target_field == "video":
            return VideoContent(url=raw_val)
        if target_field == "audio":
            return AudioContent(url=raw_val)
        if target_field == "file":
            filename = extract_filename_from_path(raw_val)
            return FileContent(name=filename, url=raw_val)

        return None  # pragma: no cover

    # noinspection PyNestedDecorators
    @field_validator("content", mode="before")
    @classmethod
    def validate_content(cls, v: Any) -> MediaContent:  # noqa: C901,D102
        """Validate the input data content.

        Parameters
        ----------
        v: Any
            The input data content

        Returns
        -------
        MediaContent
            The validated content

        Raises
        ------
        ValueError
            If the content is not valid.
        """
        if isinstance(
            v,
            (
                TextMediaContent,
                ImageMediaContent,
                ImageUrlMediaContent,
                VideoMediaContent,
                AudioMediaContent,
                FileMediaContent,
            ),
        ):
            return v

        # If it's a string, check if it is a dumped one
        if isinstance(v, str):
            return cls.content_from_string(v)

        # If it's a dictionary, check if it has a 'type' field
        if isinstance(v, dict):
            return cls.content_from_dict(v)

        # If it's a list
        if isinstance(v, list):
            return [cls.validate_content(item) for item in v]  # type: ignore

        # Default fallback
        return TextMediaContent(type="text", text=str(v))


def extract_filename_from_path(path_or_url: str) -> str:
    """Extract the filename from a given path or URL.

    Parameters
    ----------
    path_or_url: str
        The path or URL from which to extract the filename.

    Returns
    -------
    str
        The extracted filename.
    """
    if "://" in path_or_url:
        # It's a URL - parse it properly
        parsed = urlparse(path_or_url)
        # Extract filename from the path component
        filename = os.path.basename(parsed.path)

        # Handle edge cases where path might be empty or end with /
        if not filename and parsed.path.endswith("/"):  # pragma: no cover
            # Try to get the last directory name
            path_parts = [p for p in parsed.path.split("/") if p]
            filename = path_parts[-1] if path_parts else parsed.netloc
        elif not filename:  # pragma: no cover
            # Fallback to using netloc (domain) if no path
            filename = parsed.netloc

        return filename
    # else:
    # Local file path
    return os.path.basename(path_or_url)  # pragma: no cover
