import io
import json
from datetime import date, datetime, time, timedelta
from decimal import Decimal, InvalidOperation
from enum import Enum
from types import UnionType
from typing import TYPE_CHECKING, Any, Literal, Union, get_args, get_origin

if TYPE_CHECKING:
    import pandas as pd

from yera.agents.typing.utils import get_type_name
from yera.agents.typing.validate import is_enum_type, is_struct_type
from yera.dsl.struct import Struct

from .validate import is_dataframe_type


def coerce_set(value: Any, expected_type: Any) -> set:
    if isinstance(value, str):
        try:
            value = json.loads(value)
        except json.JSONDecodeError as e:
            raise TypeError(f"Cannot parse set from JSON string: {e}") from None

    if isinstance(value, set | list):
        items = value
    else:
        raise TypeError(f"Expected set or list, got {type(value).__name__}")

    type_args = get_args(expected_type)
    if not type_args:
        raise TypeError(
            "Set type must specify element type (e.g., set[int], not plain set)"
        )

    element_type = type_args[0]
    return {coerce_value(item, element_type) for i, item in enumerate(items)}


def coerce_datetime(value: Any) -> datetime:
    """Coerce to datetime."""
    if isinstance(value, datetime):
        return value
    if isinstance(value, str):
        # Try ISO format first
        try:
            return datetime.fromisoformat(value.replace("Z", "+00:00"))
        except ValueError:
            # Fallback to common formats
            for fmt in [
                "%Y-%m-%d %H:%M:%S",
                "%Y-%m-%dT%H:%M:%S",
                "%Y-%m-%d %H:%M:%S.%f",
                "%Y-%m-%dT%H:%M:%S.%f",
            ]:
                try:
                    return datetime.strptime(value, fmt)
                except ValueError:  # noqa: PERF203
                    continue
            raise TypeError(f"Cannot parse datetime from string: {value!r}") from None
    elif isinstance(value, int | float):
        # Unix timestamp
        try:
            return datetime.fromtimestamp(value)
        except (ValueError, OSError) as e:
            raise TypeError(
                f"Cannot convert timestamp {value} to datetime: {e}"
            ) from None
    else:
        raise TypeError(f"Cannot convert {type(value).__name__} to datetime")


def coerce_date(value: Any) -> date:
    """Coerce to date."""
    if isinstance(value, date) and not isinstance(value, datetime):
        return value
    if isinstance(value, datetime):
        return value.date()
    if isinstance(value, str):
        try:
            return date.fromisoformat(value)
        except ValueError:
            # Try parsing as datetime and extract date
            try:
                dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
                return dt.date()
            except ValueError:
                raise TypeError(f"Cannot parse date from string: {value!r}") from None
    else:
        raise TypeError(f"Cannot convert {type(value).__name__} to date")


def coerce_time(value: Any) -> time:
    """Coerce to time."""
    if isinstance(value, time):
        return value
    if isinstance(value, str):
        try:
            return time.fromisoformat(value)
        except ValueError:
            raise TypeError(
                f"Cannot convert {type(value).__name__} to time. "
                f"Expected ISO format time."
            ) from None
    else:
        raise TypeError(f"Cannot convert {type(value).__name__} to time")


def coerce_timedelta(value: Any) -> timedelta:
    """Coerce to timedelta."""
    if isinstance(value, timedelta):
        return value
    if isinstance(value, str):
        try:
            return timedelta(seconds=float(value))
        except ValueError:
            raise TypeError(
                f"Cannot convert timedelta from invalid string: {value!r}. "
                f"Expected a float for number of seconds."
            ) from None
    else:
        raise TypeError(f"Cannot convert {type(value).__name__} to timedelta")


def coerce_decimal(value: Any) -> Decimal:
    """Coerce to Decimal."""
    if isinstance(value, Decimal):
        return value
    if isinstance(value, int | float | str):
        try:
            return Decimal(str(value))
        except (ValueError, InvalidOperation) as e:
            raise TypeError(f"Cannot convert {value!r} to Decimal: {e}") from None
    else:
        raise TypeError(f"Cannot convert {type(value).__name__} to Decimal")


