"""Handle Model Serialization."""
import logging
import re
from typing import Dict, Callable, Any, List, Optional, Type

from atheneum import errors
from atheneum.db import db

LOGGER = logging.getLogger(__name__)


class BaseTransformer:
    """Base Model serializer."""

    type: Type[db.Model]

    def __init__(self, model: Type[db.Model]) -> None:
        """Initialize the base serializer."""
        self.model = model

    def serialize(self, options: Optional[List[str]]) -> Any:
        """Convert Model field and factories to dicts."""
        field_factories = self._serializers()
        if not options:
            options = list(field_factories.keys())
        ret = {}
        for key in options:
            if key not in field_factories:
                raise errors.ValidationError(
                    'Invalid key: %r. Valid keys: %r.' % (
                        key, list(sorted(field_factories.keys()))))
            factory = field_factories[key]
            val = factory()
            if val is not None:
                ret[key] = val
        return ret

    def deserialize(self,
                    json_model: dict,
                    options: Optional[List[str]]) -> Any:
        """Convert dict to Model."""
        field_factories = self._deserializers()
        if not options:
            options = list(field_factories.keys())
        for key in options:
            if key not in field_factories:
                raise errors.ValidationError(
                    'Invalid key: %r. Valid keys: %r.' % (
                        key, list(sorted(field_factories.keys()))))
            factory = field_factories[key]
            try:
                value = json_model[key]
                if value is not None:
                    factory(self.model, value)
            except KeyError as key_error:
                LOGGER.error(
                    'Unable to transform field: %s %s', key, key_error)
        return self.model

    def _serializers(self) -> Dict[str, Callable[[], Any]]:
        """Field definitions."""
        raise NotImplementedError()

    def _deserializers(
            self) -> Dict[str, Callable[[db.Model, Any], None]]:
        """Field definitions."""
        raise NotImplementedError()


_model_transformers: Dict[str, Type[BaseTransformer]] = {}


def register_transformer(
        model_serializer: Type[BaseTransformer]) -> Type[BaseTransformer]:
    """Add a model to the serializer mapping."""
    model_name = model_serializer.type.__name__
    if model_name not in _model_transformers:
        _model_transformers[model_name] = model_serializer
    else:
        raise KeyError(
            ' '.join([
                'A transformer for type "{}" already exists with class "{}".',
                'Cannot register a new transformer with class "{}"'
            ]).format(
                model_name,
                _model_transformers[model_name].__name__,
                model_serializer.__name__))
    return model_serializer


def serialize_model(model_obj: db.Model,
                    options: Optional[List[str]] = None) -> Any:
    """Lookup a Model and hand off to the serializer."""
    try:
        return _model_transformers[
            type(model_obj).__name__](model_obj).serialize(options)
    except KeyError:
        raise NotImplementedError(
            '{} has no registered serializers'.format(model_obj.__name__))


def deserialize_model(
        model_type: Type[db.Model],
        json_model_object: dict,
        options: Optional[List[str]] = None) -> db.Model:
    """Lookup a Model and hand it off to the deserializer."""
    try:
        transformer = _model_transformers[model_type.__name__]
        return transformer(
            transformer.type()).deserialize(json_model_object, options)
    except KeyError:
        raise NotImplementedError(
            '{} has no registered serializers'.format(model_type))


def convert_key_from_json(key: str) -> str:
    """Convert a key from camelCase."""
    substitute = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', substitute).lower()