/* ============================================================================
 * Copyright (c) Palo Alto Networks
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 * ========================================================================== */

import React, { useCallback } from "react";

import { translate } from "@docusaurus/Translate";
import { setSchemaSelection } from "@theme/ApiExplorer/SchemaSelection/slice";
import { useTypedDispatch } from "@theme/ApiItem/hooks";
import { ClosingArrayBracket, OpeningArrayBracket } from "@theme/ArrayBrackets";
import Details from "@theme/Details";
import DiscriminatorTabs from "@theme/DiscriminatorTabs";
import Markdown from "@theme/Markdown";
import SchemaItem from "@theme/SchemaItem";
import SchemaTabs from "@theme/SchemaTabs";
import TabItem from "@theme/TabItem";
import { OPENAPI_SCHEMA_ITEM } from "@theme/translationIds";
// eslint-disable-next-line import/no-extraneous-dependencies
import { merge } from "allof-merge";
import clsx from "clsx";
import isEmpty from "lodash/isEmpty";

import { getQualifierMessage, getSchemaName } from "../../markdown/schema";
import type { SchemaObject } from "../../types.d";

// eslint-disable-next-line import/no-extraneous-dependencies
// const jsonSchemaMergeAllOf = require("json-schema-merge-allof");

const mergeAllOf = (allOf: any) => {
  const onMergeError = (msg: string) => {
    console.warn(msg);
  };

  const mergedSchemas = merge(allOf, { onMergeError });

  return mergedSchemas ?? {};
};

/**
 * Recursively searches for a property in a schema, including nested
 * oneOf, anyOf, and allOf structures. This is needed for discriminators
 * where the property definition may be in a nested schema.
 */
const findProperty = (
  schema: SchemaObject,
  propertyName: string
): SchemaObject | undefined => {
  // Check direct properties first
  if (schema.properties?.[propertyName]) {
    return schema.properties[propertyName];
  }

  // Search in oneOf schemas
  if (schema.oneOf) {
    for (const subschema of schema.oneOf) {
      const found = findProperty(subschema as SchemaObject, propertyName);
      if (found) return found;
    }
  }

  // Search in anyOf schemas
  if (schema.anyOf) {
    for (const subschema of schema.anyOf) {
      const found = findProperty(subschema as SchemaObject, propertyName);
      if (found) return found;
    }
  }

  // Search in allOf schemas
  if (schema.allOf) {
    for (const subschema of schema.allOf) {
      const found = findProperty(subschema as SchemaObject, propertyName);
      if (found) return found;
    }
  }

  return undefined;
};

/**
 * Recursively searches for a discriminator in a schema, including nested
 * oneOf, anyOf, and allOf structures.
 */
const findDiscriminator = (schema: SchemaObject): any | undefined => {
  if (schema.discriminator) {
    return schema.discriminator;
  }

  if (schema.oneOf) {
    for (const subschema of schema.oneOf) {
      const found = findDiscriminator(subschema as SchemaObject);
      if (found) return found;
    }
  }

  if (schema.anyOf) {
    for (const subschema of schema.anyOf) {
      const found = findDiscriminator(subschema as SchemaObject);
      if (found) return found;
    }
  }

  if (schema.allOf) {
    for (const subschema of schema.allOf) {
      const found = findDiscriminator(subschema as SchemaObject);
      if (found) return found;
    }
  }

  return undefined;
};

interface MarkdownProps {
  text: string | undefined;
}

// Renders string as markdown, useful for descriptions and qualifiers
const MarkdownWrapper: React.FC<MarkdownProps> = ({ text }) => {
  return (
    <div style={{ marginTop: ".5rem", marginBottom: ".5rem" }}>
      <Markdown>{text}</Markdown>
    </div>
  );
};

interface SummaryProps {
  name: string;
  schemaName: string | undefined;
  schema: {
    deprecated?: boolean;
    nullable?: boolean;
  };
  required?: boolean | string[];
}

