"""Template-declared content requirements and recursive asset references.

Shared by DSL preparation and saved RenderPlan validation. Requirements use a
small JSON Schema subset; values never appear in diagnostic messages.
"""


def payload_values(props: dict) -> dict:
    """Flatten templateData wrappers, with non-null outer props taking precedence."""
    layers, seen = [], set()
    while isinstance(props, dict) and id(props) not in seen and len(layers) < 10:
        layers.append(props)
        seen.add(id(props))
        props = props.get("templateData")
    result = {}
    for layer in reversed(layers):
        result.update({k: v for k, v in layer.items() if k != "templateData" and v is not None})
    return result


def requirement_errors(value, schema: dict, path: str = "customPayload") -> list[str]:
    """Enforce type/const/enum/required/properties/items/minItems/anyOf."""
    errors = []
    kind = schema.get("type")
    matches = {"object": isinstance(value, dict), "array": isinstance(value, list),
               "string": isinstance(value, str)}
    if kind in matches and not matches[kind]:
        return [f"{path} (expected {kind})"]
    if "const" in schema and value != schema["const"]:
        return [f"{path} (unexpected value)"]
    if "enum" in schema and value not in schema["enum"]:
        return [f"{path} (unexpected value)"]
    if isinstance(value, dict):
        for key in schema.get("required", []):
            if value.get(key) is None or value.get(key) == "":
                errors.append(f"{path}.{key} (required)")
        for key, child in schema.get("properties", {}).items():
            if key in value:
                errors += requirement_errors(value[key], child, f"{path}.{key}")
    if isinstance(value, list):
        if len(value) < schema.get("minItems", 0):
            errors.append(f"{path} (too few items)")
        if "items" in schema:
            for index, item in enumerate(value):
                errors += requirement_errors(item, schema["items"], f"{path}[{index}]")
    if "anyOf" in schema:
        branches = [requirement_errors(value, branch, path) for branch in schema["anyOf"]]
        if not branches or all(branches):
            alternatives = " OR ".join("; ".join(branch) for branch in branches)
            errors.append(f"{path} must satisfy one content alternative: {alternatives}")
    return errors


def required_payload_errors(template: dict, props: dict) -> list[str]:
    caps = template.get("capabilities")
    schema = caps.get("payloadRequirements") if isinstance(caps, dict) else None
    return requirement_errors(payload_values(props), schema) if isinstance(schema, dict) else []


def asset_references(value, path: str = "", *, bound_props: bool = False):
    """Walk nested assetRef fields; bound props also use *AssetId names."""
    if isinstance(value, list):
        for index, item in enumerate(value):
            yield from asset_references(item, f"{path}[{index}]", bound_props=bound_props)
    elif isinstance(value, dict):
        for key, child in value.items():
            child_path = f"{path}.{key}" if path else key
            if key == "assetRef" or (bound_props and key.endswith("AssetId")):
                if isinstance(child, str) and child.strip():
                    yield child_path, child
            else:
                yield from asset_references(child, child_path, bound_props=bound_props)


def plan_asset_errors(plan: dict) -> list[str]:
    declared = {asset.get("assetId") for asset in plan.get("resolvedAssets", []) if isinstance(asset, dict)}
    errors = []
    for index, row in enumerate(plan.get("timeline", [])):
        for path, ref in asset_references(row.get("props", {}), f"timeline[{index}].props", bound_props=True):
            if ref not in declared:
                errors.append(f"{path} references an asset missing from resolvedAssets[]")
    return errors
