import io
from functools import partial

import flask
import jsonschema

from flaskel import cap


class Schemas:
    def __getattr__(self, item):
        return cap.config.SCHEMAS.get(item)


SCHEMAS = Schemas()


class JSONSchema:
    loader = partial(flask.json.loads)
    dumper = partial(flask.json.dumps, indent=4)
    marker = "3fb539deef7c4e2991f265c0a982f5ea"
    message_format = "{message}\nError in line {line}:\n{report}\n{message}"

    @classmethod
    def load_from_url(cls, url):
        """

        :param url:
        :return:
        """
        from flaskel.http import HTTPClient
        res = HTTPClient(url, raise_on_exc=True).get(url)
        return res.body

    @classmethod
    def load_from_file(cls, file):
        """

        :param file:
        :return:
        """
        with open(file[7:]) as f:
            return cls.loader(f.read())

    @classmethod
    def validate(cls, data, schema, raise_exc=False, pretty_error=True, checker=None):
        """

        :param data:
        :param schema:
        :param raise_exc:
        :param pretty_error:
        :param checker:
        :return:
        """
        if not schema:
            return True

        if type(schema) is str:
            if schema.startswith('http'):
                schema = cls.load_from_url(schema)
            if schema.startswith('file://'):
                schema = cls.load_from_file(schema)

        try:
            checker = checker or jsonschema.FormatChecker()
            jsonschema.validate(data, schema, format_checker=checker)
        except jsonschema.ValidationError as exc:
            if not raise_exc:
                if pretty_error:
                    cap.logger.error(cls.error_report(exc, data))
                else:
                    cap.logger.exception(exc)
                return False
            raise
        return True

    @classmethod
    def error_report(cls, e, json_object, lines_before=8, lines_after=8):
        """
        From: https://github.com/ccpgames/jsonschema-errorprinter/blob/master/jsonschemaerror.py

        Generate a detailed report of a schema validation error.
        'e' is a jsonschema.ValidationError exception that errored on
        'json_object'.
        Steps to discover the location of the validation error:
        1. Traverse the json object using the 'path' in the validation exception
           and replace the offending value with a special marker.
        2. Pretty-print the json object indendented json text.
        3. Search for the special marker in the json text to find the actual
           line number of the error.
        4. Make a report by showing the error line with a context of
          'lines_before' and 'lines_after' number of lines on each side.
        """
        if not e.path:
            return e.message or str(e)

        # Find the object that is erroring, and replace it with the marker.
        for entry in list(e.path)[:-1]:
            json_object = json_object[entry]

        orig, json_object[e.path[-1]] = json_object[e.path[-1]], cls.marker

        # Pretty print the object and search for the marker.
        json_error = cls.dumper(json_object)
        err_line = None

        for lineno, text in enumerate(io.StringIO(json_error)):
            if cls.marker in text:
                err_line = lineno
                break

        if not err_line:
            return e.message or str(e)

        report = []
        json_object[e.path[-1]] = orig
        json_error = cls.dumper(json_object)

        for lineno, text in enumerate(io.StringIO(json_error)):
            line_text = "{:4}: {}".format(lineno + 1, '>' * 3 if lineno == err_line else ' ' * 3)
            report.append(line_text + text.rstrip("\n"))

        report = report[max(0, err_line - lines_before):err_line + 1 + lines_after]
        return cls.message_format.format(
            line=err_line + 1,
            report="\n".join(report),
            message=e.message or str(e)
        )