const Summary: React.FC<SummaryProps> = ({
  name,
  schemaName,
  schema,
  required,
}) => {
  const { deprecated, nullable } = schema;

  const isRequired = Array.isArray(required)
    ? required.includes(name)
    : required === true;

  return (
    <summary>
      <span className="openapi-schema__container">
        <strong
          className={clsx("openapi-schema__property", {
            "openapi-schema__strikethrough": deprecated,
          })}
        >
          {name}
        </strong>
        <span className="openapi-schema__name"> {schemaName}</span>
        {(isRequired || deprecated || nullable) && (
          <span className="openapi-schema__divider" />
        )}
        {nullable && (
          <span className="openapi-schema__nullable">
            {translate({
              id: OPENAPI_SCHEMA_ITEM.NULLABLE,
              message: "nullable",
            })}
          </span>
        )}
        {isRequired && (
          <span className="openapi-schema__required">
            {translate({
              id: OPENAPI_SCHEMA_ITEM.REQUIRED,
              message: "required",
            })}
          </span>
        )}
        {deprecated && (
          <span className="openapi-schema__deprecated">
            {translate({
              id: OPENAPI_SCHEMA_ITEM.DEPRECATED,
              message: "deprecated",
            })}
          </span>
        )}
      </span>
    </summary>
  );
};

// Common props interface
interface SchemaProps {
  schema: SchemaObject;
  schemaType: "request" | "response";
  /**
   * Optional path identifier for tracking anyOf/oneOf selections.
   * When provided, tab selections will be dispatched to Redux state
   * to enable dynamic body example updates.
   */
  schemaPath?: string;
}

const AnyOneOf: React.FC<SchemaProps> = ({
  schema,
  schemaType,
  schemaPath,
}) => {
  const key = schema.oneOf ? "oneOf" : "anyOf";
  const schemaArray = schema[key];

  // Empty oneOf/anyOf arrays are valid in OpenAPI specs but would cause the
  // Tabs component to throw "requires at least one TabItem". Return null instead.
  if (!schemaArray || !Array.isArray(schemaArray) || schemaArray.length === 0) {
    return null;
  }

  const type = schema.oneOf
    ? translate({ id: OPENAPI_SCHEMA_ITEM.ONE_OF, message: "oneOf" })
    : translate({ id: OPENAPI_SCHEMA_ITEM.ANY_OF, message: "anyOf" });

  // Generate a unique ID for this anyOf/oneOf to prevent tab value collisions
  const uniqueId = React.useMemo(
    () => Math.random().toString(36).substring(7),
    []
  );

  // Try to get Redux dispatch - will be undefined if not inside a Provider
  let dispatch: ReturnType<typeof useTypedDispatch> | undefined;
  try {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    dispatch = useTypedDispatch();
  } catch {
    // Not inside a Redux Provider, which is fine for response schemas
    dispatch = undefined;
  }

  // Handle tab change - dispatch to Redux if schemaPath is provided
  const handleTabChange = useCallback(
    (index: number) => {
      if (schemaPath && dispatch) {
        dispatch(setSchemaSelection({ path: schemaPath, index }));
      }
    },
    [schemaPath, dispatch]
  );

  return (
    <>
      <span className="badge badge--info" style={{ marginBottom: "1rem" }}>
        {type}
      </span>
      <SchemaTabs
        groupId={`schema-${uniqueId}`}
        lazy
        onChange={handleTabChange}
      >
        {schema[key]?.map((anyOneSchema: any, index: number) => {
          // Use getSchemaName to include format info (e.g., "string<date-time>")
          const computedSchemaName = getSchemaName(anyOneSchema);

          // Determine label for the tab
          // Prefer explicit title, then computed schema name, then raw type
          let label =
            anyOneSchema.title || computedSchemaName || anyOneSchema.type;
          if (!label) {
            if (anyOneSchema.oneOf) {
              label = translate({
                id: OPENAPI_SCHEMA_ITEM.ONE_OF,
                message: "oneOf",
              });
            } else if (anyOneSchema.anyOf) {
              label = translate({
                id: OPENAPI_SCHEMA_ITEM.ANY_OF,
                message: "anyOf",
              });
            } else {
              label = `Option ${index + 1}`;
            }
          }

          // Build the nested schemaPath for child anyOf/oneOf
          const childSchemaPath = schemaPath
            ? `${schemaPath}.${index}`
            : undefined;

          return (
            // @ts-ignore
            <TabItem
              key={index}
              label={label}
              value={`${uniqueId}-${index}-item`}
            >
              {/* Handle primitive types directly */}
              {(isPrimitive(anyOneSchema) || anyOneSchema.const) && (
                <SchemaItem
                  collapsible={false}
                  name={undefined}
                  schemaName={computedSchemaName}
                  schema={anyOneSchema}
                  discriminator={false}
                  children={null}
                />
              )}

              {/* Handle empty object as a primitive type */}
              {anyOneSchema.type === "object" &&
                !anyOneSchema.properties &&
                !anyOneSchema.allOf &&
                !anyOneSchema.oneOf &&
                !anyOneSchema.anyOf && (
                  <SchemaItem
                    collapsible={false}
                    name={undefined}
                    schemaName={computedSchemaName}
                    schema={anyOneSchema}
                    discriminator={false}
                    children={null}
                  />
                )}

              {/* Handle actual object types with properties or nested schemas */}
              {/* Note: In OpenAPI, properties implies type: object even if not explicitly set */}
              {(anyOneSchema.type === "object" || !anyOneSchema.type) &&
                anyOneSchema.properties && (
                  <Properties
                    schema={anyOneSchema}
                    schemaType={schemaType}
                    schemaPath={childSchemaPath}
                  />
                )}
              {anyOneSchema.allOf && (
                <SchemaNode
                  schema={anyOneSchema}
                  schemaType={schemaType}
                  schemaPath={childSchemaPath}
                />
              )}
              {anyOneSchema.oneOf && (
                <SchemaNode
                  schema={anyOneSchema}
                  schemaType={schemaType}
                  schemaPath={childSchemaPath}
                />
              )}
              {anyOneSchema.anyOf && (
                <SchemaNode
                  schema={anyOneSchema}
                  schemaType={schemaType}
                  schemaPath={childSchemaPath}
                />
              )}
              {anyOneSchema.items && (
                <Items
                  schema={anyOneSchema}
                  schemaType={schemaType}
                  schemaPath={childSchemaPath}
                />
              )}
            </TabItem>
          );
        })}
      </SchemaTabs>
    </>
  );
};

