"""Manifest-dialect helpers, shared with the execution backends.

A v2 manifest replaces the flat ``params`` list with an embedded JSON Schema
(``inputSchema``) plus a ``uiSchema``. These helpers let consumers support both
dialects side by side. ``acellera-playmolecule`` is expected to import these
(rather than keep a private copy) so the contract can't drift.
"""

from __future__ import annotations

SPECIAL_DIR_ARGS = ("outdir", "scratchdir", "execdir")


def detect_dialect(obj: dict) -> int:
    """Return the manifest dialect (1 = legacy ``params``, 2 = JSON Schema).

    Accepts a full manifest envelope or a single function dict (some consumers
    only ever see the function entry). An explicit marker wins; otherwise the
    dialect is autodiscovered from which keys are present.
    """
    if not isinstance(obj, dict):
        return 1
    if "manifestVersion" in obj:
        return int(obj["manifestVersion"])
    if "schemaVersion" in obj:  # per-function marker
        return int(obj["schemaVersion"])
    if "inputSchema" in obj:
        return 2
    if "params" in obj:
        return 1
    return 1


def resolve_ref(node, defs):
    """Resolve ``{"$ref": "#/$defs/Name"}`` against ``defs``; return the node
    unchanged if not a ref, or ``None`` for a non-dict node."""
    if not isinstance(node, dict):
        return None
    if "$ref" in node:
        return defs.get(node["$ref"].split("/")[-1])
    return node


def iter_path_values(schema, value, defs=None, _path=()):
    """Yield ``(path_tuple, value)`` for every leaf in ``value`` whose schema
    node declares ``format == "path"``.

    Resolves ``$ref`` and descends ``object`` (``properties`` +
    ``additionalProperties``), ``array`` (``items``) and ``anyOf`` / ``oneOf``.
    ``path_tuple`` (dict keys / list indices) is suitable for :func:`set_in`.
    """
    if defs is None:
        defs = (schema or {}).get("$defs", {})
    schema = resolve_ref(schema, defs)
    if schema is None or value is None:
        return

    # Yield the leaf for any non-container value — not just ``str``. Callers in
    # the SDK pass path leaves as framework file objects (e.g. ``_File`` /
    # ``_HTTPFile``) or ``pathlib.Path``, which they stage themselves; a strict
    # ``isinstance(value, str)`` would silently skip those (leaving e.g. a _File
    # in the inputs, which then fails to JSON-serialize). ``value is None`` is
    # already handled above.
    if schema.get("format") == "path" and not isinstance(value, (dict, list)):
        yield _path, value
        return

    for branch_key in ("anyOf", "oneOf"):
        if branch_key in schema:
            for sub in schema[branch_key]:
                yield from iter_path_values(sub, value, defs, _path)
            return

    stype = schema.get("type")
    if stype == "object" and isinstance(value, dict):
        props = schema.get("properties", {})
        addl = schema.get("additionalProperties")
        for key, val in value.items():
            if key in props:
                yield from iter_path_values(props[key], val, defs, _path + (key,))
            elif isinstance(addl, dict):
                yield from iter_path_values(addl, val, defs, _path + (key,))
    elif stype == "array" and isinstance(value, list):
        items = schema.get("items", {})
        for i, val in enumerate(value):
            yield from iter_path_values(items, val, defs, _path + (i,))


def set_in(obj, path, value):
    """Set ``value`` at ``path`` (dict keys / list indices) in a nested ``obj``."""
    cur = obj
    for p in path[:-1]:
        cur = cur[p]
    cur[path[-1]] = value
