import re
from copy import deepcopy
from importlib import import_module
from importlib.resources import files
from typing import Any

from .relai_data import RELAIData


def get_component_cls_by_name(component_name: str) -> RELAIData:
    component_cls_name = None
    if component_name == "feedback-question":
        component_cls_name = "FeedbackQuestion"
    elif component_name == "feedback-questions":
        component_cls_name = "FeedbackQuestions"
    elif component_name == "image-grid":
        component_cls_name = "ImageGrid"
    elif component_name == "image":
        component_cls_name = "Image"
    elif component_name == "table":
        component_cls_name = "Table"
    elif component_name == "vega-chart":
        component_cls_name = "VegaChart"
    else:
        raise ValueError(f"Unrecognized component with name '{component_name}'.")
    return getattr(import_module(".components", package=__package__), component_cls_name)


class ApplicationResult(RELAIData):
    """Data model for an application result.
    See the schema at https://api.relai.ai/schema/application-result.schema.json for details about the accepted values.

    Args:
        application (str): Name of the application.
        application_version (str): Version of the application.
        display_name (str): Display name of the application.
        application_info (str): A short description of the application.
        started_at (str): Timestamp when the run was started.
        project_mame (str): Name of the project.
        run_id (str): ID of the run.
        components (dict[str, RELAIData]): Dictionary mapping component ids to corresponding components.
        generated_by (str, optional): Name of the user that generated the application result. Defaults to None.
        ended_at (str, optional): Timestamp when the run was ended. Defaults to None.
        schema_version (str, optional): Schema version. Defaults to "v1.0".
        metadata (dict, optional): Additional metadata. Defaults to None.
    """

    schema_file: str = files("relai_visualization").joinpath("schema/application-result.schema.json")

    def __init__(
        self,
        application: str,
        application_version: str,
        display_name: str,
        application_info: str,
        started_at: str,
        project_name: str,
        run_id: str,
        components: dict[str, RELAIData],
        generated_by: str = None,
        ended_at: str = None,
        schema_version: str = "v1.0",
        metadata: dict = None,
    ) -> None:
        if not re.match(r"^[a-z0-9-]+$", application):
            raise ValueError("Application name must only include lowercase letters, digits and hyphens.")
        if not re.match(r"^v\d+\.\d+$", application_version):
            raise ValueError("Application version must be in the format 'v{x}.{y}'.")

        if not re.match(r"^[a-z0-9-_]+$", project_name):
            raise ValueError("Project name must only include lowercase characters, hyphens, and underscores.")

        if len(components) < 1:
            raise ValueError("At least one component must be provided.")

        for id, component in components.items():
            components[id] = component.data

        super().__init__(
            schemaVersion=schema_version,
            application=application,
            applicationVersion=application_version,
            displayName=display_name,
            applicationInfo=application_info,
            generatedBy=generated_by,
            startedAt=started_at,
            endedAt=ended_at,
            projectName=project_name,
            runId=run_id,
            metadata=metadata,
            components=components,
        )

    @classmethod
    def user_feedback(cls, component: dict) -> dict:
        """Get the feedback provided by the user for the component.

        Args:
            component (dict): ApplicationResult component data as a dictionary.

        Returns:
            dict: User feedback for the component as a dictionary mapping component id to feedback values.
                Feedback for components that do not support feedback will be `None`.
        """
        output = {}
        for component_id, component_data in component["components"].items():
            try:
                output[component_id] = get_component_cls_by_name(component_data["componentName"]).user_feedback(
                    component_data
                )
            except NotImplementedError:
                output[component_id] = None
        return output

    @classmethod
    def update_with_feedback(cls, component: dict, modifies: list[str], new_value: Any) -> dict:
        """Update an ApplicationResult with user feedback.

        Args:
            component (dict): ApplicationResult component to update as a Python dictionary.
            modifies (list[str]): List of strings that uniquely identify the data to be updated.
                The first element must be the component id. The rest of the element must be identifiers for within
                the component.
            new_value (Any): New value to set.

        Returns:
            dict: Updated ApplicationResult.
        """
        component = deepcopy(component)
        component_id = modifies[0]
        try:
            component_to_update = component["components"][component_id]
        except KeyError:
            raise ValueError(f"Component with id '{component_id}' not found.")
        component_name = component_to_update["componentName"]
        try:
            component["components"][component_id] = get_component_cls_by_name(component_name).update_with_feedback(
                component_to_update, modifies[1:], new_value
            )
        except NotImplementedError:
            raise RuntimeError(
                f"Component with id '{component_id}' and name '{component_name}' does not support feedback."
            )
        return component