const Properties: React.FC<SchemaProps> = ({
  schema,
  schemaType,
  schemaPath,
}) => {
  const discriminator = schema.discriminator;
  if (discriminator && !discriminator.mapping) {
    const anyOneOf = schema.oneOf ?? schema.anyOf ?? {};
    const inferredMapping = {} as any;
    Object.entries(anyOneOf).map(([_, anyOneSchema]: [string, any]) => {
      // ensure discriminated property only renders once
      if (
        schema.properties![discriminator.propertyName] &&
        anyOneSchema.properties[discriminator.propertyName]
      )
        delete anyOneSchema.properties[discriminator.propertyName];
      return (inferredMapping[anyOneSchema.title] = anyOneSchema);
    });
    discriminator["mapping"] = inferredMapping;
  }
  if (Object.keys(schema.properties as {}).length === 0) {
    // Hide placeholder only for discriminator cleanup artifacts; preserve
    // empty object rendering for schemas that intentionally define no properties.
    if (discriminator) {
      return null;
    }
    return (
      <SchemaItem
        collapsible={false}
        name=""
        required={false}
        schemaName="object"
        schema={{}}
      />
    );
  }

  return (
    <>
      {Object.entries(schema.properties as {}).map(
        ([key, val]: [string, any]) => (
          <SchemaEdge
            key={key}
            name={key}
            schema={val}
            required={
              Array.isArray(schema.required)
                ? schema.required.includes(key)
                : false
            }
            discriminator={discriminator}
            schemaType={schemaType}
            schemaPath={schemaPath ? `${schemaPath}.${key}` : undefined}
          />
        )
      )}
    </>
  );
};

