from collections import namedtuple
import redis
from .field import Field
import logging
from ..models.db_key import DB_key

logger = logging.getLogger("POPOTO.GeoField")
from ..redis_db import POPOTO_REDIS_DB


class GeoField(Field):
    """
    A field that stores geospatial coordinates and enables geospatial search
    required: latitude, longitude
    """

    Coordinates = namedtuple("Coordinates", "latitude longitude")

    type: type = tuple
    latitude: float = None
    longitude: float = None
    default: tuple = Coordinates(None, None)
    null: bool = True

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        geofield_defaults = {
            "type": tuple,
            "latitude": None,
            "longitude": None,
            "null": True,
            "default": GeoField.Coordinates(None, None),
        }
        self.field_defaults.update(geofield_defaults)
        # set field options, let kwargs override
        for k, v in geofield_defaults.items():
            setattr(self, k, kwargs.get(k, v))

    def get_filter_query_params(self, field_name) -> set:
        return (
            super()
            .get_filter_query_params(field_name)
            .union(
                {
                    f"{field_name}",
                    f"{field_name}__isnull",
                    f"{field_name}_latitude",
                    f"{field_name}_longitude",
                    f"{field_name}_radius",
                    f"{field_name}_radius_unit",
                }
            )
        )

    @classmethod
    def is_valid(cls, field, value, null_check=True, **kwargs) -> bool:
        if not super().is_valid(field, value, null_check):
            return False
        if value is None:
            return True
        if isinstance(value, GeoField.Coordinates):
            pass
        elif isinstance(value, tuple):
            value = GeoField.Coordinates(value[0], value[1])
        else:
            logger.error(
                f"GeoField MUST be type GeoField.Coordinates or tuple, NOT {type(value)}"
            )
            return False
        if field.null and not any([value.latitude, value.longitude]):
            return True
        elif bool(value.latitude) != bool(value.longitude):
            logger.error(
                f"latitude is {value.latitude} and longitude is {value.longitude}"
            )
            logger.error(f"BOTH latitude AND longitude MUST have a value or be None")
            return False
        elif null_check and not all([value.latitude, value.longitude]):
            logger.error(
                f"latitude is {value.latitude} and longitude is {value.longitude}"
            )
            logger.error(f"BOTH latitude AND longitude MUST have a value")
            return False
        try:
            float(value.latitude), float(value.longitude)
        except ValueError as e:
            logger.error(e)
            return False
        except TypeError as e:
            logger.error(e)
            return False
        return True

    def format_value_pre_save(self, field_value):
        """
        format field_value before saving to db
        return corrected field_value
        assumes validation is already passed
        """
        if isinstance(field_value, GeoField.Coordinates):
            return field_value
        if isinstance(field_value, tuple):
            return GeoField.Coordinates(field_value[0], field_value[1])
        if self.null:
            return GeoField.Coordinates(None, None)
        return field_value

    @classmethod
    def get_geo_db_key(cls, model, field_name: str) -> DB_key:
        return cls.get_special_use_field_db_key(model, field_name)

    @classmethod
    def on_save(
        cls,
        model_instance: "Model",
        field_name: str,
        field_value: "GeoField.Coordinates",
        pipeline=None,
        **kwargs,
    ):
        geo_db_key = cls.get_geo_db_key(model_instance, field_name)
        geo_member = model_instance.db_key.redis_key
        if not field_value or not (field_value.longitude and field_value.latitude):
            if pipeline:
                return pipeline.zrem(geo_db_key.redis_key, geo_member)
            else:
                return POPOTO_REDIS_DB.zrem(geo_db_key.redis_key, geo_member)
        if pipeline:
            return pipeline.geoadd(
                name=geo_db_key.redis_key,
                values=[field_value.longitude, field_value.latitude, geo_member],
            )
        else:
            return POPOTO_REDIS_DB.geoadd(
                name=geo_db_key.redis_key,
                values=[field_value.longitude, field_value.latitude, geo_member],
            )

    @classmethod
    def on_delete(
        cls,
        model_instance: "Model",
        field_name: str,
        field_value,
        pipeline: redis.client.Pipeline = None,
        **kwargs,
    ):
        geo_db_key = cls.get_geo_db_key(model_instance, field_name)
        geo_member = model_instance.db_key.redis_key
        if pipeline:
            return pipeline.zrem(geo_db_key.redis_key, geo_member)
        else:
            return POPOTO_REDIS_DB.zrem(geo_db_key.redis_key, geo_member)

    @classmethod
    def filter_query(cls, model: "Model", field_name: str, **query_params) -> set:
        """
        :param model: the popoto.Model to query from
        :param field_name: the name of the field being filtered on
        :param query_params: dict of filter args and values
        :return: set{db_key, db_key, ..}
        """
        field = model._meta.fields[field_name]
        geo_db_key = cls.get_geo_db_key(model, field_name)
        coordinates = GeoField.Coordinates(None, None)
        member, radius, unit = None, 1, "m"
        for query_param, query_value in query_params.items():

            if query_param == f"{field_name}":
                if isinstance(query_value, GeoField.Coordinates):
                    coordinates = query_value
                elif not isinstance(query_value, tuple) or not len(query_value) == 2:
                    from ..models.query import QueryException

                    raise QueryException(
                        f"{query_param} must be assigned a tuple = (latitude, longitude)"
                    )
                else:
                    coordinates = GeoField.Coordinates(query_value[0], query_value[1])

            elif query_param.endswith("_latitude"):
                coordinates = GeoField.Coordinates(query_value, coordinates.longitude)
            elif query_param.endswith("_longitude"):
                coordinates = GeoField.Coordinates(coordinates.latitude, query_value)

            elif "_member" in query_param:
                if not isinstance(query_value, model):
                    from ..models.query import QueryException

                    raise QueryException(
                        f"{query_param} must be assigned a tuple = (latitude, longitude)"
                    )
                member = query_value

            elif query_param.endswith("_radius"):
                radius = query_value

            elif query_param.endswith("_radius_unit"):
                if query_value not in ["m", "km", "ft", "mi"]:
                    from ..models.query import QueryException

                    raise QueryException(f"{query_param} must be one of m|km|ft|mi ")
                unit = query_value

        if member:
            redis_db_keys_list = POPOTO_REDIS_DB.georadiusbymember(
                geo_db_key.redis_key,
                member=member.db_key.redis_key,
                radius=radius,
                unit=unit,  # , withdist=True, sort='asc'
            )

        elif coordinates.latitude and coordinates.longitude:
            # logger.debug(f"geo query on {dict(model=model._meta.db_class_key, longitude=coordinates.longitude, latitude=coordinates.latitude, radius=radius, unit=unit)}")
            redis_db_keys_list = POPOTO_REDIS_DB.georadius(
                geo_db_key.redis_key,
                longitude=coordinates.longitude,
                latitude=coordinates.latitude,
                radius=radius,
                unit=unit,  # , withdist=True, sort='asc'
            )
            # logger.debug(f"geo query returned {redis_db_keys_list}")
        else:
            from ..models.query import QueryException

            raise QueryException(
                f"missing one or more required parameters. "
                f"geofilter requires either coordinates or instance of the same model"
            )

        return set(redis_db_keys_list)
