"""Kloppy EventDataset to SPADL converter."""

import warnings
from typing import Optional, Union, cast

import kloppy
import pandas as pd  # type: ignore
from kloppy.domain import (
    BodyPart,
    CardType,
    CarryEvent,
    ClearanceEvent,
    CoordinateSystem,
    Dimension,
    DuelEvent,
    DuelResult,
    DuelType,
    Event,
    EventDataset,
    EventType,
    FoulCommittedEvent,
    GoalkeeperActionType,
    GoalkeeperEvent,
    InterceptionResult,
    MiscontrolEvent,
    Orientation,
    Origin,
    PassEvent,
    PassResult,
    PassType,
    PitchDimensions,
    Provider,
    Qualifier,
    RecoveryEvent,
    SetPieceType,
    ShotEvent,
    ShotResult,
    TakeOnEvent,
    TakeOnResult,
    VerticalOrientation,
)
from packaging import version
from pandera.typing import DataFrame

from . import config as spadlconfig
from .base import _add_dribbles, _fix_clearances
from .schema import SPADLSchema

_KLOPPY_VERSION = version.parse(kloppy.__version__)
_SUPPORTED_PROVIDERS = {
    Provider.STATSBOMB: version.parse("3.14.0"),
}


def convert_to_actions(
    dataset: EventDataset, game_id: Optional[Union[str, int]] = None
) -> DataFrame[SPADLSchema]:
    """Convert a Kloppy event data set to SPADL actions.

    Parameters
    ----------
    dataset : EventDataset
        A Kloppy event data set.
    game_id : str or int, optional
        The identifier of the game. If not provided, the game id will not be
        set in the SPADL DataFrame.

    Returns
    -------
    actions : pd.DataFrame
        DataFrame with corresponding SPADL actions.

    """
    # Check if Kloppy is installed and if the version is supported
    if dataset.metadata.provider not in _SUPPORTED_PROVIDERS:
        warnings.warn(
            f"Converting {dataset.metadata.provider} data is not yet supported. "
            f"The result may be incorrect or incomplete. "
            f"Supported providers are: {', '.join([p.value for p in _SUPPORTED_PROVIDERS.keys()])}"
        )
    elif _KLOPPY_VERSION < _SUPPORTED_PROVIDERS[dataset.metadata.provider]:
        warnings.warn(
            f"Converting {dataset.metadata.provider} data is only supported from "
            f"Kloppy version {_SUPPORTED_PROVIDERS[dataset.metadata.provider]} (you have {_KLOPPY_VERSION}). "
            f"The result may be incorrect or incomplete."
        )

    # Convert the dataset to the SPADL coordinate system
    new_dataset = dataset.transform(
        to_orientation=Orientation.FIXED_HOME_AWAY,  # FIXME
        to_coordinate_system=_SoccerActionCoordinateSystem(normalized=False),
    )

    # Convert the events to SPADL actions
    actions = []
    for event in new_dataset.events:
        action = dict(
            game_id=game_id,
            original_event_id=event.event_id,
            period_id=event.period.id,
            time_seconds=event.timestamp,
            team_id=event.team.team_id if event.team else None,
            player_id=event.player.player_id if event.player else None,
            start_x=event.coordinates.x if event.coordinates else None,
            start_y=event.coordinates.y if event.coordinates else None,
            **_get_end_location(event),
            **_parse_event(event),
        )
        actions.append(action)

    # Create the SPADL actions DataFrame
    df_actions = (
        pd.DataFrame(actions)
        .sort_values(["game_id", "period_id", "time_seconds"], kind="mergesort")
        .reset_index(drop=True)
    )
    df_actions = df_actions[df_actions.type_id != spadlconfig.actiontypes.index("non_action")]

    df_actions = _fix_clearances(df_actions)

    df_actions["action_id"] = range(len(df_actions))
    df_actions = _add_dribbles(df_actions)

    return cast(DataFrame[SPADLSchema], df_actions)


class _SoccerActionCoordinateSystem(CoordinateSystem):
    @property
    def provider(self) -> Provider:
        return "SoccerAction"

    @property
    def origin(self) -> Origin:
        return Origin.BOTTOM_LEFT

    @property
    def vertical_orientation(self) -> VerticalOrientation:
        return VerticalOrientation.BOTTOM_TO_TOP

    @property
    def pitch_dimensions(self) -> PitchDimensions:
        return PitchDimensions(
            x_dim=Dimension(0, spadlconfig.field_length),
            y_dim=Dimension(0, spadlconfig.field_width),
        )