const PropertyDiscriminator: React.FC<SchemaEdgeProps> = ({
  name,
  schemaName,
  schema,
  schemaType,
  discriminator,
  required,
}) => {
  if (!schema) {
    return null;
  }

  return (
    <>
      <div className="openapi-discriminator__item openapi-schema__list-item">
        <div>
          <span className="openapi-schema__container">
            <strong className="openapi-discriminator__name openapi-schema__property">
              {name}
            </strong>
            {schemaName && (
              <span className="openapi-schema__name"> {schemaName}</span>
            )}
            {required && <span className="openapi-schema__divider"></span>}
            {required && (
              <span className="openapi-schema__required">
                {translate({
                  id: OPENAPI_SCHEMA_ITEM.REQUIRED,
                  message: "required",
                })}
              </span>
            )}
          </span>
          <div style={{ marginLeft: "1rem" }}>
            {schema.description && (
              <MarkdownWrapper text={schema.description} />
            )}
            {getQualifierMessage(discriminator) && (
              <MarkdownWrapper text={getQualifierMessage(discriminator)} />
            )}
          </div>
          <DiscriminatorTabs className="openapi-tabs__discriminator">
            {Object.keys(discriminator.mapping).map((key, index) => (
              // @ts-ignore
              <TabItem
                key={index}
                label={key}
                value={`${index}-item-discriminator`}
              >
                <SchemaNode
                  schema={discriminator.mapping[key]}
                  schemaType={schemaType}
                />
              </TabItem>
            ))}
          </DiscriminatorTabs>
        </div>
      </div>
      {schema.properties &&
        Object.entries(schema.properties as {}).map(
          ([key, val]: [string, any]) =>
            key !== discriminator.propertyName && (
              <SchemaEdge
                key={key}
                name={key}
                schema={val}
                required={
                  Array.isArray(schema.required)
                    ? schema.required.includes(key)
                    : false
                }
                discriminator={false}
                schemaType={schemaType}
              />
            )
        )}
    </>
  );
};

interface DiscriminatorNodeProps {
  discriminator: any;
  schema: SchemaObject;
  schemaType: "request" | "response";
}

const DiscriminatorNode: React.FC<DiscriminatorNodeProps> = ({
  discriminator,
  schema,
  schemaType,
}) => {
  let discriminatedSchemas: any = {};
  let inferredMapping: any = {};

  // Search for the discriminator property in the schema, including nested structures
  const discriminatorProperty =
    findProperty(schema, discriminator.propertyName) ?? {};

  if (schema.allOf) {
    const mergedSchemas = mergeAllOf(schema) as SchemaObject;
    if (mergedSchemas.oneOf || mergedSchemas.anyOf) {
      discriminatedSchemas = mergedSchemas.oneOf || mergedSchemas.anyOf;
    }
  } else if (schema.oneOf || schema.anyOf) {
    discriminatedSchemas = schema.oneOf || schema.anyOf;
  }

  // Handle case where no mapping is defined
  if (!discriminator.mapping) {
    Object.entries(discriminatedSchemas).forEach(
      ([_, subschema]: [string, any], index) => {
        inferredMapping[subschema.title ?? `PROP${index}`] = subschema;
      }
    );
    discriminator.mapping = inferredMapping;
  }

  // Merge sub schema discriminator property with parent
  Object.keys(discriminator.mapping).forEach((key) => {
    const subSchema = discriminator.mapping[key];

    // Handle discriminated schema with allOf
    let mergedSubSchema = {} as SchemaObject;
    if (subSchema.allOf) {
      mergedSubSchema = mergeAllOf(subSchema) as SchemaObject;
    }

    const subProperties = subSchema.properties || mergedSubSchema.properties;
    // Add a safeguard check to avoid referencing subProperties if it's undefined
    if (subProperties && subProperties[discriminator.propertyName]) {
      if (schema.properties) {
        schema.properties![discriminator.propertyName] = {
          ...schema.properties![discriminator.propertyName],
          ...subProperties[discriminator.propertyName],
        };
        if (subSchema.required && !schema.required) {
          schema.required = subSchema.required;
        }
        // Avoid duplicating property
        delete subProperties[discriminator.propertyName];
      } else {
        schema.properties = {};
        schema.properties[discriminator.propertyName] =
          subProperties[discriminator.propertyName];
        // Avoid duplicating property
        delete subProperties[discriminator.propertyName];
      }
    }
  });

  const name = discriminator.propertyName;
  const schemaName = getSchemaName(discriminatorProperty);
  // Default case for discriminator without oneOf/anyOf/allOf
  return (
    <PropertyDiscriminator
      name={name}
      schemaName={schemaName}
      schema={schema}
      schemaType={schemaType}
      discriminator={discriminator}
      required={
        Array.isArray(schema.required)
          ? schema.required.includes(name)
          : schema.required
      }
    />
  );
};

