import ipaddress
from distutils.version import StrictVersion
from typing import Type, Union

import colour
from boltons import urlutils
from flask import current_app as app, g
from marshmallow import utils
from marshmallow.fields import Field, Nested as MarshmallowNested, \
    ValidationError as _ValidationError, missing_
from marshmallow.validate import Validator
from marshmallow_enum import EnumField as _EnumField
from sqlalchemy_utils import PhoneNumber

from teal import db as tealdb
from teal.resource import Schema
from teal.utils import if_none_return_none


class Version(Field):
    """A python StrictVersion field, like '1.0.1'."""

    @if_none_return_none
    def _serialize(self, value, attr, obj):
        return str(value)

    @if_none_return_none
    def _deserialize(self, value, attr, data):
        return StrictVersion(value)


class Color(Field):
    """Any color field that can be accepted by the colour package."""

    @if_none_return_none
    def _serialize(self, value, attr, obj):
        return str(value)

    @if_none_return_none
    def _deserialize(self, value, attr, data):
        return colour.Color(value)


class URL(Field):
    def __init__(self, require_path=False,
                 default=missing_, attribute=None, data_key=None, error=None, validate=None,
                 required=False, allow_none=None, load_only=False, dump_only=False,
                 missing=missing_, error_messages=None, **metadata):
        super().__init__(default, attribute, data_key, error, validate, required, allow_none,
                         load_only, dump_only, missing, error_messages, **metadata)
        self.require_path = require_path

    @if_none_return_none
    def _serialize(self, value, attr, obj):
        return value.to_text()

    @if_none_return_none
    def _deserialize(self, value, attr, data):
        url = urlutils.URL(value)
        if url.scheme or url.host:
            if self.require_path:
                if url.path and url.path != '/':
                    return url
            else:
                return url
        raise ValueError('Not a valid URL.')


class IP(Field):
    @if_none_return_none
    def _serialize(self, value: Union[ipaddress.IPv4Address, ipaddress.IPv6Address], attr, obj):
        return str(value)

    @if_none_return_none
    def _deserialize(self, value: str, attr, data):
        return ipaddress.ip_address(value)


class Phone(Field):
    @if_none_return_none
    def _serialize(self, value: PhoneNumber, attr, obj):
        return value.international

    @if_none_return_none
    def _deserialize(self, value: str, attr, data):
        phone = PhoneNumber(value)
        if not phone.is_valid_number():
            raise ValueError('The phone number is invalid.')
        return phone