def _get_end_location(event: Event) -> dict[str, Optional[float]]:
    if isinstance(event, PassEvent):
        if event.receiver_coordinates:
            return {
                "end_x": event.receiver_coordinates.x,
                "end_y": event.receiver_coordinates.y,
            }
    elif isinstance(event, CarryEvent):
        if event.end_coordinates:
            return {
                "end_x": event.end_coordinates.x,
                "end_y": event.end_coordinates.y,
            }
    elif isinstance(event, ShotEvent):
        if event.result_coordinates:
            return {
                "end_x": event.result_coordinates.x,
                "end_y": event.result_coordinates.y,
            }
    if event.coordinates:
        return {"end_x": event.coordinates.x, "end_y": event.coordinates.y}
    return {"end_x": None, "end_y": None}


def _parse_event(event: Event) -> dict[str, int]:
    events = {
        EventType.PASS: _parse_pass_event,
        EventType.SHOT: _parse_shot_event,
        EventType.TAKE_ON: _parse_take_on_event,
        EventType.CARRY: _parse_carry_event,
        EventType.FOUL_COMMITTED: _parse_foul_event,
        EventType.DUEL: _parse_duel_event,
        EventType.CLEARANCE: _parse_clearance_event,
        EventType.MISCONTROL: _parse_miscontrol_event,
        EventType.GOALKEEPER: _parse_goalkeeper_event,
        EventType.INTERCEPTION: _parse_interception_event,
        # other non-action events
        # EventType.GENERIC: _parse_event_as_non_action,
        # EventType.RECOVERY: _parse_event_as_non_action,
        # EventType.SUBSTITUTION: _parse_event_as_non_action,
        # EventType.CARD: _parse_event_as_non_action,
        # EventType.PLAYER_ON: _parse_event_as_non_action,
        # EventType.PLAYER_OFF: _parse_event_as_non_action,
        # EventType.BALL_OUT: _parse_event_as_non_action,
        # EventType.FORMATION_CHANGE:_parse_event_as_non_action,
    }
    parser = events.get(event.event_type, _parse_event_as_non_action)
    a, r, b = parser(event)
    return {
        "type_id": spadlconfig.actiontypes.index(a),
        "result_id": spadlconfig.results.index(r),
        "bodypart_id": spadlconfig.bodyparts.index(b),
    }


def _qualifiers(event: Event) -> list[Qualifier]:
    if event.qualifiers:
        return [q.value for q in event.qualifiers]
    return []


def _parse_bodypart(qualifiers: list[Qualifier], default: str = "foot") -> str:
    if BodyPart.HEAD in qualifiers:
        b = "head"
    elif BodyPart.RIGHT_FOOT in qualifiers:
        b = "foot_right"
    elif BodyPart.LEFT_FOOT in qualifiers:
        b = "foot_left"
    elif BodyPart.CHEST in qualifiers or BodyPart.OTHER in qualifiers:
        b = "other"
    elif BodyPart.HEAD_OTHER in qualifiers:
        b = "head/other"
    else:
        b = default
    return b


def _parse_event_as_non_action(event: Event) -> tuple[str, str, str]:
    a = "non_action"
    r = "success"
    b = "foot"
    return a, r, b


def _parse_pass_event(event: PassEvent) -> tuple[str, str, str]:  # noqa: C901
    qualifiers = _qualifiers(event)
    b = _parse_bodypart(qualifiers)

    a = "pass"  # default
    r = None
    if SetPieceType.FREE_KICK in qualifiers:
        if (
            PassType.CHIPPED_PASS in qualifiers
            or PassType.CROSS in qualifiers
            or PassType.HIGH_PASS in qualifiers
            or PassType.LONG_BALL in qualifiers
        ):
            a = "freekick_crossed"
        else:
            a = "freekick_short"
    elif SetPieceType.CORNER_KICK in qualifiers:
        if (
            PassType.CHIPPED_PASS in qualifiers
            or PassType.CROSS in qualifiers
            or PassType.HIGH_PASS in qualifiers
            or PassType.LONG_BALL in qualifiers
        ):
            a = "corner_crossed"
        else:
            a = "corner_short"
    elif SetPieceType.GOAL_KICK in qualifiers:
        a = "goalkick"
    elif SetPieceType.THROW_IN in qualifiers:
        a = "throw_in"
        b = "other"
    elif PassType.CROSS in qualifiers:
        a = "cross"
    else:
        a = "pass"

    if BodyPart.KEEPER_ARM in qualifiers:
        b = "other"

    if r is None:
        if event.result in [PassResult.INCOMPLETE, PassResult.OUT]:
            r = "fail"
        elif event.result == PassResult.OFFSIDE:
            r = "offside"
        elif event.result == PassResult.COMPLETE:
            r = "success"
        else:
            # discard interrupted events
            a = "non_action"
            r = "success"

    return a, r, b


