from __future__ import absolute_import

import glob
import json
import logging
import os.path
import yaml

# Import third party libs
try:
    from jsonschema import validate as _validate
    from jsonschema.validators import validator_for as _validator_for
    from jsonschema.exceptions import SchemaError, ValidationError
    HAS_JSONSCHEMA = True
except ImportError:
    HAS_JSONSCHEMA = False

__virtualname__ = 'modelschema'

LOG = logging.getLogger(__name__)


def __virtual__():
    """
    Only load if jsonschema library exist.
    """
    if not HAS_JSONSCHEMA:
        return (
            False,
            'Can not load module jsonschema: jsonschema library not found')
    return __virtualname__


def _get_base_dir():
    return __salt__['config.get']('pilllar_schema_path',
                                  '/usr/share/salt-formulas/env')


def _dict_deep_merge(a, b, path=None):
    """
    Merges dict(b) into dict(a)
    """
    if path is None:
        path = []
    for key in b:
        if key in a:
            if isinstance(a[key], dict) and isinstance(b[key], dict):
                _dict_deep_merge(a[key], b[key], path + [str(key)])
            elif a[key] == b[key]:
                pass  # same leaf value
            else:
                raise Exception(
                    'Conflict at {}'.format('.'.join(path + [str(key)])))
        else:
            a[key] = b[key]
    return a


def schema_list():
    """
    Returns list of all defined schema files.

    CLI Examples:

    .. code-block:: bash

        salt-call modelutils.schema_list


    """
    output = {}
    schemas = glob.glob('{}/*/schemas/*.yaml'.format(_get_base_dir()))
    for schema in schemas:
        if os.path.exists(schema):
            role_name = schema.split('/')[-1].replace('.yaml', '')
            service_name = schema.split('/')[-3]
            print role_name, service_name
            name = '{}-{}'.format(service_name, role_name)
            output[name] = {
                'service': service_name,
                'role': role_name,
                'path': schema,
                'valid': schema_validate(service_name, role_name)[name]
            }
    return output


def schema_get(service, role):
    """
    Returns pillar schema for given service and role. If no service and role
    is specified, method will return all known schemas.

    CLI Examples:

    .. code-block:: bash

        salt-call modelutils.schema_get ntp server

    """
    schema_path = 'salt://{}/schemas/{}.yaml'.format(service, role)
    schema = __salt__['cp.get_file_str'](schema_path)
    if schema:
        try:
            data = yaml.safe_load(schema)
        except yaml.YAMLError as exc:
            raise Exception("Failed to parse schema:{}\n"
                            "{}".format(schema_path, exc))
    else:
        raise Exception("Schema not found:{}".format(schema_path))
    return {'{}-{}'.format(service, role): data}


def schema_validate(service, role):
    """
    Validates pillar schema itself of given service and role.

    CLI Examples:

    .. code-block:: bash

        salt-call modelutils.schema_validate ntp server

    """

    schema = schema_get(service, role)['{}-{}'.format(service, role)]
    cls = _validator_for(schema)
    LOG.debug("Validating schema..")
    try:
        cls.check_schema(schema)
        LOG.debug("Schema is valid")
        data = 'Schema is valid'
    except SchemaError as exc:
        LOG.error("SchemaError:{}".format(exc))
        raise Exception("SchemaError")
    return {'{}-{}'.format(service, role): data}


def model_validate(service=None, role=None):
    """
    Validates pillar metadata by schema for given service and role. If
    no service and role is specified, method will validate all defined
    services.

    CLI Example:
    .. code-block:: bash
        salt-run modelschema.model_validate keystone server

    """
    schema = schema_get(service, role)['{}-{}'.format(service, role)]
    model = __salt__['pillar.get']('{}:{}'.format(service, role))
    try:
        _validate(model, schema)
        data = 'Model is valid'
    except SchemaError as exc:
        LOG.error("SchemaError:{}".format(exc))
        raise Exception("SchemaError")
    except ValidationError as exc:
        LOG.error("ValidationError:{}\nInstance:{}\n"
                  "Schema title:{}\n"
                  "SchemaPath:{}".format(exc.message,
                                         exc.instance,
                                         exc.schema.get(
                                             "title",
                                             "Schema title not set!"),
                                         exc.schema_path))
        raise Exception("ValidationError")
    return {'{}-{}'.format(service, role): data}


def data_validate(model, schema):
    """
    Validates model by given schema.

    CLI Example:
    .. code-block:: bash
        salt-run modelschema.data_validate {'a': 'b'} {'a': 'b'}
    """
    try:
        _validate(model, schema)
        data = 'Model is valid'
    except SchemaError as exc:
        LOG.error("SchemaError:{}".format(exc))
        raise Exception("SchemaError")
    except ValidationError as exc:
        LOG.error("ValidationError:{}\nInstance:{}\n"
                  "Schema title:{}\n"
                  "SchemaPath:{}".format(exc.message,
                                         exc.instance,
                                         exc.schema.get(
                                             "title",
                                             "Schema title not set!"),
                                         exc.schema_path))
        raise Exception("ValidationError")
    return data


def schema_from_tests(service):
    """
    Generate pillar schema skeleton for given service. Method iterates throught
    test pillars and generates schema scaffold structure in JSON format that
    can be passed to service like http://jsonschema.net/ to get the basic
    schema for the individual roles of the service.

    CLI Examples:

    .. code-block:: bash

        salt-call modelutils.schema_from_tests keystone
    """
    pillars = glob.glob(
        '{}/{}/tests/pillar/*.sls'.format(_get_base_dir(), service))
    raw_data = {}
    for pillar in pillars:
        if os.path.exists(pillar):
            with open(pillar, 'r') as stream:
                try:
                    data = yaml.load(stream)
                except yaml.YAMLError as exc:
                    data = {}
                    LOG.error('{}: {}'.format(pillar, repr(exc)))
            try:
                _dict_deep_merge(raw_data, data)
            except Exception as exc:
                LOG.error('{}: {}'.format(pillar, repr(exc)))
    if service not in raw_data.keys():
        LOG.error("Could not find applicable  data "
                  "for:{}\n at:{}".format(service, _get_base_dir()))
        raise Exception("DataError")

    data = raw_data[service]
    output = {}
    for role_name, role in data.items():
        output[role_name] = json.dumps(role)
    return output