# -*- coding: utf-8 -*-
"""Dependency-free JSON-Schema validation for the A3 ticket (E0803).

The Odoo runtime image has no ``jsonschema`` package, so this implements the
subset of JSON Schema the official NACEF "Ticket" schema (v1.1.4, extracted
from ``nacef-smdf-api-1.2.0``) actually uses: type, required, properties,
items, enum, const, pattern, minLength/maxLength, minimum/maximum, minItems,
additionalProperties, allOf and if/then/else. ``format`` is treated as an
annotation (not enforced), matching most validators' default.
"""
import json
import os
import re

_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "ticket_schema_1_1_4.json")
_SCHEMA = None


class SchemaError(Exception):
    """Raised when an instance does not conform. ``errors`` is the full list."""

    def __init__(self, errors):
        self.errors = errors
        shown = "; ".join(errors[:8]) + (" …" if len(errors) > 8 else "")
        super().__init__(shown or "schema validation failed")


def ticket_schema():
    global _SCHEMA
    if _SCHEMA is None:
        with open(_SCHEMA_PATH, encoding="utf-8") as fh:
            _SCHEMA = json.load(fh)
    return _SCHEMA


def _is_type(value, t):
    if t == "integer":
        return isinstance(value, int) and not isinstance(value, bool)
    if t == "number":
        return isinstance(value, (int, float)) and not isinstance(value, bool)
    if t == "boolean":
        return isinstance(value, bool)
    return isinstance(value, {
        "object": dict, "array": list, "string": str,
    }.get(t, object))


def _collect(instance, schema, path, errors):
    t = schema.get("type")
    if t and not _is_type(instance, t):
        errors.append("%s: expected type %s" % (path or "(root)", t))
        return errors
    if "const" in schema and instance != schema["const"]:
        errors.append("%s: must equal %r" % (path or "(root)", schema["const"]))
    if "enum" in schema and instance not in schema["enum"]:
        errors.append("%s: %r not in %r" % (path, instance, schema["enum"]))
    if isinstance(instance, str):
        if "minLength" in schema and len(instance) < schema["minLength"]:
            errors.append("%s: shorter than %d" % (path, schema["minLength"]))
        if "maxLength" in schema and len(instance) > schema["maxLength"]:
            errors.append("%s: longer than %d" % (path, schema["maxLength"]))
        if "pattern" in schema and not re.search(schema["pattern"], instance):
            errors.append("%s: %r violates %s" % (path, instance, schema["pattern"]))
    if isinstance(instance, (int, float)) and not isinstance(instance, bool):
        if "minimum" in schema and instance < schema["minimum"]:
            errors.append("%s: %s < minimum %s" % (path, instance, schema["minimum"]))
        if "maximum" in schema and instance > schema["maximum"]:
            errors.append("%s: %s > maximum %s" % (path, instance, schema["maximum"]))
    if isinstance(instance, list):
        if "minItems" in schema and len(instance) < schema["minItems"]:
            errors.append("%s: fewer than %d items" % (path, schema["minItems"]))
        items = schema.get("items")
        if items:
            for i, el in enumerate(instance):
                _collect(el, items, "%s[%d]" % (path, i), errors)
    if isinstance(instance, dict):
        props = schema.get("properties", {})
        for req in schema.get("required", []):
            if req not in instance:
                errors.append("%s: missing required '%s'" % (path or "(root)", req))
        if schema.get("additionalProperties") is False:
            extra = sorted(set(instance) - set(props))
            if extra:
                errors.append("%s: unexpected keys %s" % (path or "(root)", extra))
        for key, val in instance.items():
            if key in props:
                _collect(val, props[key], "%s/%s" % (path, key), errors)
    for sub in schema.get("allOf", []):
        _collect(instance, sub, path, errors)
    if "if" in schema:
        if not _collect(instance, schema["if"], path, []):
            if "then" in schema:
                _collect(instance, schema["then"], path, errors)
        elif "else" in schema:
            _collect(instance, schema["else"], path, errors)
    return errors


def validate(instance, schema=None):
    """Return the list of violations (empty == valid)."""
    return _collect(instance, schema or ticket_schema(), "", [])


def validate_ticket(instance):
    """Validate an A3 ticket dict; raise :class:`SchemaError` if invalid."""
    errors = validate(instance)
    if errors:
        raise SchemaError(errors)
    return True
