"""CLI helpers for ontology inspection commands exposed via cf-cli."""

from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple

from rdflib import BNode, Graph, Literal, URIRef
from rdflib.collection import Collection
from rdflib.namespace import OWL, RDF, RDFS

from .ontology_manager import OntologyManager

try:
    from cf_cli.registry import register_command as _register_command
except ImportError:  # pragma: no cover - optional dependency
    _register_command = None

REGISTER_COMMAND = _register_command


CF_NAMESPACE = "https://cogniflow.org/ns#"
CF_NAMESPACE_ALT = "http://cogniflow.org/ns#"


def _apply_semantics_env(
    semantics_dir: Optional[Path],
    enable_steps: bool,
) -> None:
    if semantics_dir:
        os.environ["CF_SEMANTICS_DIR"] = str(semantics_dir)
    if enable_steps:
        os.environ["CF_ENABLE_STEP_PACKAGES"] = "1"


def _parse_semantics_dir(value: Optional[str]) -> Optional[Path]:
    if not value:
        return None
    return Path(value).expanduser().resolve()


def _add_semantics_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--semantics-dir", default=None, help="Target semantics directory.")
    parser.add_argument(
        "--enable-step-packages",
        action="store_true",
        help="Enable step package discovery (CF_ENABLE_STEP_PACKAGES=1).",
    )


def _shrink_uri(graph: Graph, uri: URIRef) -> str:
    try:
        return graph.namespace_manager.qname(uri)
    except (ValueError, KeyError, AttributeError):
        return str(uri)


def _shrink_uri_from_context(uri: URIRef, context: dict[str, Any]) -> str:
    text = str(uri)
    best_prefix = None
    best_ns = None
    for prefix, ns in context.items():
        if not isinstance(prefix, str) or not isinstance(ns, str):
            continue
        if text.startswith(ns):
            if best_ns is None or len(ns) > len(best_ns):
                best_ns = ns
                best_prefix = prefix
    if best_prefix and best_ns is not None:
        local = text[len(best_ns) :]
        if local:
            return f"{best_prefix}:{local}"
    return text


def _local_name(uri: URIRef) -> str:
    text = str(uri)
    if "#" in text:
        return text.split("#", 1)[1]
    if "/" in text:
        return text.rsplit("/", 1)[-1]
    return text


def _format_value(graph: Graph, value: Any) -> str:
    if isinstance(value, URIRef):
        return _shrink_uri(graph, value)
    if isinstance(value, Literal):
        native = value.toPython()
        return str(native)
    if isinstance(value, BNode):
        return f"_:{value}"
    return str(value)


def _format_values(graph: Graph, values: Sequence[Any]) -> str:
    return ", ".join(_format_value(graph, v) for v in values)


def _resolve_curie(graph: Graph, value: str) -> Optional[URIRef]:
    if ":" not in value:
        return None
    prefix, local = value.split(":", 1)
    if prefix == "cf":
        return URIRef(f"{CF_NAMESPACE}{local}")
    for ns_prefix, ns_uri in graph.namespace_manager.namespaces():
        if ns_prefix == prefix:
            return URIRef(f"{ns_uri}{local}")
    return None


def _resolve_term(
    graph: Graph,
    value: str,
    candidates: Optional[Iterable[URIRef]] = None,
) -> Optional[URIRef]:
    text = value.strip()
    if not text:
        return None
    if text.startswith("http://") or text.startswith("https://"):
        return URIRef(text)

    curie = _resolve_curie(graph, text)
    if curie is not None:
        return curie

    # Default to CF namespace for bare local names.
    if ":" not in text:
        return URIRef(f"{CF_NAMESPACE}{text}")

    # Fallback: try to match label/local name against candidates.
    if candidates:
        needle = text.lower()
        for candidate in candidates:
            if _local_name(candidate).lower() == needle:
                return candidate
    for subj, label in graph.subject_objects(RDFS.label):
        if not isinstance(subj, URIRef):
            continue
        if str(label).strip().lower() == text.lower():
            return subj
    return None