const AdditionalProperties: React.FC<SchemaProps> = ({
  schema,
  schemaType,
}) => {
  const additionalProperties = schema.additionalProperties;

  if (!additionalProperties) return null;

  // Handle free-form objects
  if (additionalProperties === true || isEmpty(additionalProperties)) {
    return (
      <SchemaItem
        name="property name*"
        required={false}
        schemaName="any"
        schema={schema}
        collapsible={false}
        discriminator={false}
      />
    );
  }

  // Handle objects, arrays, complex schemas
  if (
    additionalProperties.properties ||
    additionalProperties.items ||
    additionalProperties.allOf ||
    additionalProperties.additionalProperties ||
    additionalProperties.oneOf ||
    additionalProperties.anyOf
  ) {
    const title =
      additionalProperties.title || getSchemaName(additionalProperties);
    const required = schema.required || false;
    return (
      <SchemaNodeDetails
        name="property name*"
        schemaName={title}
        required={required}
        nullable={schema.nullable}
        schema={additionalProperties}
        schemaType={schemaType}
      />
    );
  }

  // Handle primitive types
  if (
    additionalProperties.type === "string" ||
    additionalProperties.type === "boolean" ||
    additionalProperties.type === "integer" ||
    additionalProperties.type === "number" ||
    additionalProperties.type === "object"
  ) {
    const schemaName = getSchemaName(additionalProperties);
    return (
      <SchemaItem
        name="property name*"
        required={false}
        schemaName={schemaName}
        schema={additionalProperties}
        collapsible={false}
        discriminator={false}
        children={null}
      />
    );
  }

  // Unknown type
  return null;
};

const SchemaNodeDetails: React.FC<SchemaEdgeProps> = ({
  name,
  schemaName,
  schema,
  required,
  schemaType,
  schemaPath,
}) => {
  return (
    <SchemaItem collapsible={true}>
      <Details
        className="openapi-markdown__details"
        summary={
          <Summary
            name={name}
            schemaName={schemaName}
            schema={schema}
            required={required}
          />
        }
      >
        <div style={{ marginLeft: "1rem" }}>
          {schema.description && <MarkdownWrapper text={schema.description} />}
          {getQualifierMessage(schema) && (
            <MarkdownWrapper text={getQualifierMessage(schema)} />
          )}
          <SchemaNode
            schema={schema}
            schemaType={schemaType}
            schemaPath={schemaPath}
          />
        </div>
      </Details>
    </SchemaItem>
  );
};

