import datetime as dt

import asyncio
import json

from typing import List, Dict, Optional, Union, TYPE_CHECKING, TypedDict, Any

from shimoku.api.base_resource import Resource

if TYPE_CHECKING:
    from shimoku.api.resources.app import App

import logging
from shimoku.execution_logger import log_error

logger = logging.getLogger(__name__)


class Activity(Resource):
    """
    A class that contains all the information necessary to create and run an activity.
    """

    _module_logger = logger
    resource_type = "activity"
    alias_field = "name"
    plural = "activities"

    class ActivityParams(TypedDict):
        name: Optional[str]
        settings: dict
        activityTemplateWithMode: Optional[dict]
        universeApiKeyId: Optional[str]

    class Run(Resource):
        """
        Run of an activity, which is a single execution of the activity.
        """

        _module_logger = logger
        resource_type = "run"
        plural = "runs"

        class Log(Resource):
            """
            Log of an activity run.
            """

            _module_logger = logger
            resource_type = "log"
            plural = "logs"

            def __init__(
                self,
                parent: "Activity.Run",
                uuid: Optional[str] = None,
                db_resource: Optional[Dict] = None,
            ):
                params = dict(
                    dateTime=str(dt.datetime.now().isoformat())[:-3] + "Z",
                    message="message",
                    severity="INFO",
                    tags={},
                )

                super().__init__(
                    parent=parent,
                    uuid=uuid,
                    db_resource=db_resource,
                    check_params_before_creation=["message", "severity"],
                    params_to_serialize=["tags"],
                    params=params,
                )

        def __init__(
            self,
            parent: "Activity",
            uuid: Optional[str] = None,
            db_resource: Optional[Dict] = None,
        ):
            params = dict(
                settings={},
            )

            super().__init__(
                parent=parent,
                uuid=uuid,
                db_resource=db_resource,
                children=[Activity.Run.Log],
                params_to_serialize=["settings"],
                params=params,
            )

        async def create_log(
            self, message: str, severity: str, tags: Dict[str, str]
        ) -> "Activity.Run.Log":
            """
            Creates a log for the activity run.
            :param message: The message of the log.
            :param severity: The severity of the log.
            :param tags: The tags of the log.
            :return: The created log.
            """
            if tags and (
                not isinstance(tags, Dict)
                or any(
                    [
                        not isinstance(key, str) or not isinstance(value, str)
                        for key, value in tags.items()
                    ]
                )
            ):
                log_error(
                    logger, "The tags must be a dictionary of strings.", ValueError
                )

            return await self._base_resource.create_child(
                Activity.Run.Log, message=message, severity=severity, tags=tags
            )

        async def get_log(self, uuid: str) -> Optional["Activity.Run.Log"]:
            """
            Gets a log of the activity run.
            :param uuid: The uuid of the log.
            :return: The log.
            """
            return await self._base_resource.get_child(Activity.Run.Log, uuid)

        async def get_logs(self) -> List["Activity.Run.Log"]:
            """
            Gets all the logs of the activity run.
            :return: The logs.
            """
            logs = await self._base_resource.get_children(Activity.Run.Log)
            return sorted(logs, key=lambda log: log["dateTime"])

        async def trigger_webhook(self):
            """
            Triggers the webhook of the activity in a run.
            """
            endpoint = (
                self._base_resource.base_url
                + f"run/{self._base_resource.id}/triggerWebhook"
            )

            response = await self._base_resource.api_client.query_element(
                method="POST",
                endpoint=endpoint,
            )

            return response["STATUS"]

        def cascade_to_dict(self) -> Dict[str, Any]:
            """
            Returns the run as a dictionary.
            :return: The run as a dictionary.
            """
            run_dict = super().cascade_to_dict()
            run_dict["logs"] = sorted(run_dict["logs"], key=lambda log: log["dateTime"])
            return run_dict

    @staticmethod
    def _runs_ordering(run: "Activity.Run") -> dt.datetime:
        """Returns the datetime of the last log of the run. Used for sorting runs.
        :param run: The run.
        :return: The datetime of the last log of the run.
        """
        logs = run._base_resource.children[Activity.Run.Log]
        if len(logs) > 0:
            return max(
                [
                    dt.datetime.strptime(log["dateTime"], "%Y-%m-%dT%H:%M:%S.%fZ")
                    if log["dateTime"]
                    else dt.datetime.min
                    for log_id, log in logs
                ]
            )
        else:
            return dt.datetime.min

    @staticmethod
    async def sort_runs_by_log_time(runs: List["Activity.Run"]) -> List["Activity.Run"]:
        """Returns the runs ordered by the datetime of the last log of the run.
        :param runs: The runs.
        :return: The runs ordered by the datetime of the last log of the run.
        """
        await asyncio.gather(*[run.get_logs() for run in runs])
        return sorted(runs, key=Activity._runs_ordering)

    def __init__(
        self,
        parent: "App",
        uuid: Optional[str] = None,
        alias: Optional[str] = None,
        db_resource: Optional[Dict] = None,
    ):
        params: Activity.ActivityParams = dict(
            name=alias,
            settings={},
            activityTemplateWithMode=None,
            universeApiKeyId=None,
        )

        super().__init__(
            parent=parent,
            uuid=uuid,
            db_resource=db_resource,
            params=params,
            children=[Activity.Run],
            check_params_before_creation=["name"],
            params_to_serialize=["settings"],
        )

    async def delete(self):
        """Deletes the activity."""
        return await self._base_resource.delete()

    async def update(self):
        """Updates the activity."""
        return await self._base_resource.update()

    # Webhook methods
    async def create_webhook(self, url: str, method: str, headers: Dict[str, str]):
        """Creates a webhook for the activity.
        :param url: The url of the webhook.
        :param method: The method of the webhook.
        :param headers: The headers of the webhook.
        """
        if not headers:
            headers = {}
        if not isinstance(headers, Dict) or any(
            [
                not isinstance(key, str) or not isinstance(value, str)
                for key, value in headers.items()
            ]
        ):
            log_error(
                logger, "The headers must be a dictionary of strings.", ValueError
            )

        endpoint = (
            self._base_resource.base_url + f"activity/{self._base_resource.id}/webhook"
        )
        params = dict(
            url=url,
            method=method,
            headers=json.dumps(headers),
        )
        await self._base_resource.api_client.query_element(
            method="POST", endpoint=endpoint, **{"body_params": params}
        )

    # Run methods
    async def get_run(self, uuid: str) -> Optional["Activity.Run"]:
        """Gets a run of the activity.
        :param uuid: The uuid of the run.
        :return: The run.
        """
        return await self._base_resource.get_child(Activity.Run, uuid)

    async def get_runs(
        self, how_many_runs: Optional[int] = None
    ) -> List["Activity.Run"]:
        """Gets the last runs of the activity.
        :param how_many_runs: The number of runs to get.
        :return: The runs.
        """
        runs = await Activity.sort_runs_by_log_time(
            await self._base_resource.get_children(Activity.Run)
        )
        return runs[-how_many_runs:] if how_many_runs is not None else runs

    async def create_run(
        self, settings: Optional[Union[Dict, str]] = None
    ) -> "Activity.Run":
        """Creates a run of the activity.
        :param settings: The settings of the run or id of the run to copy the settings from.
        :return: The run.
        """
        if isinstance(settings, str):
            settings = (await self.get_run(settings))["settings"]
            assert isinstance(settings, dict)

        return await self._base_resource.create_child(Activity.Run, settings=settings)