def _collect_classes(graph: Graph, *, include_external: bool = False) -> List[URIRef]:
    class_nodes = set(graph.subjects(RDF.type, OWL.Class)) | set(
        graph.subjects(RDF.type, RDFS.Class)
    )
    classes: List[URIRef] = []
    for node in class_nodes:
        if not isinstance(node, URIRef):
            continue
        if not include_external:
            uri = str(node)
            if not (uri.startswith(CF_NAMESPACE) or uri.startswith(CF_NAMESPACE_ALT)):
                continue
        classes.append(node)
    return sorted(classes, key=lambda n: _shrink_uri(graph, n))


def _collect_superclasses(graph: Graph, class_uri: URIRef) -> List[URIRef]:
    seen: set[URIRef] = set()
    stack = [class_uri]
    while stack:
        current = stack.pop()
        for parent in graph.objects(current, RDFS.subClassOf):
            if not isinstance(parent, URIRef):
                continue
            if parent in seen:
                continue
            seen.add(parent)
            stack.append(parent)
    return sorted(seen, key=lambda n: _shrink_uri(graph, n))


def _collect_subclasses(graph: Graph, class_uri: URIRef) -> List[URIRef]:
    subclasses: List[URIRef] = []
    for subj in graph.subjects(RDFS.subClassOf, class_uri):
        if isinstance(subj, URIRef):
            subclasses.append(subj)
    return sorted(subclasses, key=lambda n: _shrink_uri(graph, n))


def _collect_union_members(graph: Graph, class_uri: URIRef) -> List[URIRef]:
    members: List[URIRef] = []
    for union_node in graph.objects(class_uri, OWL.unionOf):
        if not isinstance(union_node, BNode):
            continue
        try:
            for item in Collection(graph, union_node):
                if isinstance(item, URIRef):
                    members.append(item)
        except (TypeError, ValueError):
            continue
    return sorted(members, key=lambda n: _shrink_uri(graph, n))


def _collect_properties_for_domain(graph: Graph, domain: URIRef) -> List[URIRef]:
    props: List[URIRef] = []
    for prop in graph.subjects(RDFS.domain, domain):
        if isinstance(prop, URIRef):
            props.append(prop)
    return props


def _property_type(graph: Graph, prop: URIRef) -> str:
    if (prop, RDF.type, OWL.ObjectProperty) in graph:
        return "ObjectProperty"
    if (prop, RDF.type, OWL.DatatypeProperty) in graph:
        return "DatatypeProperty"
    return "Property"


def _describe_class(
    graph: Graph,
    class_uri: URIRef,
    *,
    include_inherited: bool,
) -> Dict[str, Any]:
    cf_desc = URIRef(f"{CF_NAMESPACE}description")
    label = graph.value(class_uri, RDFS.label)
    description = graph.value(class_uri, cf_desc) or graph.value(class_uri, RDFS.comment)
    superclasses = _collect_superclasses(graph, class_uri)
    subclasses = _collect_subclasses(graph, class_uri)
    union_members = _collect_union_members(graph, class_uri)

    direct_props = set(_collect_properties_for_domain(graph, class_uri))
    inherited_props: set[URIRef] = set()
    if include_inherited:
        for parent in superclasses:
            inherited_props.update(_collect_properties_for_domain(graph, parent))
        inherited_props.difference_update(direct_props)

    def _format_props(props: Iterable[URIRef]) -> List[Dict[str, Any]]:
        entries: List[Dict[str, Any]] = []
        for prop in sorted(props, key=lambda n: _shrink_uri(graph, n)):
            ranges = [obj for obj in graph.objects(prop, RDFS.range) if isinstance(obj, URIRef)]
            entries.append(
                {
                    "id": prop,
                    "label": graph.value(prop, RDFS.label),
                    "description": graph.value(prop, cf_desc),
                    "type": _property_type(graph, prop),
                    "ranges": ranges,
                }
            )
        return entries

    return {
        "id": class_uri,
        "label": label,
        "description": description,
        "superclasses": superclasses,
        "subclasses": subclasses,
        "union_members": union_members,
        "properties": _format_props(direct_props),
        "inherited_properties": _format_props(inherited_props),
    }


