from abc import ABC, abstractmethod
from collections import UserDict
from enum import StrEnum
from typing import Annotated, Any

from pydantic import Field

from .model import DurationSeconds, FrozenBaseModel

#
# Base application configuration
#

class BaseApplicationConfig(FrozenBaseModel):
    """
    Base class for application configurations to inherit from.

    Pydantic models are used to define application configurations, enabling validation and
    serialization/deserialization to/from JSON for storage and transmission.

    This base class requires every configuration to have two common fields:
        - `name`: A unique name to identify the configuration.
        - `timeout_s`: Maximum allowed duration of the application run in seconds. (If set to 0, the application will run until completion.)
    """

    name: str = Field(
        title             = "Configuration Name",
        description       = "Name to identify the configuration.",
        min_length        = 1,
        max_length        = 250,                       # Limit to 250 (i.e. 255 - '.json') for filesystem compatibility, as the name can also be used as filename
        pattern           = r"^[\w-]+(?:\s[\w-]+)*$",  # Alphanumeric, underscores, hyphens and spaces only, while disallowing leading, trailing and consecutive spaces
        json_schema_extra = {"format": "grid-break"},
    )
    """
    A unique name to characterise this configuration. This is used to identify the configuration, and must not overlap with other
    config names for the same application. Only alphanumeric characters, underscores, hyphens and spaces are allowed, while leading,
    trailing and consecutive spaces are disallowed.

    Must be at least 1 character and at most 250 characters long.
    """

    timeout_s: Annotated[
        DurationSeconds,
        Field(
            title       = "Timeout",
            description = "Maximum allowed duration of the application run in seconds, after which it will be forcefully stopped. " \
                          "If set to 0, the application will run until completion.",
            default     = 3600 * 12,  # Default to 12 hours
            gt          = -1,         # Use this instead of ge=0 to overwrite the inherited constraint from DurationSeconds
        ),
    ]
    """
    The maximum allowed duration of the application run in seconds. If set to 0, the application will run until completion.
    Defaults to 12 hours (43200 seconds).

    Otherwise, if the application's run exceeds this duration, it will be forcefully stopped.
    """

    def estimate_duration_s(self) -> float | None:
        """
        Optionally override this method to provide an estimate of the duration (in seconds) of a run through of the application
        using this config, to help with planning and scheduling of application runs on the device using the integrated queue system.
        """
        return None

#
# Run info data
#

class OutputType(StrEnum):
    """
    Types of output generated by an application run summary.

    **Note**: Currently, only `TEXT` is supported to be displayed via the system web interface.
    """

    TEXT = "text"
    """
    Plain text output.
    """

    #
    # Support for the following types are planned for future releases:
    #

    JSON = "json"
    """
    JSON-serializable data structure.
    """

    MARKDOWN = "markdown"
    """
    Markdown-formatted text.
    """

    BASE64 = "base64"
    """
    Base64-encoded binary data, e.g. images or files.
    """

    HTML = "html"
    """
    HTML-formatted text.
    """

class RunSummary(UserDict[str, Any]):
    """
    Dictionary-based run summary supporting arbitrary fields.

    All run summaries must have a `type` field indicating the output type.
    Additional type-specific fields and arbitrary custom fields can be added.

    Required key:
        type (OutputType): The type of output.

    Type-specific required keys:
        - For TEXT/MARKDOWN/HTML: `content` (str) - The text content.
        - For JSON: `data` (dict) - The JSON data structure.
        - For BASE64: `content` (str) - Base64-encoded string, and optionally `mime_type` (str, e.g. "image/png").

    Additional arbitrary keys are allowed for custom metadata and future extensibility.

    Examples:
        **Text summary**:
            ```python
            summary = RunSummary(type=OutputType.TEXT, content="Results here")
            summary["accuracy"] = 0.95
            summary["metadata"] = {"version": "1.0"}
            ```

        **JSON summary**:
            ```python
            summary = RunSummary(type=OutputType.JSON, data={"results": [1, 2, 3]})
            summary["custom_field"] = "custom_value"
            ```

    Note:
        Custom fields are JSON serialized, but presently not used by the system.
    """

    def __init__(self, type: OutputType, **kwargs: Any):
        """
        Initialize a run summary.

        Args:
            type: The output type for this summary.
            **kwargs: Additional fields specific to the type and custom metadata.
        """
        super().__init__(type=type, **kwargs)

    @property
    def type(self) -> OutputType:
        """The output type of this summary."""
        return self.data["type"]

#
# Application base class
#

class BaseApplication[T: BaseApplicationConfig](ABC):
    """
    Base class for applications that can be run on the device.

    In the `__init__.py` of each application module, an instance of a class inheriting from this base class
    must be created and assigned to the `application` variable, so that the system can discover and run the application.

    The `run()` method serves as the main entry point for executing the application with a given configuration.
    The output directory of each run is provided as a unique, time-stamped directory within the device's recording storage directory,
    where the application can save results and raw outputs.

    `T` is a generic type parameter representing the configuration class used by the application, which must inherit from `BaseApplicationConfig`.
    """

    @abstractmethod
    def run(self, config: T, output_directory: str) -> RunSummary | None:
        """
        Run the application with the given configuration, saving results and raw outputs to the specified relative path within the
        recording storage directory of the device.

        Args:
            config: The application configuration to use for this run. Provided as an instance of the configuration class returned
                by `config_class()`, generated by the system from user configurations stored on the device.
            output_directory: The relative path within the device's recording storage directory where outputs should be saved.

        Returns:
            Optionally return a :class:`RunSummary` object summarising the outcome of the run.
        """
        ...

    @staticmethod
    @abstractmethod
    def config_class() -> type[T]:
        """
        Return the configuration class used by this application.
        The configuration must inherit from :class:`BaseApplicationConfig`.

        Note that the returned type must be the class itself, not an instance.
        """
        ...
