import re
from functools import wraps, partial
from pydantic import ValidationError, BaseModel
import falcon

from falibrary.config import default_config
from falibrary.route import OpenAPI, RedocPage
from falibrary.utils import find_routes, parse_path


class Falibrary:
    """
    :param app: Falcon instance
    :param kwargs: key-value for config
    """

    def __init__(self, app=None, **kwargs):
        self.app = app
        self.models = {}
        self.config = default_config
        for key, value in kwargs.items():
            setattr(self.config, key.upper(), value)

        self.STATUS = re.compile(r'(?P<code>^\d{3}) (?P<msg>[\w ]+$)')
        if self.app:
            assert isinstance(app, falcon.API)
            self._register_route()

    def register(self, app):
        assert isinstance(app, falcon.API)
        self.app = app
        self._register_route()

    def validate(self, query=None, data=None, resp=None, x=[]):
        """
        validate query, JSON data, and response according to
        ``pydantic.BaseModel``

        :param query: Schema for query args
        :param data: Schema for JSON data
        :param response: Schema for JSON response
        :param x: List of :class:`falcon.status_codes`
        """
        def decorator_validation(func):
            @wraps(func)
            def validation(self, _req, _resp, *args, **kwargs):
                try:
                    if query:
                        setattr(_req.context, 'query', query(**_req.params))
                    if data:
                        setattr(_req.context, 'data', data(**_req.media))
                except ValidationError as err:
                    raise falcon.HTTPUnprocessableEntity(
                        'Schema failed validation',
                        description=str(err),
                    )
                except Exception:
                    raise

                response = func(self, _req, _resp, *args, **kwargs)
                if resp:
                    _resp.media = response.dict()

                return response

            # register ``pydantic.BaseModel``
            for name, model in zip(
                ('query', 'data', 'resp'), (query, data, resp)
            ):
                if model:
                    assert issubclass(model, BaseModel)
                    self.models[model.__name__] = model.schema()
                    setattr(validation, name, model.__name__)

            # handle exceptions
            code_msg = {}
            for exception in x:
                match = self.STATUS.match(exception)
                assert match
                code_msg[match.group('code')] = match.group('msg')

            if code_msg:
                validation.x = code_msg

            return validation
        return decorator_validation

    def _register_route(self):
        """
        register doc page and OpenAPI spec file
        """
        self.config.SPEC_URL = f'/{self.config.PATH}/{self.config.FILENAME}'
        self.app.add_route(
            f'/{self.config.PATH}',
            RedocPage(self.config)
        )
        self.app.add_route(
            self.config.SPEC_URL,
            OpenAPI(self)
        )

    @property
    def spec(self):
        if not hasattr(self, '_spec'):
            self._generate_spec()
        return self._spec

    def _generate_spec(self):
        routes = {}
        for route in find_routes(self.app._router._roots):
            path, parameters = parse_path(route.uri_template)
            routes[path] = {}
            for method, func in route.method_map.items():
                if isinstance(func, partial):
                    # ignore exception handlers
                    continue

                name = route.resource.__class__.__name__
                spec = {
                    'summary': name,
                    'operationID': name + '__' + method.lower(),
                }

                if hasattr(func, 'query'):
                    spec['requestBody'] = {
                        'content': {
                            'application/json': {
                                'schema': {
                                    '$ref': f'#/components/schemas/{func.data}'
                                }
                            }
                        }
                    }

                if hasattr(func, 'query'):
                    parameters.append({
                        'name': func.query,
                        'in': 'query',
                        'required': True,
                        'schema': {
                            '$ref': f'#/components/schemas/{func.query}',
                        }
                    })
                spec['parameters'] = parameters

                if hasattr(func, 'resp'):
                    spec['responses'] = {
                        '200': {
                            'description': 'Successful Response',
                            'content': {
                                'application/json': {
                                    'schema': {
                                        '$ref': f'#/components/schemas/{func.resp}'
                                    }
                                }
                            }
                        }
                    }
                else:
                    spec['responses'] = {
                        '200': {
                            'description': 'Successful Response',
                        }
                    }

                if any([hasattr(func, schema)
                        for schema in ('query', 'data', 'resp')]):
                    spec['responses']['422'] = {
                        'description': 'Validation Error',
                    }

                if hasattr(func, 'x'):
                    for code, msg in func.x.items():
                        spec['responses'][str(code)] = {
                            'description': msg,
                        }

                routes[path][method.lower()] = spec

        definitions = {}
        for _, schema in self.models.items():
            if 'definitions' in schema:
                for key, value in schema['definitions'].items():
                    definitions[key] = value
                del schema['definitions']

        data = {
            'openapi': self.config.OPENAPI_VERSION,
            'info': {
                'title': self.config.TITLE,
                'version': self.config.VERSION,
            },
            'paths': {
                **routes
            },
            'components': {
                'schemas': {
                    name: schema for name, schema in self.models.items()
                },
            },
            'definitions': definitions
        }
        self._spec = data