def _collect_instances(
    graph: Graph,
    class_uri: URIRef,
    *,
    include_subclasses: bool,
) -> List[URIRef]:
    types = {class_uri}
    if include_subclasses:
        types.update(_collect_subclasses(graph, class_uri))
    instances: set[URIRef] = set()
    for t in types:
        for subj in graph.subjects(RDF.type, t):
            if isinstance(subj, URIRef):
                instances.add(subj)
    return sorted(instances, key=lambda n: _shrink_uri(graph, n))


def _collect_instance_properties(graph: Graph, instance: URIRef) -> List[Tuple[URIRef, List[Any]]]:
    props: Dict[URIRef, List[Any]] = {}
    for pred, obj in graph.predicate_objects(instance):
        if pred == RDF.type:
            continue
        if not isinstance(pred, URIRef):
            continue
        props.setdefault(pred, []).append(obj)
    return sorted(props.items(), key=lambda item: _shrink_uri(graph, item[0]))


def _describe_step(graph: Graph, step_uri: URIRef) -> Dict[str, Any]:
    cf_ns = CF_NAMESPACE
    cf_desc = URIRef(f"{cf_ns}description")
    label = graph.value(step_uri, RDFS.label)
    description = graph.value(step_uri, cf_desc)
    category = graph.value(step_uri, URIRef(f"{cf_ns}stepCategory"))
    package = graph.value(step_uri, URIRef(f"{cf_ns}belongsToStepPackage"))

    impls = [
        obj
        for obj in graph.objects(step_uri, URIRef(f"{cf_ns}hasImplementation"))
        if isinstance(obj, URIRef)
    ]

    def _literal_bool(value: Optional[Literal]) -> Optional[bool]:
        if value is None:
            return None
        raw = str(value).strip().lower()
        if raw in {"true", "1", "yes", "y"}:
            return True
        if raw in {"false", "0", "no", "n"}:
            return False
        return None

    def _parse_ports(predicate: URIRef) -> List[Dict[str, Any]]:
        ports: List[Dict[str, Any]] = []
        for port in graph.objects(step_uri, predicate):
            if not isinstance(port, (URIRef, BNode)):
                continue
            port_name = graph.value(port, URIRef(f"{cf_ns}portName"))
            port_type = graph.value(port, URIRef(f"{cf_ns}portType"))
            port_desc = graph.value(port, cf_desc)
            port_key = graph.value(port, URIRef(f"{cf_ns}portKey"))
            optional_literal = graph.value(port, URIRef(f"{cf_ns}isOptional"))
            default_literal = graph.value(port, URIRef(f"{cf_ns}defaultValue"))
            min_value = graph.value(port, URIRef(f"{cf_ns}minValue"))
            max_value = graph.value(port, URIRef(f"{cf_ns}maxValue"))
            allowed_values = list(graph.objects(port, URIRef(f"{cf_ns}allowedValue")))
            ports.append(
                {
                    "id": port,
                    "name": port_name,
                    "type": port_type,
                    "description": port_desc,
                    "port_key": port_key,
                    "optional": _literal_bool(optional_literal),
                    "default": default_literal,
                    "min": min_value,
                    "max": max_value,
                    "allowed": allowed_values,
                }
            )
        return sorted(ports, key=lambda p: str(p.get("name") or ""))

    def _parse_parameters() -> List[Dict[str, Any]]:
        params: List[Dict[str, Any]] = []
        required_ids = {
            str(obj)
            for obj in graph.objects(step_uri, URIRef(f"{cf_ns}requiresParameter"))
            if isinstance(obj, URIRef)
        }
        for param in graph.objects(step_uri, URIRef(f"{cf_ns}hasParameter")):
            if not isinstance(param, (URIRef, BNode)):
                continue
            param_name = graph.value(param, URIRef(f"{cf_ns}parameterName"))
            if not param_name:
                continue
            ptype = graph.value(param, URIRef(f"{cf_ns}parameterType"))
            pdesc = graph.value(param, cf_desc)
            default_value = graph.value(param, URIRef(f"{cf_ns}defaultValue"))
            min_value = graph.value(param, URIRef(f"{cf_ns}minValue"))
            max_value = graph.value(param, URIRef(f"{cf_ns}maxValue"))
            allowed_values = list(graph.objects(param, URIRef(f"{cf_ns}allowedValue")))
            required_literal = graph.value(param, URIRef(f"{cf_ns}isRequired"))
            required = _literal_bool(required_literal)
            if required is None:
                required = str(param) in required_ids
            params.append(
                {
                    "id": param,
                    "name": param_name,
                    "type": ptype,
                    "description": pdesc,
                    "default": default_value,
                    "min": min_value,
                    "max": max_value,
                    "allowed": allowed_values,
                    "required": bool(required),
                }
            )
        return sorted(params, key=lambda p: str(p.get("name") or ""))

    return {
        "id": step_uri,
        "label": label,
        "description": description,
        "category": category,
        "package": package,
        "implementations": impls,
        "inputs": _parse_ports(URIRef(f"{cf_ns}hasInput")),
        "outputs": _parse_ports(URIRef(f"{cf_ns}hasOutput")),
        "parameters": _parse_parameters(),
    }