class NestedOn(MarshmallowNested):
    """A relationship with a resource schema that emulates the
    relationships in SQLAlchemy.

    It allows instantiating SQLA models when deserializing NestedOn
    values in two fashions:

    - If the :attr:`.only_query` is set, NestedOn expects a scalar
      (str, int...) value when deserializing, and tries to get
      an existing model that has such value. Typical case is setting
      :attr:`.only_query` to ``id``, and then pass-in the id
      of a nested model. In such case NestedOn will change the id
      for the model representing the ID.
    - If :attr:`.only_query` is not set, NestedOn expects the
      value to deserialize to be a dictionary, and instantiates
      the model with the values of the dictionary. In this case
      NestedOn requires :attr:`.polymorphic_on` to be set as a field,
      usually called ``type``, that references a subclass of Model;
      ex. {'type': 'SpecificDevice', ...}.

    When serializing from :meth:`teal.resource.Schema.jsonify` it
    serializes nested relationships up to a defined limit.

    :param polymorphic_on: The field name that discriminates
                               the type of object. For example ``type``.
                               Then ``type`` contains the class name
                               of a subschema of ``nested``.
    """
    NESTED_LEVEL = '_level'
    NESTED_LEVEL_MAX = '_level_max'

    def __init__(self,
                 nested,
                 polymorphic_on: str,
                 db: tealdb.SQLAlchemy,
                 collection_class=list,
                 default=missing_,
                 exclude=tuple(),
                 only_query: str = None,
                 only=None,
                 **kwargs):
        self.polymorphic_on = polymorphic_on
        self.collection_class = collection_class
        self.only_query = only_query
        assert isinstance(polymorphic_on, str)
        assert isinstance(only, str) or only is None
        super().__init__(nested, default, exclude, only, **kwargs)
        self.db = db

    def _deserialize(self, value, attr, data):
        if self.many and not utils.is_collection(value):
            self.fail('type', input=value, type=value.__class__.__name__)

        if isinstance(self.only, str):  # self.only is a field name
            if self.many:
                value = self.collection_class({self.only: v} for v in value)
            else:
                value = {self.only: value}
        # New code:
        parent_schema = app.resources[super().schema.t].SCHEMA
        if self.many:
            return self.collection_class(self._deserialize_one(single, parent_schema, attr)
                                         for single in value)
        else:
            return self._deserialize_one(value, parent_schema, attr)

    def _deserialize_one(self, value, parent_schema: Type[Schema], attr):
        if isinstance(value, dict) and self.polymorphic_on in value:
            type = value[self.polymorphic_on]
            resource = app.resources[type]
            if not issubclass(resource.SCHEMA, parent_schema):
                raise ValidationError('{} is not a sub-type of {}'.format(type, parent_schema.t),
                                      field_names=[attr])
            schema = resource.SCHEMA(only=self.only,
                                     exclude=self.exclude,
                                     context=getattr(self.parent, 'context', {}),
                                     load_only=self._nested_normalized_option('load_only'),
                                     dump_only=self._nested_normalized_option('dump_only'))
            schema.ordered = getattr(self.parent, 'ordered', False)
            value = schema.load(value)
            model = self._model(type)(**value)
        elif self.only_query:  # todo test only_query
            model = self._model(parent_schema.t).query.filter_by(**{self.only_query: value}).one()
        else:
            raise ValidationError('\'Type\' field required to disambiguate resources.',
                                  field_names=[attr])
        assert isinstance(model, tealdb.Model)
        return model

    def _model(self, type: str) -> Type[tealdb.Model]:
        """Given the type of a model it returns the model class."""
        return self.db.Model._decl_class_registry.data[type]()

    def serialize(self, attr: str, obj, accessor=None) -> dict:
        """See class docs."""
        if g.get(NestedOn.NESTED_LEVEL) == g.get(NestedOn.NESTED_LEVEL_MAX):
            # Idea from https://marshmallow-sqlalchemy.readthedocs.io
            # /en/latest/recipes.html#smart-nested-field
            # Gets the FK of the relationship instead of the full object
            # This won't work for many-many relationships (as they are lists)
            # In such case return None
            # todo is this the behaviour we want?
            return getattr(obj, attr + '_id', None)
        setattr(g, NestedOn.NESTED_LEVEL, g.get(NestedOn.NESTED_LEVEL) + 1)
        ret = super().serialize(attr, obj, accessor)
        setattr(g, NestedOn.NESTED_LEVEL, g.get(NestedOn.NESTED_LEVEL) - 1)
        return ret


class IsType(Validator):
    """
    Validator which succeeds if the value it is passed is a registered
    resource type.

    :param parent: If set, type must be a subtype of such resource.
                   By default accept any resource.
    """
    # todo remove if not needed
    no_type = 'Type does not exist.'
    no_subtype = 'Type is not a descendant type of {parent}'

    def __init__(self, parent: str = None) -> None:
        self.parent = parent  # type: str

    def _repr_args(self):
        return 'parent={0!r}'.format(self.parent)

    def __call__(self, type: str):
        assert not self.parent or self.parent in app.resources
        try:
            r = app.resources[type]
            if self.parent:
                if not issubclass(r.__class__, app.resources[self.parent].__class__):
                    raise ValidationError(self.no_subtype.format(self.parent))
        except KeyError:
            raise ValidationError(self.no_type)


class ValidationError(_ValidationError):
    code = 422


class EnumField(_EnumField):
    """
    An EnumField that allows
    generating OpenApi enums through Apispec.
    """

    def __init__(self, enum, by_value=False, load_by=None, dump_by=None, error='', *args,
                 **kwargs):
        super().__init__(enum, by_value, load_by, dump_by, error, *args, **kwargs)
        self.metadata['enum'] = [e.name for e in enum]