def coerce_enum(value: Any, expected_type: type[Enum]) -> Enum:
    """Coerce to Enum type."""
    if isinstance(value, expected_type):
        return value
    if isinstance(value, str):
        # Try by name first
        try:
            return expected_type[value]
        except KeyError:
            valid_names = [e.name for e in expected_type]
            raise TypeError(
                f"'{value}' string value is not a valid {expected_type.__name__}. "
                f"Valid names: {valid_names}."
            ) from None
    elif isinstance(value, int):
        try:
            return expected_type(value)
        except ValueError:
            valid_values = [e.value for e in expected_type]
            raise TypeError(
                f"{value} is not a valid {expected_type.__name__} value. "
                f"Valid values: {valid_values}"
            ) from None
    else:
        raise TypeError(
            f"Cannot convert {type(value).__name__} to {expected_type.__name__}. "
            f"Expected str or int."
        )


def serialise_dataframe(df: "pd.DataFrame") -> bytes:
    """Serialize DataFrame to Parquet bytes."""
    buffer = io.BytesIO()
    df.to_parquet(buffer, engine="pyarrow")
    return buffer.getvalue()


def coerce_dataframe(value: Any) -> "pd.DataFrame":
    """Coerce to pandas DataFrame."""
    import pandas as pd

    if isinstance(value, pd.DataFrame):
        return value
    if isinstance(value, bytes):
        return pd.read_parquet(io.BytesIO(value), engine="pyarrow")
    raise TypeError(
        f"Cannot convert {type(value).__name__} to DataFrame. "
        f"Must be a pandas DataFrame or parquet bytes."
    )


def coerce_none(value: Any) -> None:
    """Coerce to None type."""
    if value is None or value == "None":
        return
    raise TypeError(f"Expected None, got {type(value).__name__}")


def coerce_bool(value: Any) -> bool:
    """Coerce to bool with proper string handling."""
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        if value.lower() == "true":
            return True
        if value.lower() == "false":
            return False
        raise TypeError(
            f"Cannot convert {value!r} to bool. "
            f"Expected one of: true, false, 1, 0, yes, no, y, n, on, off (case-insensitive)"
        )
    if isinstance(value, int):
        return bool(value)
    raise TypeError(f"Cannot convert {type(value).__name__} to bool")


def coerce_str(value: Any) -> str:
    """Coerce to str with proper string handling."""
    try:
        return str(value)
    except (ValueError, TypeError) as e:
        raise TypeError(f"Cannot convert {value!r} to str. ") from None


def coerce_int(value: Any) -> int:
    """Coerce to int with proper string handling."""
    try:
        return int(value)
    except (ValueError, TypeError) as e:
        raise TypeError(f"Cannot convert {value!r} to int") from None


def coerce_float(value: Any) -> float:
    """Coerce to float with proper string handling."""
    try:
        return float(value)
    except (ValueError, TypeError) as e:
        raise TypeError(f"Cannot convert {value!r} to float") from None


def coerce_literal(value: Any, expected_type: Any) -> Any:
    """Coerce to Literal type."""
    allowed_values = get_args(expected_type)

    # First try exact match
    if value in allowed_values:
        return value

    # Try coercing to match one of the literal values
    for literal_value in allowed_values:
        literal_type = type(literal_value)

        # Skip None in literals for coercion attempts
        if literal_value is None:
            continue

        try:
            # Try to coerce the input value to the type of this literal
            coerced = coerce_value(value, literal_type)
            if coerced == literal_value:
                return literal_value
        except (TypeError, ValueError):
            continue

    # If no match found, raise error with helpful message
    raise TypeError(
        f"Value {value!r} is not one of the allowed literal values: {allowed_values}"
    )