def _print_class_info(graph: Graph, info: Dict[str, Any], *, show_inherited: bool) -> None:
    class_uri = info["id"]
    print(f"Class: {_shrink_uri(graph, class_uri)}")
    print(f"IRI: {class_uri}")
    label = info.get("label")
    if label:
        print(f"Label: {label}")
    description = info.get("description")
    if description:
        print(f"Description: {description}")

    if info.get("union_members"):
        print("Union Of:")
        for member in info["union_members"]:
            print(f"- {_shrink_uri(graph, member)}")

    print("Superclasses:")
    if info["superclasses"]:
        for parent in info["superclasses"]:
            print(f"- {_shrink_uri(graph, parent)}")
    else:
        print("- (none)")

    print("Subclasses:")
    if info["subclasses"]:
        for child in info["subclasses"]:
            print(f"- {_shrink_uri(graph, child)}")
    else:
        print("- (none)")

    print("Properties (direct):")
    if info["properties"]:
        for prop in info["properties"]:
            line = f"- {_shrink_uri(graph, prop['id'])} ({prop['type']})"
            ranges = prop.get("ranges") or []
            if ranges:
                line += f" -> {_format_values(graph, ranges)}"
            print(line)
            if prop.get("label"):
                print(f"  Label: {prop['label']}")
            if prop.get("description"):
                print(f"  Description: {prop['description']}")
    else:
        print("- (none)")

    if show_inherited:
        print("Properties (inherited):")
        if info["inherited_properties"]:
            for prop in info["inherited_properties"]:
                line = f"- {_shrink_uri(graph, prop['id'])} ({prop['type']})"
                ranges = prop.get("ranges") or []
                if ranges:
                    line += f" -> {_format_values(graph, ranges)}"
                print(line)
                if prop.get("label"):
                    print(f"  Label: {prop['label']}")
                if prop.get("description"):
                    print(f"  Description: {prop['description']}")
        else:
            print("- (none)")


