import pytz

from typing import Union, Any, get_type_hints

from decimal import Decimal
from datetime import datetime, date, time

from sqlalchemy import Column
from sqlalchemy.orm import CompositeProperty
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.relationships import RelationshipProperty
from sqlalchemy.ext.hybrid import HYBRID_PROPERTY, HYBRID_METHOD

from cytra.helpers import to_camel_case
from cytra.constants import (
    ISO_DATETIME_FORMAT,
    ISO_DATE_FORMAT,
    ISO_DATETIME_PATTERN,
    ISO_TIME_FORMAT,
)


class Transformer:
    """Model transformer abstract class"""

    @classmethod
    def export_column_name(cls, name):
        """
        Export column name
        :param name:
        :return:
        """
        raise NotImplementedError  # pragma: no cover

    @classmethod
    def export_datetime(cls, value: datetime):
        """
        Export python datetime
        :param value:
        :return:
        """
        raise NotImplementedError  # pragma: no cover

    @classmethod
    def export_date(cls, value: date):
        """
        Export python date
        :param value:
        :return:
        """
        raise NotImplementedError  # pragma: no cover

    @classmethod
    def export_time(cls, value: time):
        """
        Export python time
        :param value:
        :return:
        """
        raise NotImplementedError  # pragma: no cover

    @classmethod
    def import_datetime(cls, value: Union[str, int]) -> datetime:
        """
        Import datetime field
        :param value:
        :return:
        """
        raise NotImplementedError  # pragma: no cover

    @classmethod
    def import_date(cls, value: Union[str, int]) -> date:
        """
        Import date
        :param value:
        :return:
        """
        raise NotImplementedError  # pragma: no cover

    @classmethod
    def import_time(cls, value: Union[str, int]) -> time:
        """
        Import time
        :param value:
        :return:
        """
        raise NotImplementedError  # pragma: no cover


class DefaultTransformer(Transformer):
    @classmethod
    def export_column_name(cls, key):
        return to_camel_case(key)

    @classmethod
    def export_datetime(cls, value):
        return value.isoformat()

    @classmethod
    def export_date(cls, value):
        return value.isoformat()

    @classmethod
    def export_time(cls, value):
        return value.isoformat()

    @classmethod
    def import_datetime(cls, value):
        match = ISO_DATETIME_PATTERN.match(value)
        if not match:
            raise ValueError("Invalid datetime format")

        res = datetime.strptime(match.group(1), ISO_DATETIME_FORMAT)
        if match.group(2) and len(match.group(2)) > 0:
            res = res.replace(microsecond=int(match.group(2)))

        return res

    @classmethod
    def import_date(cls, value):
        try:
            return datetime.strptime(value, ISO_DATE_FORMAT).date()
        except ValueError:
            raise ValueError("Invalid date format")

    @classmethod
    def import_time(cls, value):
        try:
            return datetime.strptime(value, ISO_TIME_FORMAT).time()
        except ValueError:
            raise ValueError("Invalid date format")


class DefaultTransformerUTC(DefaultTransformer):
    @classmethod
    def export_datetime(cls, value):
        value = value.replace(tzinfo=pytz.utc)
        return super().export_datetime(value)

    @classmethod
    def export_time(cls, value):
        value = value.replace(tzinfo=pytz.utc)
        return super().export_time(value)


class TransformMixin:
    """
    Transform columns

    Note: All mixins must respect to this mixin while intracting with
    outside of the model.
    """

    __transformer__ = DefaultTransformerUTC

    @classmethod
    def import_value(cls, c: Column, v: Union[None, str]) -> Any:
        """
        Import value for a column.
        :param column:
        :param v:
        :return:
        """
        if v is None:
            return v

        if isinstance(c, Column) or isinstance(c, InstrumentedAttribute):
            try:
                type_ = c.type.python_type
            except NotImplementedError:
                type_ = None

        elif hasattr(c, "descriptor"):
            # using type annotation for hybrid_property
            cd = c.descriptor
            if (
                hasattr(cd, "extension_type")
                and cd.extension_type == HYBRID_PROPERTY
            ):
                type_ = get_type_hints(cd.fget).get("return")
            else:
                type_ = None

        elif (
            hasattr(c, "extension_type")
            and c.extension_type == HYBRID_PROPERTY
        ):
            type_ = get_type_hints(c.fget).get("return")

        else:  # pragma: nocover
            type_ = None

        if type_ is bool and not isinstance(v, bool):
            return str(v).lower() == "true"

        if type_ == datetime:
            return cls.__transformer__.import_datetime(v)

        if type_ == date:
            return cls.__transformer__.import_date(v)

        if type_ == time:
            return cls.__transformer__.import_time(v)

        return v

    @classmethod
    def export_value(cls, c: Column, v: Any) -> tuple:
        """
        Prepare column value to export.
        :param c:
        :param v:
        :return:
        """
        if (
            hasattr(c, "property")
            and isinstance(c.property, RelationshipProperty)
            and c.property.uselist
        ):
            return [c.to_dict() for c in v]

        if hasattr(c, "property") and isinstance(
            c.property, CompositeProperty
        ):
            return v.__composite_values__()

        if v is None:
            return v

        if isinstance(v, datetime):
            return cls.__transformer__.export_datetime(v)

        if isinstance(v, date):
            return cls.__transformer__.export_date(v)

        if isinstance(v, time):
            return cls.__transformer__.export_time(v)

        if hasattr(v, "to_dict"):
            return v.to_dict()

        if isinstance(v, Decimal):
            return str(v)

        if c.extension_type == HYBRID_METHOD:
            return v(cls)

        return v

    @classmethod
    def export_column_name(cls, col_key: str, col_info: dict) -> str:
        return cls.__transformer__.export_column_name(
            col_info.get("dict_key", col_key)
        )
