import json
from typing import Final, Optional

from moto.stepfunctions.parser.api import HistoryEventType, TaskFailedEventDetails
from moto.stepfunctions.parser.asl.component.common.error_name.custom_error_name import (
    CustomErrorName,
)
from moto.stepfunctions.parser.asl.component.common.error_name.error_name import (
    ErrorName,
)
from moto.stepfunctions.parser.asl.component.common.error_name.failure_event import (
    FailureEvent,
)
from moto.stepfunctions.parser.asl.component.state.exec.state_task.credentials import (
    StateCredentials,
)
from moto.stepfunctions.parser.asl.component.state.exec.state_task.service.resource import (
    ResourceCondition,
    ResourceRuntimePart,
)
from moto.stepfunctions.parser.asl.component.state.exec.state_task.service.state_task_service_callback import (
    StateTaskServiceCallback,
)
from moto.stepfunctions.parser.asl.eval.environment import Environment
from moto.stepfunctions.parser.asl.eval.event.event_detail import EventDetails
from moto.stepfunctions.parser.asl.utils.boto_client import boto_client_for
from moto.stepfunctions.parser.asl.utils.encoding import to_json_str

_SUPPORTED_INTEGRATION_PATTERNS: Final[set[ResourceCondition]] = {
    ResourceCondition.WaitForTaskToken,
}
_FAILED_ENTRY_ERROR_NAME: Final[ErrorName] = CustomErrorName(
    error_name="EventBridge.FailedEntry"
)

_SUPPORTED_API_PARAM_BINDINGS: Final[dict[str, set[str]]] = {"putevents": {"Entries"}}


class SfnFailedEntryCountException(RuntimeError):
    cause: Final[Optional[dict]]

    def __init__(self, cause: Optional[dict]):
        super().__init__(json.dumps(cause))
        self.cause = cause


class StateTaskServiceEvents(StateTaskServiceCallback):
    def __init__(self):
        super().__init__(supported_integration_patterns=_SUPPORTED_INTEGRATION_PATTERNS)

    def _get_supported_parameters(self) -> Optional[set[str]]:
        return _SUPPORTED_API_PARAM_BINDINGS.get(self.resource.api_action.lower())

    def _from_error(self, env: Environment, ex: Exception) -> FailureEvent:
        if isinstance(ex, SfnFailedEntryCountException):
            return FailureEvent(
                env=env,
                error_name=_FAILED_ENTRY_ERROR_NAME,
                event_type=HistoryEventType.TaskFailed,
                event_details=EventDetails(
                    taskFailedEventDetails=TaskFailedEventDetails(
                        error=_FAILED_ENTRY_ERROR_NAME.error_name,
                        cause=ex.cause,
                        resource=self._get_sfn_resource(),
                        resourceType=self._get_sfn_resource_type(),
                    )
                ),
            )
        return super()._from_error(env=env, ex=ex)

    @staticmethod
    def _normalised_request_parameters(env: Environment, parameters: dict):
        entries = parameters.get("Entries", [])
        for entry in entries:
            # Optimised integration for events automatically stringifies "Entries.Detail" if this is not a string,
            #  and only if these are json objects.
            if "Detail" in entry:
                detail = entry.get("Detail")
                if isinstance(detail, dict):
                    entry["Detail"] = to_json_str(
                        detail
                    )  # Pass runtime error upstream.

            # The execution ARN and the state machine ARN are automatically appended to the Resources
            #  field of each PutEventsRequestEntry.
            resources = entry.get("Resources", [])
            resources.append(
                env.states.context_object.context_object_data["StateMachine"]["Id"]
            )
            resources.append(
                env.states.context_object.context_object_data["Execution"]["Id"]
            )
            entry["Resources"] = resources

    def _eval_service_task(
        self,
        env: Environment,
        resource_runtime_part: ResourceRuntimePart,
        normalised_parameters: dict,
        state_credentials: StateCredentials,
    ):
        self._normalised_request_parameters(env=env, parameters=normalised_parameters)
        service_name = self._get_boto_service_name()
        api_action = self._get_boto_service_action()
        events_client = boto_client_for(
            service=service_name,
            region=resource_runtime_part.region,
            state_credentials=state_credentials,
        )
        response = getattr(events_client, api_action)(**normalised_parameters)
        response.pop("ResponseMetadata", None)

        # If the response from PutEvents contains a non-zero FailedEntryCount then the
        #  Task state fails with the error EventBridge.FailedEntry.
        if self.resource.api_action == "putEvents":
            failed_entry_count = response.get("FailedEntryCount", 0)
            if failed_entry_count > 0:
                # TODO: pipe events' cause in the exception object. At them moment
                #  LS events does not update this field.
                raise SfnFailedEntryCountException(cause=response)

        env.stack.append(response)