def _print_instances(
    graph: Graph,
    instances: List[URIRef],
    *,
    limit: Optional[int],
    show_details: bool,
) -> None:
    if limit is not None:
        instances = instances[: max(limit, 0)]

    print("Instances:")
    if not instances:
        print("- (none)")
        return

    for inst in instances:
        label = graph.value(inst, RDFS.label)
        desc = graph.value(inst, URIRef(f"{CF_NAMESPACE}description"))
        line = f"- {_shrink_uri(graph, inst)}"
        if label:
            line += f" — {label}"
        print(line)
        if desc:
            print(f"  Description: {desc}")
        if not show_details:
            continue
        props = _collect_instance_properties(graph, inst)
        if not props:
            continue
        print("  Properties:")
        for pred, values in props:
            print(f"  - {_shrink_uri(graph, pred)}: {_format_values(graph, values)}")


def _print_instances_quads(
    om: OntologyManager,
    graph: Graph,
    instances: List[URIRef],
    *,
    limit: Optional[int],
    show_details: bool,
) -> None:
    if limit is not None:
        instances = instances[: max(limit, 0)]

    print("Instances:")
    if not instances:
        print("- (none)")
        return

    desc_pred = URIRef(f"{CF_NAMESPACE}description")
    alt_desc_pred = URIRef(f"{CF_NAMESPACE_ALT}description")

    for inst in instances:
        props = om.get_instance_properties_quads(str(inst))
        label_vals = props.get(RDFS.label, [])
        desc_vals = props.get(desc_pred, []) or props.get(alt_desc_pred, [])
        label = label_vals[0] if label_vals else None
        desc = desc_vals[0] if desc_vals else None
        line = f"- {_shrink_uri(graph, inst)}"
        if label:
            line += f" — {_format_value(graph, label)}"
        print(line)
        if desc:
            print(f"  Description: {_format_value(graph, desc)}")
        if not show_details:
            continue
        if not props:
            continue
        print("  Properties:")
        for pred in sorted(props.keys(), key=lambda p: _shrink_uri(graph, p)):
            values = props[pred]
            if pred == RDF.type:
                continue
            print(f"  - {_shrink_uri(graph, pred)}: {_format_values(graph, values)}")


def _print_step_info(
    graph: Graph,
    info: Dict[str, Any],
    *,
    emit: Optional[callable] = None,
) -> None:
    output = emit or print
    step_uri = info["id"]
    output(f"Step: {_shrink_uri(graph, step_uri)}")
    output(f"IRI: {step_uri}")
    if info.get("label"):
        output(f"Label: {info['label']}")
    if info.get("description"):
        output(f"Description: {info['description']}")
    if info.get("package"):
        output(f"Package: {_format_value(graph, info['package'])}")
    if info.get("category"):
        output(f"Category: {_format_value(graph, info['category'])}")
    if info.get("implementations"):
        output(f"Implementations: {_format_values(graph, info['implementations'])}")

    def _render_ports(title: str, ports: List[Dict[str, Any]]) -> None:
        output(f"{title}:")
        if not ports:
            output("- (none)")
            return
        for port in ports:
            name = _format_value(graph, port["name"]) if port.get("name") else "(unnamed)"
            ptype = _format_value(graph, port["type"]) if port.get("type") else "?"
            line = f"- {name} ({ptype})"
            if port.get("port_key"):
                line += f" key={_format_value(graph, port['port_key'])}"
            output(line)
            if port.get("description"):
                output(f"  Description: {_format_value(graph, port['description'])}")
            if port.get("optional") is True:
                output("  Optional: true")
            if port.get("optional") is False:
                output("  Optional: false")
            if port.get("default") is not None:
                output(f"  Default: {_format_value(graph, port['default'])}")
            if port.get("min") is not None or port.get("max") is not None:
                min_val = _format_value(graph, port["min"]) if port.get("min") is not None else ""
                max_val = _format_value(graph, port["max"]) if port.get("max") is not None else ""
                range_text = f"{min_val} .. {max_val}".strip()
                output(f"  Range: {range_text}")
            if port.get("allowed"):
                output(f"  Allowed: {_format_values(graph, port['allowed'])}")

    _render_ports("Inputs", info["inputs"])
    _render_ports("Outputs", info["outputs"])

    output("Parameters:")
    params = info["parameters"]
    if not params:
        output("- (none)")
    else:
        for param in params:
            name = _format_value(graph, param["name"])
            ptype = _format_value(graph, param["type"]) if param.get("type") else "?"
            line = f"- {name} ({ptype})"
            if param.get("required"):
                line += " required"
            output(line)
            if param.get("description"):
                output(f"  Description: {_format_value(graph, param['description'])}")
            if param.get("default") is not None:
                output(f"  Default: {_format_value(graph, param['default'])}")
            if param.get("min") is not None or param.get("max") is not None:
                min_val = _format_value(graph, param["min"]) if param.get("min") is not None else ""
                max_val = _format_value(graph, param["max"]) if param.get("max") is not None else ""
                range_text = f"{min_val} .. {max_val}".strip()
                output(f"  Range: {range_text}")
            if param.get("allowed"):
                output(f"  Allowed: {_format_values(graph, param['allowed'])}")


