/**
 * JSON Schema → ``FieldNode`` tree.
 *
 *  Handles the OpenAPI 3.x subset we actually see: objects with
 *  ``properties`` + ``required``, arrays with ``items``, enums,
 *  primitives with ``format``. ``allOf`` / ``oneOf`` / ``anyOf`` are
 *  flattened best-effort — we pick the first branch so the UI shows
 *  *a* shape rather than an empty row. Upstream dereferencing
 *  (``dereferenceSchema``) is expected to have resolved ``$ref`` before
 *  this runs.
 */

import type { FieldKind, FieldNode } from './types';

type JsonSchemaNode = Record<string, unknown> & {
    type?: string;
    properties?: Record<string, JsonSchemaNode>;
    required?: string[];
    items?: JsonSchemaNode;
    enum?: unknown[];
    description?: string;
    format?: string;
    allOf?: JsonSchemaNode[];
    oneOf?: JsonSchemaNode[];
    anyOf?: JsonSchemaNode[];
};

/** Hard cap on recursion so self-referential schemas can't blow the
 *  call stack. Anything this deep is unreadable in a docs view anyway. */
const MAX_DEPTH = 5;

/** Merge ``allOf`` branches into a single pseudo-object. Shallow merge
 *  is enough for the display — deep merge would require semantic JSON
 *  Schema reasoning that we don't need for a read-only viewer. */
function mergeAllOf(branches: JsonSchemaNode[]): JsonSchemaNode {
    const properties: Record<string, JsonSchemaNode> = {};
    const required: string[] = [];
    for (const b of branches) {
        if (b.properties) Object.assign(properties, b.properties);
        if (Array.isArray(b.required)) required.push(...b.required);
    }
    return { type: 'object', properties, required };
}

function describeType(node: JsonSchemaNode): { label: string; kind: FieldKind } {
    if (node.type === 'array') {
        const itemLabel = node.items ? describeType(node.items).label : 'any';
        return { label: `array<${itemLabel}>`, kind: 'array' };
    }
    if (node.type === 'object' || node.properties) {
        return { label: 'object', kind: 'object' };
    }
    const base = node.type || 'any';
    if (Array.isArray(node.enum) && node.enum.length > 0) {
        return { label: `${base} enum`, kind: 'primitive' };
    }
    if (node.format) {
        return { label: `${base} (${node.format})`, kind: 'primitive' };
    }
    return { label: base, kind: 'primitive' };
}

function resolveCombinators(node: JsonSchemaNode): JsonSchemaNode {
    // ``allOf`` → merge shallowly. Other combinators → pick the first
    // branch (see module docstring rationale).
    if (Array.isArray(node.allOf) && node.allOf.length > 0) {
        return { ...mergeAllOf(node.allOf), description: node.description };
    }
    if (Array.isArray(node.oneOf) && node.oneOf.length > 0) {
        return { ...node.oneOf[0]!, description: node.description ?? node.oneOf[0]!.description };
    }
    if (Array.isArray(node.anyOf) && node.anyOf.length > 0) {
        return { ...node.anyOf[0]!, description: node.description ?? node.anyOf[0]!.description };
    }
    return node;
}

function buildNode(
    name: string,
    schema: JsonSchemaNode,
    isRequired: boolean,
    depth: number,
): FieldNode {
    const resolved = resolveCombinators(schema);
    const { label, kind } = describeType(resolved);
    const node: FieldNode = {
        name,
        type: label,
        kind,
        required: isRequired,
        description: resolved.description,
    };

    if (Array.isArray(resolved.enum) && resolved.enum.length > 0) {
        node.enumValues = resolved.enum.map((v) => String(v));
    }

    if (depth >= MAX_DEPTH) return node;

    if (kind === 'object' && resolved.properties) {
        const required = new Set(resolved.required ?? []);
        node.children = Object.entries(resolved.properties).map(([key, child]) =>
            buildNode(key, child, required.has(key), depth + 1),
        );
    } else if (kind === 'array' && resolved.items) {
        // Synthesise a single ``[]`` child. If items are objects, the
        // child's own children will appear — so the tree reads as
        // ``tags → [] → { id, name }``.
        node.children = [buildNode('[]', resolved.items, false, depth + 1)];
    }

    return node;
}

/** Top-level entry. The root schema is usually an object or array; we
 *  return its children directly so the tree starts one level deep
 *  (skipping the redundant "root" node). */
export function buildSchemaTree(schema: JsonSchemaNode | undefined): FieldNode[] {
    if (!schema) return [];
    const root = buildNode('', schema, false, 0);
    if (root.children && root.children.length > 0) return root.children;
    // Primitive body (rare) — return the root itself as a one-row tree.
    if (root.kind === 'primitive' || (!root.children && root.name === '')) {
        return [{ ...root, name: root.name || '(body)' }];
    }
    return [];
}