const Items: React.FC<{
  schema: any;
  schemaType: "request" | "response";
  schemaPath?: string;
}> = ({ schema, schemaType, schemaPath }) => {
  // Process schema.items to handle allOf merging
  let itemsSchema = schema.items;
  if (schema.items?.allOf) {
    itemsSchema = mergeAllOf(schema.items) as SchemaObject;
  }

  // Handle complex schemas with multiple schema types
  const hasOneOfAnyOf = itemsSchema?.oneOf || itemsSchema?.anyOf;
  const hasProperties = itemsSchema?.properties;
  const hasAdditionalProperties = itemsSchema?.additionalProperties;

  // Build the items schema path
  const itemsSchemaPath = schemaPath ? `${schemaPath}.items` : undefined;

  if (hasOneOfAnyOf || hasProperties || hasAdditionalProperties) {
    return (
      <>
        <OpeningArrayBracket />
        {hasOneOfAnyOf && (
          <AnyOneOf
            schema={itemsSchema}
            schemaType={schemaType}
            schemaPath={itemsSchemaPath}
          />
        )}
        {hasProperties && (
          <Properties
            schema={itemsSchema}
            schemaType={schemaType}
            schemaPath={itemsSchemaPath}
          />
        )}
        {hasAdditionalProperties && (
          <AdditionalProperties schema={itemsSchema} schemaType={schemaType} />
        )}
        <ClosingArrayBracket />
      </>
    );
  }

  // Handles basic types (string, number, integer, boolean, object)
  if (
    itemsSchema?.type === "string" ||
    itemsSchema?.type === "number" ||
    itemsSchema?.type === "integer" ||
    itemsSchema?.type === "boolean" ||
    itemsSchema?.type === "object"
  ) {
    return (
      <div style={{ marginLeft: ".5rem" }}>
        <OpeningArrayBracket />
        <SchemaItem
          collapsible={false}
          name="" // No name for array items
          schemaName={getSchemaName(itemsSchema)}
          schema={itemsSchema}
          discriminator={false}
          children={null}
        />
        <ClosingArrayBracket />
      </div>
    );
  }

  // Handles fallback case (use createEdges logic)
  return (
    <>
      <OpeningArrayBracket />
      {Object.entries(itemsSchema || {}).map(([key, val]: [string, any]) => (
        <SchemaEdge
          key={key}
          name={key}
          schema={val}
          schemaType={schemaType}
          required={
            Array.isArray(schema.required)
              ? schema.required.includes(key)
              : false
          }
        />
      ))}
      <ClosingArrayBracket />
    </>
  );
};

interface SchemaEdgeProps {
  name: string;
  schemaName?: string;
  schema: SchemaObject;
  required?: boolean | string[];
  nullable?: boolean | undefined;
  discriminator?: any;
  schemaType: "request" | "response";
  schemaPath?: string;
}