def _cmd_classes(args: argparse.Namespace) -> int:
    semantics_dir = _parse_semantics_dir(args.semantics_dir)
    _apply_semantics_env(semantics_dir, bool(args.enable_step_packages))
    om = OntologyManager(load_resources=False)
    if om.quads_available():
        context = om.get_quads_context()
        classes = om.list_classes_with_labels_quads(include_external=bool(args.all))
        print(f"Classes: {len(classes)}")
        for cls, label in classes:
            line = f"- {_shrink_uri_from_context(cls, context)}"
            if label:
                line += f" — {label}"
            print(line)
        return 0

    graph = om.graph
    classes = _collect_classes(graph, include_external=bool(args.all))
    print(f"Classes: {len(classes)}")
    for cls in classes:
        label = graph.value(cls, RDFS.label)
        line = f"- {_shrink_uri(graph, cls)}"
        if label:
            line += f" — {label}"
        print(line)
    return 0


def _cmd_class(args: argparse.Namespace) -> int:
    semantics_dir = _parse_semantics_dir(args.semantics_dir)
    _apply_semantics_env(semantics_dir, bool(args.enable_step_packages))
    om = OntologyManager(load_resources=False)
    if om.quads_available():
        graph = om.get_namespace_graph()
        info = om.get_class_info_quads(
            args.class_id,
            include_inherited=bool(args.include_inherited),
        )
        if not info:
            print(f"Class not found: {args.class_id}", file=sys.stderr)
            return 2
        _print_class_info(graph, info, show_inherited=bool(args.include_inherited))
        if args.instances:
            class_uri = info["id"]
            instances = om.list_instances_quads(
                str(class_uri),
                include_subclasses=bool(args.include_subclasses),
            )
            limit = None if args.instances_limit is None else int(args.instances_limit)
            _print_instances_quads(
                om,
                graph,
                instances,
                limit=limit,
                show_details=bool(args.instance_details),
            )
        return 0

    graph = om.graph
    classes = _collect_classes(graph, include_external=True)
    class_uri = _resolve_term(graph, args.class_id, candidates=classes)
    if class_uri is None or class_uri not in classes:
        print(f"Class not found: {args.class_id}", file=sys.stderr)
        return 2

    info = _describe_class(graph, class_uri, include_inherited=bool(args.include_inherited))
    _print_class_info(graph, info, show_inherited=bool(args.include_inherited))

    if args.instances:
        instances = _collect_instances(
            graph,
            class_uri,
            include_subclasses=bool(args.include_subclasses),
        )
        limit = None if args.instances_limit is None else int(args.instances_limit)
        _print_instances(
            graph,
            instances,
            limit=limit,
            show_details=bool(args.instance_details),
        )

    return 0


