#!/usr/bin/env python
"""Build the property schema for a given configuration.

The structural base — every property's type and cardinality (``array``/``scalar``)
— is a configuration invariant, generated offline from the corpus and frozen in
:data:`guessit.schema_generated.GUESSIT_SCHEMA`. Only the closed-vocabulary ``enum`` lists
depend on the effective configuration (a custom ``advanced_config`` can add e.g.
a ``streaming_service`` or an ``edition``), so a configuration-aware schema is the
frozen base with its enums overlaid from ``properties(options)``.

:func:`build_json_schema` converts such a mapping into a draft-07 JSON Schema and
is the single source used both here and by ``scripts/gen_schema.py``.
"""

from __future__ import annotations

from copy import deepcopy
from typing import Any


def _enum_values(values: Any) -> set[Any]:
    """Flatten a ``properties()`` value list into scalar enum members.

    Compound declared values (e.g. edition's ``['Ultimate', 'Collector']``) are
    flattened; ``None`` (free/computed value marker) is dropped.
    """
    flattened: set[Any] = set()
    for value in values or []:
        if value is None:
            continue
        if isinstance(value, list):
            flattened.update(item for item in value if item is not None)
        else:
            flattened.add(value)
    return flattened


def overlay_config_enums(base: dict[str, Any], properties: dict[str, Any]) -> dict[str, Any]:
    """Overlay the enums from ``properties()`` onto the frozen structural base.

    ``base`` is :data:`GUESSIT_SCHEMA` (type + cardinality per property, plus the
    default enums). ``properties`` is the output of ``api.properties(options)`` for
    the effective configuration. The result keeps the base structure and, for every
    closed-vocabulary property, unions its enum with the configured values — so a
    custom vocabulary shows up without ever dropping a known default value.

    A property present in ``properties`` but absent from the base (introduced by a
    custom configuration or ``rules_builder``) is added with an inferred type; its
    cardinality is unknown, so it defaults to a scalar.
    """
    schema = deepcopy(base)

    for name, spec in schema.items():
        if "enum" not in spec:
            continue
        merged = set(spec["enum"]) | _enum_values(properties.get(name))
        spec["enum"] = sorted(merged, key=str)

    for name, values in properties.items():
        if name in schema:
            continue
        members = _enum_values(values)
        numeric = bool(members) and all(isinstance(item, int) and not isinstance(item, bool) for item in members)
        entry: dict[str, Any] = {
            "type": ["number"] if numeric else ["string"],
            "array": False,
            "scalar": True,
        }
        if members:
            entry["enum"] = sorted(members, key=str)
        schema[name] = entry

    return dict(sorted(schema.items(), key=lambda item: str(item[0])))


def build_json_schema(schema: dict[str, dict[str, Any]]) -> dict[str, Any]:
    """Build the draft-07 JSON Schema from a ``GUESSIT_SCHEMA``-shaped mapping."""
    language_def = {
        "type": ["object", "string"],
        "description": "A language or country: a string code, or an object.",
        "properties": {
            "name": {"type": "string"},
            "alpha3": {"type": "string"},
            "alpha2": {"type": "string"},
            "country": {"type": ["string", "null"]},
            "script": {"type": ["string", "null"]},
        },
    }
    json_base = {"string": "string", "number": "number", "boolean": "boolean", "object": "object"}

    def base_fragment(spec: dict[str, Any]) -> dict[str, Any]:
        if "Language" in spec["type"]:
            return {"$ref": "#/definitions/Language"}
        frag: dict[str, Any] = {"type": [json_base[t] for t in spec["type"]]}
        if len(frag["type"]) == 1:
            frag["type"] = frag["type"][0]
        if spec.get("enum"):
            frag["enum"] = list(spec["enum"])
        return frag

    properties: dict[str, Any] = {}
    for name, spec in schema.items():
        base = base_fragment(spec)
        if spec["array"] and spec["scalar"]:
            properties[name] = {"oneOf": [base, {"type": "array", "items": base}]}
        elif spec["array"]:
            properties[name] = {"type": "array", "items": base}
        else:
            properties[name] = base

    return {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "$id": "https://github.com/guessit-io/guessit/output-schema.json",
        "title": "guessit output",
        "description": "Metadata extracted from a release name. Every property is optional; "
        "presence depends on the input. Generated by scripts/gen_schema.py.",
        "type": "object",
        "definitions": {"Language": language_def},
        "properties": properties,
        "additionalProperties": True,
    }