const SchemaEdge: React.FC<SchemaEdgeProps> = ({
  name,
  schema,
  required,
  discriminator,
  schemaType,
  schemaPath,
}) => {
  if (
    (schemaType === "request" && schema.readOnly) ||
    (schemaType === "response" && schema.writeOnly)
  ) {
    return null;
  }

  const schemaName = getSchemaName(schema);

  if (discriminator && discriminator.propertyName === name) {
    return (
      <PropertyDiscriminator
        name={name}
        schemaName={schemaName}
        schema={schema}
        schemaType={schemaType}
        discriminator={discriminator}
        required={required}
      />
    );
  }

  if (schema.oneOf || schema.anyOf) {
    // return <AnyOneOf schema={schema} schemaType={schemaType} />;
    return (
      <SchemaNodeDetails
        name={name}
        schemaName={schemaName}
        schemaType={schemaType}
        required={required}
        schema={schema}
        nullable={schema.nullable}
        schemaPath={schemaPath}
      />
    );
  }

  if (schema.properties) {
    return (
      <SchemaNodeDetails
        name={name}
        schemaName={schemaName}
        schemaType={schemaType}
        required={required}
        schema={schema}
        nullable={schema.nullable}
        schemaPath={schemaPath}
      />
    );
  }

  if (schema.additionalProperties) {
    return (
      <SchemaNodeDetails
        name={name}
        schemaName={schemaName}
        schemaType={schemaType}
        required={required}
        schema={schema}
        nullable={schema.nullable}
        schemaPath={schemaPath}
      />
    );
  }

  if (schema.items?.properties) {
    return (
      <SchemaNodeDetails
        name={name}
        schemaName={schemaName}
        required={required}
        nullable={schema.nullable}
        schema={schema}
        schemaType={schemaType}
        schemaPath={schemaPath}
      />
    );
  }

  if (schema.items?.anyOf || schema.items?.oneOf || schema.items?.allOf) {
    return (
      <SchemaNodeDetails
        name={name}
        schemaName={schemaName}
        required={required}
        nullable={schema.nullable}
        schema={schema}
        schemaType={schemaType}
        schemaPath={schemaPath}
      />
    );
  }

  if (schema.allOf) {
    // handle circular properties
    if (
      schema.allOf &&
      schema.allOf.length &&
      schema.allOf.length === 1 &&
      typeof schema.allOf[0] === "string"
    ) {
      return (
        <SchemaItem
          collapsible={false}
          name={name}
          required={
            Array.isArray(required) ? required.includes(name) : required
          }
          schemaName={schema.allOf[0]}
          schema={schema.allOf[0]}
          discriminator={false}
          children={null}
        />
      );
    }
    const mergedSchemas = mergeAllOf(schema) as SchemaObject;

    if (
      (schemaType === "request" && mergedSchemas.readOnly) ||
      (schemaType === "response" && mergedSchemas.writeOnly)
    ) {
      return null;
    }

    const mergedSchemaName = getSchemaName(mergedSchemas);

    if (mergedSchemas.oneOf || mergedSchemas.anyOf) {
      return (
        <SchemaNodeDetails
          name={name}
          schemaName={mergedSchemaName}
          required={
            Array.isArray(required) ? required.includes(name) : required
          }
          nullable={mergedSchemas.nullable}
          schema={mergedSchemas}
          schemaType={schemaType}
          schemaPath={schemaPath}
        />
      );
    }

    if (mergedSchemas.properties !== undefined) {
      return (
        <SchemaNodeDetails
          name={name}
          schemaName={mergedSchemaName}
          required={
            Array.isArray(required) ? required.includes(name) : required
          }
          nullable={mergedSchemas.nullable}
          schema={mergedSchemas}
          schemaType={schemaType}
          schemaPath={schemaPath}
        />
      );
    }

    if (mergedSchemas.items?.properties) {
      return (
        <SchemaNodeDetails
          name={name}
          schemaName={mergedSchemaName}
          required={
            Array.isArray(required) ? required.includes(name) : required
          }
          nullable={mergedSchemas.nullable}
          schema={mergedSchemas}
          schemaType={schemaType}
          schemaPath={schemaPath}
        />
      );
    }

    return (
      <SchemaItem
        collapsible={false}
        name={name}
        required={Array.isArray(required) ? required.includes(name) : required}
        schemaName={mergedSchemaName}
        schema={mergedSchemas}
        discriminator={false}
        children={null}
      />
    );
  }

  return (
    <SchemaItem
      collapsible={false}
      name={name}
      required={Array.isArray(required) ? required.includes(name) : required}
      schemaName={schemaName}
      schema={schema}
      discriminator={false}
      children={null}
    />
  );
};

function renderChildren(
  schema: SchemaObject,
  schemaType: "request" | "response",
  schemaPath?: string
) {
  return (
    <>
      {schema.oneOf && (
        <AnyOneOf
          schema={schema}
          schemaType={schemaType}
          schemaPath={schemaPath}
        />
      )}
      {schema.anyOf && (
        <AnyOneOf
          schema={schema}
          schemaType={schemaType}
          schemaPath={schemaPath}
        />
      )}
      {schema.properties && (
        <Properties
          schema={schema}
          schemaType={schemaType}
          schemaPath={schemaPath}
        />
      )}
      {schema.additionalProperties && (
        <AdditionalProperties schema={schema} schemaType={schemaType} />
      )}
      {schema.items && (
        <Items
          schema={schema}
          schemaType={schemaType}
          schemaPath={schemaPath}
        />
      )}
    </>
  );
}