def _cmd_steps(args: argparse.Namespace) -> int:
    semantics_dir = _parse_semantics_dir(args.semantics_dir)
    _apply_semantics_env(semantics_dir, bool(args.enable_step_packages))
    om = OntologyManager(load_resources=False)
    if om.quads_available():
        graph = om.get_namespace_graph()
        if args.details:
            steps = om.list_processing_steps_quads()
            ordered_steps = sorted(steps, key=lambda n: _shrink_uri(graph, n))
            if args.limit is not None:
                ordered_steps = ordered_steps[: max(int(args.limit), 0)]

            print(f"ProcessingSteps: {len(ordered_steps)}")
            if not ordered_steps:
                print(
                    "No ProcessingStep instances found. If you expected step packages, "
                    "re-run with --enable-step-packages or set CF_ENABLE_STEP_PACKAGES=1."
                )
                return 0

            output_lines: list[str] = []
            for step_uri in ordered_steps:
                info = om.get_processing_step_info_quads(str(step_uri))
                if not info:
                    continue
                buffer: list[str] = []
                _print_step_info(graph, info, emit=buffer.append)
                output_lines.extend(buffer)
            if output_lines:
                print("\n".join(output_lines))
            return 0

        basics = om.list_processing_steps_basic_quads()
        if args.limit is not None:
            basics = basics[: max(int(args.limit), 0)]

        print(f"ProcessingSteps: {len(basics)}")
        if not basics:
            print(
                "No ProcessingStep instances found. If you expected step packages, "
                "re-run with --enable-step-packages or set CF_ENABLE_STEP_PACKAGES=1."
            )
            return 0
        lines: list[str] = []
        for entry in basics:
            step_uri = entry["id"]
            label = entry.get("label")
            line = f"- {_shrink_uri(graph, step_uri)}"
            if label:
                line += f" — {label}"
            lines.append(line)
            if entry.get("description"):
                lines.append(f"  Description: {entry['description']}")
        if lines:
            print("\n".join(lines))
        return 0

    graph = om.graph
    step_type = URIRef(f"{CF_NAMESPACE}ProcessingStep")
    step_type_alt = URIRef(f"{CF_NAMESPACE_ALT}ProcessingStep")
    steps = {
        subj
        for subj in graph.subjects(RDF.type, step_type)
        if isinstance(subj, URIRef)
    }
    steps.update(
        {
            subj
            for subj in graph.subjects(RDF.type, step_type_alt)
            if isinstance(subj, URIRef)
        }
    )

    ordered_steps = sorted(steps, key=lambda n: _shrink_uri(graph, n))
    if args.limit is not None:
        ordered_steps = ordered_steps[: max(int(args.limit), 0)]

    print(f"ProcessingSteps: {len(ordered_steps)}")
    if not ordered_steps:
        print(
            "No ProcessingStep instances found. If you expected step packages, "
            "re-run with --enable-step-packages or set CF_ENABLE_STEP_PACKAGES=1."
        )
        return 0

    for step_uri in ordered_steps:
        info = _describe_step(graph, step_uri)
        if args.details:
            _print_step_info(graph, info)
        else:
            label = info.get("label")
            line = f"- {_shrink_uri(graph, step_uri)}"
            if label:
                line += f" — {label}"
            print(line)
            if info.get("description"):
                print(f"  Description: {info['description']}")
    return 0