def coerce_struct(value: Any, expected_type: type[Struct]) -> Any:
    """Coerce to Struct (Pydantic model) type."""
    if isinstance(value, expected_type):
        return value
    if isinstance(value, dict):
        return expected_type.model_validate(value)
    if isinstance(value, str):
        return expected_type.model_validate_json(value)
    raise TypeError(
        f"Cannot convert {type(value).__name__} to {expected_type.__name__}. "
        f"Expected dict or JSON string."
    )


def coerce_union(value: Any, expected_type: Any) -> Any:
    """Coerce to Union type by trying each type in order."""
    type_args = get_args(expected_type)
    last_error = None

    for arg_type in type_args:
        try:
            return coerce_value(value, arg_type)
        except (TypeError, ValueError) as e:  # noqa: PERF203
            last_error = e
            continue

    raise TypeError(
        f"Cannot coerce value to any of {type_args}. Last error: {last_error}"
    )


def coerce_list(value: Any, expected_type: Any) -> list:
    """Coerce to list[T] type."""
    if isinstance(value, str):
        try:
            value = json.loads(value)
        except json.JSONDecodeError as e:
            raise TypeError(f"Cannot parse list from JSON string: {e}") from None

    if not isinstance(value, list):
        raise TypeError(f"Expected list, got {type(value).__name__}")

    type_args = get_args(expected_type)
    if not type_args:
        raise TypeError(
            "List type must specify element type (e.g., list[int], not plain list)"
        )

    element_type = type_args[0]
    return [coerce_value(item, element_type) for i, item in enumerate(value)]


def coerce_dict(value: Any, expected_type: Any) -> dict:
    """Coerce to dict[K, V] type."""
    # Handle string input (JSON)
    if isinstance(value, str):
        try:
            value = json.loads(value)
        except json.JSONDecodeError as e:
            raise TypeError(f"Cannot parse dict from JSON string: {e}") from None

    if not isinstance(value, dict):
        raise TypeError(f"Expected dict, got {type(value).__name__}")

    type_args = get_args(expected_type)
    if not type_args:
        raise TypeError(
            "Dict type must specify key and value types (e.g., dict[str, int], "
            "not plain dict)"
        )

    key_type, value_type = type_args[0], type_args[1]
    return {
        coerce_value(k, key_type): coerce_value(v, value_type) for k, v in value.items()
    }


def coerce_fallback(value: Any, expected_type: Any) -> Any:
    """Fallback coercion - just check isinstance."""
    raise TypeError(
        f"Unsupported type {get_type_name(expected_type)} for coercion of value "
        f"{value}. Type must be validated as allowed before coercion."
    )


PARAMETERISED_TYPES = (list, set, dict, Literal, Union, UnionType)


def is_generic(t: type) -> bool:
    return t in PARAMETERISED_TYPES


TYPE_COERCERS = {
    bool: coerce_bool,
    int: coerce_int,
    float: coerce_float,
    str: coerce_str,
    type(None): coerce_none,
    datetime: coerce_datetime,
    date: coerce_date,
    time: coerce_time,
    timedelta: coerce_timedelta,
    Decimal: coerce_decimal,
    list: coerce_list,
    dict: coerce_dict,
    set: coerce_set,
    Literal: coerce_literal,
    Union: coerce_union,
    UnionType: coerce_union,
}


def coerce_value(value: Any, expected_type: Any) -> Any:
    """Coerce a value to the expected type.

    Delegates to specialized handlers based on type structure.
    """
    if expected_type is None:
        return coerce_none(value)

    origin = get_origin(expected_type)

    if is_struct_type(expected_type):
        return coerce_struct(value, expected_type)

    if is_enum_type(expected_type):
        return coerce_enum(value, expected_type)

    if is_dataframe_type(expected_type):
        return coerce_dataframe(value)

    handler = TYPE_COERCERS.get(origin or expected_type)
    if handler is None:
        return coerce_fallback(value, expected_type)

    if is_generic(origin or expected_type):
        # noinspection PyArgumentList
        return handler(value, expected_type)

    return handler(value)
