import pymongo
from yaml import load
from flask import Flask
from flask_swagger_ui import get_swaggerui_blueprint
from crudcast.models import Model
from crudcast.users import User
from crudcast.methods import Method
import os


class CrudcastApp(Flask):
    """
    The main Crudcast app object, which extends the Flask app

    :param import_name: The `import_name` parameter for Flask
    :param config_file: Local path to a valid `config.yml`
    :param kwargs: additional arguments to be passed to Flask
    """
    user_manager = None

    crudcast_config = {
        # database
        "mongo_url": "mongodb://localhost:27017/",
        "db_name": "database",

        # swagger
        'swagger': {
            'swagger': '2.0',
            'basePath': '/api',
            'url': '/api/docs',
            'info': {
                'description': 'This is an API automatically generated by crudcast',
                'version': '1.0.0',
                'title': 'My Crudcast app'
            }
        },
    } #: this is the default crudcast config. Any options supplied in your `config.yml` will overwrite these

    models = {}
    methods = {}
    user_config = {
        'fields': {},
        'options': {
            'auth_type': 'basic'
        }
    }

    client = None
    db = None

    def set_crudcast_config(self, config_file):
        """
        Overwrites `self.crudcast_config` with the content of the config file

        :param config_file: a local file path to a valid config file
        """
        with open(config_file, 'r') as f:
            options = load(f.read())

        for key, val in options.items():
            if key not in ['models', 'methods']:
                self.crudcast_config[key] = val

        self.client = pymongo.MongoClient(self.crudcast_config['mongo_url'])
        self.db = self.client[self.crudcast_config['db_name']]

        users = options.get('users')
        if not users:
            users = {}

        for key, val in users.items():
            self.user_config[key] = val

        if 'users' in options.keys():
            self.crudcast_config['users'] = users

        for model_name, model_options in options['models'].items():
            m = {
                'name': model_name,
                'collection': self.db[model_name],
                'fields': model_options.pop('fields', {}),
                'options': model_options
            }
            self.models[model_name] = m
            if 'users' in options.keys():
                self.models['user'] = {
                    'name': 'user',
                    'collection': self.db['user'],
                    'fields': self.user_config['fields'],
                    'options': self.user_config['options']
                }

        for method in options.get('methods', []):
            file = os.path.abspath(method.pop('file'))
            self.methods[method['path']] = Method(file=file, **method)

    def get_tag(self, model):
        """
        Returns the tag name/description to be used in the swagger view for each model

        :type model: crudcast.modles.Model
        :rtype: dict
        """
        return {
            'name': model.name,
            'description': model.options.get('description')
        }

    def get_model_path(self, model):
        """
        Generates a path entry for the swagger view for each model

        :type model: crudcast.models.Model
        :rtype: dict
        """
        parameters = []

        auth_type = model.options.get('auth_type')

        for field in model.fields:
            parameter = {
                'name': field.name,
                'in': 'query',
                'description': 'Filter for %s objects based on %s' % (model.name, field.name),
                'required': False,
            }

            parameters.append(parameter)

        post_parameters = [
            {
                'name': 'body',
                'in': 'body',
                'required': True,
                'description': 'New %s object' % model.name,
                'schema': {
                    '$ref': '#/definitions/%s' % model.name
                }
            }
        ]

        return {
            'get': {
                'tags': [model.name],
                'summary': 'List all %s objects' % model.name,
                'consumes': 'application/json',
                'produces': 'application/json',
                'parameters': parameters,
                'responses': {
                    '200': {
                        'description': 'successful operation',
                        'schema': {
                            '$ref': '#/definitions/%s' % model.name
                        }
                    }
                },
                'security': [
                    {
                        'basicAuth': []
                    }
                ] if auth_type else []
            },
            'post': {
                'tags': [model.name],
                'summary': 'Create a new %s' % model.name,
                'consumes': 'application/json',
                'produces': 'application/json',
                'parameters': post_parameters,
                'responses': {
                    '200': {
                        'description': 'object created',
                        'schema': {
                            '$ref': '#/definitions/%s' % model.name
                        }
                    }
                },
                'security': [
                    {
                        'basicAuth': []
                    }
                ] if auth_type else []
            }
        }

    def get_definition(self, model):
        """
        Returns the model definition based on the model's fields

        :type model: crudcast.models.Model
        :rtype: dict
        """
        definition_properties = {
            '_id': {
                'type': 'string',
            }
        }
        for field in model.fields:
            param = {
                'type': field.type
            }
            if field.type == 'array':
                param['items'] = {'type': 'string'}
            definition_properties[field.name] = param
        return {
            'type': 'object',
            'required': [field.name for field in model.fields if field.required],
            'properties': definition_properties
        }

    def get_instance_path(self, model):
        """
        Returns the path entries for instance level calls such as PUT, DELETE, RETRIEVE

        :type model: crudcast.models.Model
        :rtype: dict
        """
        parameters = [
            {
                'name': '_id',
                'in': 'path',
                'required': True,
                'type': 'string',
                'description': 'ID of %s object' % model.name
            }
        ]
        post_parameters = [
            {
                'name': '_id',
                'in': 'path',
                'required': True,
                'type': 'string',
                'description': 'ID of %s object' % model.name
            },
            {
                'name': 'body',
                'in': 'body',
                'required': True,
                'description': 'New %s object' % model.name,
                'schema': {
                    '$ref': '#/definitions/%s' % model.name
                }
            }
        ]
        return {
            'get': {
                'tags': [model.name],
                'summary': 'Retrieve a %s object' % model.name,
                'parameters': parameters,
                'consumes': 'application/json',
                'produces': 'application/json',
                'responses': {
                    '200': {
                        'description': 'successful operation',
                        'schema': {
                            '$ref': '#/definitions/%s' % model.name
                        }
                    }
                }
            },
            'put': {
                'tags': [model.name],
                'summary': 'Update a %s object' % model.name,
                'consumes': 'application/json',
                'produces': 'application/json',
                'parameters': post_parameters,
                'responses': {
                    '200': {
                        'description': 'successful operation',
                        'schema': {
                            '$ref': '#/definitions/%s' % model.name
                        }
                    }
                }
            },
            'delete': {
                'tags': [model.name],
                'summary': 'Delete a %s object' % model.name,
                'parameters': parameters,
                'consumes': 'application/json',
                'produces': 'application/json',
                'responses': {
                    '200': {
                        'description': 'successful operation',
                        'schema': {
                            '$ref': '#/definitions/%s' % model.name
                        }
                    }
                }
            }
        }

    def get_security_definitions(self):
        return {
                'basicAuth': {
                    'type': 'basic',
                }
        }

    @property
    def swagger_config(self):
        """
        Retrieves the complete swagger configuration

        :rtype: dict
        """
        config = self.crudcast_config['swagger']
        tags = []
        paths = {}
        definitions = {}

        for model_name, model_attrs in self.models.items():
            model = Model(model_name, self)
            tags.append(self.get_tag(model))
            paths['/%s/' % model_name] = self.get_model_path(model)
            paths['/%s/{_id}/' % model_name] = self.get_instance_path(model)
            definitions[model.name] = self.get_definition(model)

        for method_path, method in self.methods.items():
            paths['/' + method_path] = method.swagger_definition

        config['tags'] = tags
        config['paths'] = paths
        config['definitions'] = definitions

        if self.user_manager:
            config['securityDefinitions'] = self.get_security_definitions()

        return config

    def get_swagger_ui_view(self):
        """
        Creates a swagger view using `flask_swagger_ui`

        :return: a swagger view
        """
        return get_swaggerui_blueprint(
            self.crudcast_config['swagger']['url'],
            '/swagger',
        )

    def __init__(self, import_name, config_file, **kwargs):
        self.set_crudcast_config(config_file)

        if 'users' in self.crudcast_config.keys():
            self.user_manager = User(app=self, **self.crudcast_config['users'])

        super().__init__(import_name, **kwargs)