def _cmd_step(args: argparse.Namespace) -> int:
    semantics_dir = _parse_semantics_dir(args.semantics_dir)
    _apply_semantics_env(semantics_dir, bool(args.enable_step_packages))
    om = OntologyManager(load_resources=False)
    if om.quads_available():
        graph = om.get_namespace_graph()
        info = om.get_processing_step_info_quads(args.step_id)
        if not info:
            print(f"Step not found: {args.step_id}", file=sys.stderr)
            return 2
        _print_step_info(graph, info)
        return 0

    graph = om.graph
    step_type = URIRef(f"{CF_NAMESPACE}ProcessingStep")
    step_type_alt = URIRef(f"{CF_NAMESPACE_ALT}ProcessingStep")
    steps = {
        subj
        for subj in graph.subjects(RDF.type, step_type)
        if isinstance(subj, URIRef)
    }
    steps.update(
        {
            subj
            for subj in graph.subjects(RDF.type, step_type_alt)
            if isinstance(subj, URIRef)
        }
    )

    step_uri = _resolve_term(graph, args.step_id, candidates=steps)
    if step_uri is None or step_uri not in steps:
        print(f"Step not found: {args.step_id}", file=sys.stderr)
        return 2

    info = _describe_step(graph, step_uri)
    _print_step_info(graph, info)
    return 0


def _register_cf_cli() -> None:
    if REGISTER_COMMAND is None:
        return

    def _add_classes_parser(
        subparsers: argparse._SubParsersAction,
    ) -> argparse.ArgumentParser:
        parser = subparsers.add_parser("classes", help="List ontology classes.")
        _add_semantics_args(parser)
        parser.add_argument(
            "--all",
            action="store_true",
            help="Include classes outside the Cogniflow namespace.",
        )
        parser.set_defaults(_cf_handler=_cmd_classes)
        return parser

    def _add_class_parser(
        subparsers: argparse._SubParsersAction,
    ) -> argparse.ArgumentParser:
        parser = subparsers.add_parser("class", help="Describe an ontology class.")
        _add_semantics_args(parser)
        parser.add_argument("class_id", help="Class CURIE/IRI (e.g. cf:ProcessingStep).")
        parser.add_argument(
            "--include-inherited",
            action="store_true",
            help="Include properties inherited from superclasses.",
        )
        parser.add_argument(
            "--instances",
            action="store_true",
            help="List instances of the class.",
        )
        parser.add_argument(
            "--include-subclasses",
            action="store_true",
            help="Include instances of subclasses when listing instances.",
        )
        parser.add_argument(
            "--instances-limit",
            type=int,
            default=None,
            help="Limit number of instances shown.",
        )
        parser.add_argument(
            "--instance-details",
            action="store_true",
            help="Include predicate/value properties for each instance.",
        )
        parser.set_defaults(_cf_handler=_cmd_class)
        return parser

    def _add_steps_parser(
        subparsers: argparse._SubParsersAction,
    ) -> argparse.ArgumentParser:
        parser = subparsers.add_parser("steps", help="List ProcessingStep instances.")
        _add_semantics_args(parser)
        parser.add_argument(
            "--details",
            action="store_true",
            help="Print full step details (ports, parameters).",
        )
        parser.add_argument(
            "--limit",
            type=int,
            default=None,
            help="Limit number of steps shown.",
        )
        parser.set_defaults(_cf_handler=_cmd_steps)
        return parser

    def _add_step_parser(
        subparsers: argparse._SubParsersAction,
    ) -> argparse.ArgumentParser:
        parser = subparsers.add_parser("step", help="Describe a ProcessingStep.")
        _add_semantics_args(parser)
        parser.add_argument("step_id", help="Step CURIE/IRI (e.g. cfbs:AverageStep).")
        parser.set_defaults(_cf_handler=_cmd_step)
        return parser

    REGISTER_COMMAND(
        "ontology",
        "classes",
        _add_classes_parser,
        group_help="Ontology inspection",
        command_help="List ontology classes",
    )
    REGISTER_COMMAND(
        "ontology",
        "class",
        _add_class_parser,
        group_help="Ontology inspection",
        command_help="Describe an ontology class",
    )
    REGISTER_COMMAND(
        "ontology",
        "steps",
        _add_steps_parser,
        group_help="Ontology inspection",
        command_help="List processing steps",
    )
    REGISTER_COMMAND(
        "ontology",
        "step",
        _add_step_parser,
        group_help="Ontology inspection",
        command_help="Describe a processing step",
    )


_register_cf_cli()
