"""
xmldev.schema
~~~~~~~~~~~~~
JSON ground-truth schema loader and AST dataclasses.
"""
from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import jsonschema

# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------

class SchemaLoadError(ValueError):
    """Raised when the ground-truth schema JSON is invalid (XM01)."""
    code = "XM01"


# ---------------------------------------------------------------------------
# Meta-schema: validates the ground-truth JSON files users supply
# ---------------------------------------------------------------------------

_META_SCHEMA: dict[str, Any] = {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "xmldev Ground Truth Schema",
    "type": "object",
    "required": ["root"],
    "additionalProperties": False,
    "properties": {
        "root": {"$ref": "#/definitions/ElementSpec"},
        "global": {
            "type": "object",
            "additionalProperties": False,
            "properties": {
                "allow_unknown": {
                    "type": "string",
                    "enum": ["keep", "drop", "move_to_extension"],
                    "default": "move_to_extension",
                },
                "extension_element_name": {"type": "string", "default": "extensions"},
                "fuzzy": {
                    "type": "object",
                    "additionalProperties": False,
                    "properties": {
                        "name_threshold": {"type": "number", "minimum": 0, "maximum": 1},
                        "permissive_threshold": {"type": "number", "minimum": 0, "maximum": 1},
                        "max_renames_per_doc": {"type": "integer", "minimum": 1},
                    },
                },
            },
        },
    },
    "definitions": {
        "ElementSpec": {
            "type": "object",
            "required": ["name"],
            "additionalProperties": False,
            "properties": {
                "name": {"type": "string"},
                "namespace": {"type": "string"},
                "text_type": {
                    "type": "string",
                    "enum": ["string", "int", "float", "date", "bool", "enum", "regex"],
                },
                "enum_values": {"type": "array", "items": {"type": "string"}},
                "pattern": {"type": "string"},
                "attrs": {
                    "type": "object",
                    "additionalProperties": {"$ref": "#/definitions/AttrSpec"},
                },
                "children": {
                    "type": "array",
                    "items": {"$ref": "#/definitions/ChildSpec"},
                },
                "min_occurs": {"type": "integer", "minimum": 0},
                "max_occurs": {
                    "oneOf": [
                        {"type": "integer", "minimum": 1},
                        {"type": "string", "enum": ["unbounded"]},
                    ]
                },
                "order_enforced": {"type": "boolean"},
                "aliases": {"type": "array", "items": {"type": "string"}},
                "default": {},
                "description": {"type": "string"},
                "conditional_rules": {
                    "type": "array",
                    "items": {"$ref": "#/definitions/ConditionalRule"},
                },
                "custom_validator": {"type": "string"},
            },
        },
        "AttrSpec": {
            "type": "object",
            "required": ["name"],
            "additionalProperties": False,
            "properties": {
                "name": {"type": "string"},
                "type": {
                    "type": "string",
                    "enum": ["string", "int", "float", "enum", "bool", "regex"],
                },
                "required": {"type": "boolean"},
                "default": {},
                "enum_values": {"type": "array", "items": {"type": "string"}},
                "pattern": {"type": "string"},
                "aliases": {"type": "array", "items": {"type": "string"}},
            },
        },
        "ChildSpec": {
            "type": "object",
            "required": ["spec"],
            "additionalProperties": False,
            "properties": {
                "spec": {"$ref": "#/definitions/ElementSpec"},
                "min_occurs": {"type": "integer", "minimum": 0},
                "max_occurs": {
                    "oneOf": [
                        {"type": "integer", "minimum": 1},
                        {"type": "string", "enum": ["unbounded"]},
                    ]
                },
                "order_index": {"type": "integer"},
                "required_if": {"type": "object"},
            },
        },
        "ConditionalRule": {
            "type": "object",
            "additionalProperties": False,
            "properties": {
                "when": {"type": "string"},
                "then": {"type": "string"},
            },
        },
    },
}


# ---------------------------------------------------------------------------
# AST dataclasses
# ---------------------------------------------------------------------------

@dataclass
class AttrSpec:
    name: str
    type: str = "string"
    required: bool = False
    default: Any = None
    enum_values: list[str] = field(default_factory=list)
    pattern: str | None = None
    aliases: list[str] = field(default_factory=list)


@dataclass
class ConditionalRule:
    when: str = ""
    then: str = ""


@dataclass
class ElementSpec:
    name: str
    namespace: str | None = None
    text_type: str = "string"
    enum_values: list[str] = field(default_factory=list)
    pattern: str | None = None
    attrs: dict[str, AttrSpec] = field(default_factory=dict)
    children: list["ChildSpec"] = field(default_factory=list)
    min_occurs: int = 0
    max_occurs: int | str = 1
    order_enforced: bool = False
    aliases: list[str] = field(default_factory=list)
    default: Any = None
    description: str = ""
    conditional_rules: list[ConditionalRule] = field(default_factory=list)
    custom_validator: str | None = None


@dataclass
class ChildSpec:
    spec: ElementSpec
    min_occurs: int = 0
    max_occurs: int | str = 1
    order_index: int | None = None
    required_if: dict[str, Any] = field(default_factory=dict)