def _parse_shot_event(event: ShotEvent) -> tuple[str, str, str]:
    qualifiers = _qualifiers(event)
    b = _parse_bodypart(qualifiers)

    if SetPieceType.FREE_KICK in qualifiers:
        a = "shot_freekick"
    elif SetPieceType.PENALTY in qualifiers:
        a = "shot_penalty"
    else:
        a = "shot"

    if event.result == ShotResult.GOAL:
        r = "success"
    elif event.result == ShotResult.OWN_GOAL:
        a = "bad_touch"
        r = "owngoal"
    else:
        r = "fail"

    return a, r, b


def _parse_take_on_event(event: TakeOnEvent) -> tuple[str, str, str]:
    a = "take_on"

    if event.result == TakeOnResult.COMPLETE:
        r = "success"
    else:
        r = "fail"

    b = "foot"

    return a, r, b


def _parse_carry_event(_e: CarryEvent) -> tuple[str, str, str]:
    a = "dribble"
    r = "success"
    b = "foot"
    return a, r, b


def _parse_interception_event(event: RecoveryEvent) -> tuple[str, str, str]:
    a = "interception"
    qualifiers = _qualifiers(event)
    b = _parse_bodypart(qualifiers, default="foot")

    if event.result == InterceptionResult.LOST or event.result == InterceptionResult.OUT:
        r = "fail"
    else:
        r = "success"

    return a, r, b


def _parse_foul_event(event: FoulCommittedEvent) -> tuple[str, str, str]:
    a = "foul"
    r = "fail"
    b = "foot"

    qualifiers = _qualifiers(event)
    if CardType.FIRST_YELLOW in qualifiers:
        r = "yellow_card"
    elif CardType.SECOND_YELLOW in qualifiers:
        r = "red_card"
    elif CardType.RED in qualifiers:
        r = "red_card"

    return a, r, b


def _parse_duel_event(event: DuelEvent) -> tuple[str, str, str]:
    qualifiers = _qualifiers(event)

    a = "non_action"
    b = "foot"
    if DuelType.GROUND in qualifiers and DuelType.LOOSE_BALL not in qualifiers:
        a = "tackle"
        b = "foot"

    if event.result == DuelResult.LOST:
        r = "fail"
    else:
        r = "success"

    return a, r, b


def _parse_clearance_event(event: ClearanceEvent) -> tuple[str, str, str]:
    a = "clearance"
    r = "success"
    qualifiers = _qualifiers(event)
    b = _parse_bodypart(qualifiers)
    return a, r, b


def _parse_miscontrol_event(event: MiscontrolEvent) -> tuple[str, str, str]:
    a = "bad_touch"
    r = "fail"
    b = "foot"
    return a, r, b


def _parse_goalkeeper_event(event: GoalkeeperEvent) -> tuple[str, str, str]:
    a = "non_action"
    r = "success"
    qualifiers = _qualifiers(event)
    b = _parse_bodypart(qualifiers, default="other")

    if GoalkeeperActionType.SAVE in qualifiers:
        a = "keeper_save"
        r = "success"
    # if GoalkeeperActionType.SAVE_ATTEMPT in qualifiers:
    #     a = "keeper_save"
    #     r = "fail"
    if GoalkeeperActionType.CLAIM in qualifiers:
        a = "keeper_claim"
    if GoalkeeperActionType.SMOTHER in qualifiers:
        a = "keeper_claim"
    if GoalkeeperActionType.PUNCH in qualifiers:
        a = "keeper_punch"
    if GoalkeeperActionType.PICK_UP in qualifiers:
        a = "keeper_pick_up"
    if GoalkeeperActionType.REFLEX in qualifiers:
        pass

    return a, r, b