const SchemaNode: React.FC<SchemaProps> = ({
  schema,
  schemaType,
  schemaPath,
}) => {
  if (
    (schemaType === "request" && schema.readOnly) ||
    (schemaType === "response" && schema.writeOnly)
  ) {
    return null;
  }

  // Resolve discriminator recursively so nested oneOf/anyOf/allOf compositions
  // can still render discriminator tabs.
  let workingSchema = schema;
  const resolvedDiscriminator =
    schema.discriminator ?? findDiscriminator(schema);
  if (schema.allOf && !schema.discriminator && resolvedDiscriminator) {
    workingSchema = mergeAllOf(schema) as SchemaObject;
  }
  if (!workingSchema.discriminator && resolvedDiscriminator) {
    workingSchema.discriminator = resolvedDiscriminator;
  }

  if (workingSchema.discriminator) {
    const { discriminator } = workingSchema;
    return (
      <DiscriminatorNode
        discriminator={discriminator}
        schema={workingSchema}
        schemaType={schemaType}
      />
    );
  }

  // Handle allOf, oneOf, anyOf without discriminators
  if (schema.allOf) {
    // Check if allOf contains multiple oneOf/anyOf items that should be rendered separately
    const oneOfItems = schema.allOf.filter(
      (item: any) => item.oneOf || item.anyOf
    );
    const hasMultipleChoices = oneOfItems.length > 1;

    if (hasMultipleChoices) {
      // Render each oneOf/anyOf constraint first, then shared properties
      const mergedSchemas = mergeAllOf(schema) as SchemaObject;

      if (
        (schemaType === "request" && mergedSchemas.readOnly) ||
        (schemaType === "response" && mergedSchemas.writeOnly)
      ) {
        return null;
      }

      return (
        <div>
          {/* Render all oneOf/anyOf constraints first */}
          {schema.allOf.map((item: any, index: number) => {
            if (item.oneOf || item.anyOf) {
              const itemSchemaPath = schemaPath
                ? `${schemaPath}.allOf.${index}`
                : undefined;
              return (
                <div key={index}>
                  <AnyOneOf
                    schema={item}
                    schemaType={schemaType}
                    schemaPath={itemSchemaPath}
                  />
                </div>
              );
            }
            return null;
          })}
          {/* Then render shared properties from the merge */}
          {mergedSchemas.properties && (
            <Properties
              schema={mergedSchemas}
              schemaType={schemaType}
              schemaPath={schemaPath}
            />
          )}
          {mergedSchemas.items && (
            <Items
              schema={mergedSchemas}
              schemaType={schemaType}
              schemaPath={schemaPath}
            />
          )}
        </div>
      );
    }

    // For other allOf cases, use standard merge behavior
    const mergedSchemas = mergeAllOf(schema) as SchemaObject;

    if (
      (schemaType === "request" && mergedSchemas.readOnly) ||
      (schemaType === "response" && mergedSchemas.writeOnly)
    ) {
      return null;
    }

    return (
      <div>
        {mergedSchemas.oneOf && (
          <AnyOneOf
            schema={mergedSchemas}
            schemaType={schemaType}
            schemaPath={schemaPath}
          />
        )}
        {mergedSchemas.anyOf && (
          <AnyOneOf
            schema={mergedSchemas}
            schemaType={schemaType}
            schemaPath={schemaPath}
          />
        )}
        {mergedSchemas.properties && (
          <Properties
            schema={mergedSchemas}
            schemaType={schemaType}
            schemaPath={schemaPath}
          />
        )}
        {mergedSchemas.items && (
          <Items
            schema={mergedSchemas}
            schemaType={schemaType}
            schemaPath={schemaPath}
          />
        )}
      </div>
    );
  }

  // Handle primitives
  if (
    schema.type &&
    !schema.oneOf &&
    !schema.anyOf &&
    !schema.properties &&
    !schema.allOf &&
    !schema.items &&
    !schema.additionalProperties
  ) {
    const schemaName = getSchemaName(schema);
    return (
      <SchemaItem
        collapsible={false}
        name={schema.type}
        required={Boolean(schema.required)}
        schemaName={schemaName}
        schema={schema}
        discriminator={false}
        children={null}
      />
    );
  }

  return renderChildren(schema, schemaType, schemaPath);
};

export default SchemaNode;

type PrimitiveSchemaType =
  | Exclude<NonNullable<SchemaObject["type"]>, "object" | "array">
  | "null";

const PRIMITIVE_TYPES: Record<PrimitiveSchemaType, true> = {
  string: true,
  number: true,
  integer: true,
  boolean: true,
  null: true,
} as const;

const isPrimitive = (schema: SchemaObject) => {
  // Enum-only schemas (without explicit type) should be treated as primitives
  // This is valid JSON Schema where enum values define the constraints
  if (schema.enum && !schema.type) {
    return true;
  }
  return PRIMITIVE_TYPES[schema.type as PrimitiveSchemaType];
};