@dataclass
class GlobalConfig:
    allow_unknown: str = "move_to_extension"
    extension_element_name: str = "extensions"
    name_threshold: float = 0.85
    permissive_threshold: float = 0.70
    max_renames_per_doc: int = 10


@dataclass
class Schema:
    root: ElementSpec
    global_config: GlobalConfig = field(default_factory=GlobalConfig)


# ---------------------------------------------------------------------------
# Loader
# ---------------------------------------------------------------------------

class SchemaLoader:
    """Load and validate a ground-truth JSON schema file."""

    @classmethod
    def load(cls, source: str | bytes | Path) -> Schema:
        """
        Parse and validate a ground-truth JSON schema.

        Parameters
        ----------
        source:
            A JSON string/bytes, or a filesystem path to a .json file.

        Returns
        -------
        Schema
            Validated schema AST.

        Raises
        ------
        SchemaLoadError (XM01)
            If the JSON is malformed or does not conform to the meta-schema.
        """
        raw = cls._read_source(source)
        data = cls._parse_json(raw)
        cls._validate_against_meta(data)
        return cls._build_ast(data)

    # ------------------------------------------------------------------
    # private helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _read_source(source: str | bytes | Path) -> str:
        if isinstance(source, (str, bytes)):
            # Could be JSON content or a file path string
            s = source.decode() if isinstance(source, bytes) else source
            s_stripped = s.strip()
            # If it starts with { or [ it's almost certainly JSON (not a path)
            if s_stripped.startswith(("{", "[")):
                try:
                    json.loads(s_stripped)
                    return s_stripped
                except json.JSONDecodeError as exc:
                    raise SchemaLoadError(f"[XM01] Invalid JSON in schema: {exc}") from exc
            # Otherwise treat as a file path
            p = Path(s_stripped)
            if not p.exists():
                raise SchemaLoadError(
                    f"[XM01] Schema source is not valid JSON and not a readable file: {s_stripped!r}"
                )
            return p.read_text(encoding="utf-8")
        elif isinstance(source, Path):
            return source.read_text(encoding="utf-8")
        raise SchemaLoadError(f"Unsupported source type: {type(source)}")

    @staticmethod
    def _parse_json(raw: str) -> dict:
        try:
            return json.loads(raw)
        except json.JSONDecodeError as exc:
            raise SchemaLoadError(f"[XM01] Invalid JSON in schema: {exc}") from exc

    @staticmethod
    def _validate_against_meta(data: dict) -> None:
        try:
            jsonschema.validate(instance=data, schema=_META_SCHEMA)
        except jsonschema.ValidationError as exc:
            raise SchemaLoadError(f"[XM01] Schema does not conform to xmldev spec: {exc.message}") from exc

    @classmethod
    def _build_ast(cls, data: dict) -> Schema:
        global_cfg = cls._build_global(data.get("global", {}))
        root_spec = cls._build_element(data["root"])
        return Schema(root=root_spec, global_config=global_cfg)

    @staticmethod
    def _build_global(g: dict) -> GlobalConfig:
        fuzzy = g.get("fuzzy", {})
        return GlobalConfig(
            allow_unknown=g.get("allow_unknown", "move_to_extension"),
            extension_element_name=g.get("extension_element_name", "extensions"),
            name_threshold=fuzzy.get("name_threshold", 0.85),
            permissive_threshold=fuzzy.get("permissive_threshold", 0.70),
            max_renames_per_doc=fuzzy.get("max_renames_per_doc", 10),
        )

    @classmethod
    def _build_element(cls, d: dict) -> ElementSpec:
        attrs = {
            k: cls._build_attr(v)
            for k, v in d.get("attrs", {}).items()
        }
        children = [cls._build_child(c) for c in d.get("children", [])]
        cond_rules = [
            ConditionalRule(when=r.get("when", ""), then=r.get("then", ""))
            for r in d.get("conditional_rules", [])
        ]
        return ElementSpec(
            name=d["name"],
            namespace=d.get("namespace"),
            text_type=d.get("text_type", "string"),
            enum_values=d.get("enum_values", []),
            pattern=d.get("pattern"),
            attrs=attrs,
            children=children,
            min_occurs=d.get("min_occurs", 0),
            max_occurs=d.get("max_occurs", 1),
            order_enforced=d.get("order_enforced", False),
            aliases=d.get("aliases", []),
            default=d.get("default"),
            description=d.get("description", ""),
            conditional_rules=cond_rules,
            custom_validator=d.get("custom_validator"),
        )

    @classmethod
    def _build_child(cls, d: dict) -> ChildSpec:
        return ChildSpec(
            spec=cls._build_element(d["spec"]),
            min_occurs=d.get("min_occurs", 0),
            max_occurs=d.get("max_occurs", 1),
            order_index=d.get("order_index"),
            required_if=d.get("required_if", {}),
        )

    @staticmethod
    def _build_attr(d: dict) -> AttrSpec:
        return AttrSpec(
            name=d["name"],
            type=d.get("type", "string"),
            required=d.get("required", False),
            default=d.get("default"),
            enum_values=d.get("enum_values", []),
            pattern=d.get("pattern"),
            aliases=d.get("aliases", []),
        )
