import { OpenAIError } from '../error';
import {
  AutoParseableResponseFormat,
  AutoParseableTextFormat,
  AutoParseableTool,
  makeParseableResponseFormat,
  makeParseableTextFormat,
  makeParseableTool,
} from '../lib/parser';
import { AutoParseableResponseTool, makeParseableResponseTool } from '../lib/ResponsesParser';
import { type JSONSchema } from '../lib/jsonschema';
import {
  assertNoNestedSchemaIds,
  forEachJSONSchemaChild,
  hasOnlyRefAndAnnotations,
  normalizeObjectAllOfForExclusivity,
  resolveLocalRef,
  rewriteLocalRefsIntoMovedOneOfBranches,
  toStrictJsonSchema,
} from '../lib/transform';
import { ResponseFormatJSONSchema } from '../resources/index';
import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses';

type StandardSchemaIssue = {
  readonly message: string;
  readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined;
};

type StandardSchemaResult<Output> =
  | {
      readonly value: Output;
      readonly issues?: undefined;
    }
  | {
      readonly issues: ReadonlyArray<StandardSchemaIssue>;
    };

type StandardJSONSchemaOptions = {
  readonly target: 'draft-07';
  readonly libraryOptions?: Record<string, unknown> | undefined;
};

type StandardSchemaLike<Input = unknown, Output = Input> = {
  readonly '~standard': {
    readonly version: 1;
    readonly vendor: string;
    readonly types?:
      | {
          readonly input: Input;
          readonly output: Output;
        }
      | undefined;
    readonly validate: (
      value: unknown,
    ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
    readonly jsonSchema?:
      | {
          readonly input: (options: StandardJSONSchemaOptions) => Record<string, unknown>;
          readonly output?: (options: StandardJSONSchemaOptions) => Record<string, unknown>;
        }
      | undefined;
  };
};

type InferStandardOutput<Schema extends StandardSchemaLike> =
  [NonNullable<Schema['~standard']['types']>] extends [never] ? unknown
  : NonNullable<Schema['~standard']['types']> extends { readonly output: infer Output } ? Output
  : unknown;

type StandardSchemaJSONSchemaProps = {
  /**
   * A JSON Schema override for Standard Schema implementations that do not
   * expose `~standard.jsonSchema.input()`.
   */
  schema?: JSONSchema | Record<string, unknown> | undefined;
};

type StandardResponseFormatProps = Omit<ResponseFormatJSONSchema.JSONSchema, 'schema' | 'strict' | 'name'> &
  StandardSchemaJSONSchemaProps;

type StandardTextFormatProps = Omit<
  ResponseFormatTextJSONSchemaConfig,
  'schema' | 'type' | 'strict' | 'name'
> &
  StandardSchemaJSONSchemaProps;

type StandardToolFunction<Parameters extends StandardSchemaLike> = (
  args: InferStandardOutput<Parameters>,
) => unknown | Promise<unknown>;

type StandardToolOptions<Parameters extends StandardSchemaLike> = {
  name: string;
  parameters: Parameters;
  /**
   * A JSON Schema override for Standard Schema implementations that do not
   * expose `~standard.jsonSchema.input()`.
   */
  schema?: JSONSchema | Record<string, unknown> | undefined;
  function?: StandardToolFunction<Parameters> | undefined;
  description?: string | undefined;
};

type StandardToolReturnOptions<
  Parameters extends StandardSchemaLike,
  Function extends StandardToolFunction<Parameters> | undefined,
> = {
  arguments: InferStandardOutput<Parameters>;
  name: string;
  function: Function;
};

function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
  return typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function';
}

function formatStandardSchemaIssues(issues: ReadonlyArray<StandardSchemaIssue>): string {
  return issues
    .map((issue) => {
      const path = issue.path
        ?.map((segment) =>
          typeof segment === 'object' && segment !== null && 'key' in segment ? segment.key : segment,
        )
        .map(String)
        .join('.');
      return path ? `${path}: ${issue.message}` : issue.message;
    })
    .join('; ');
}

const JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']);

type JSONPrimitive = string | number | boolean | null;

function getSchemaTypes(schema: unknown): Set<string> | undefined {
  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return undefined;

  const type = (schema as Record<string, unknown>)['type'];
  if (type === undefined) {
    return getLiteralSchemaTypes(schema);
  }
  const types = Array.isArray(type) ? type : [type];
  if (
    types.length === 0 ||
    !types.every((value) => typeof value === 'string' && JSON_SCHEMA_TYPES.has(value))
  ) {
    return undefined;
  }

  return new Set(types);
}

function isJSONPrimitive(value: unknown): value is JSONPrimitive {
  return (
    value === null ||
    typeof value === 'string' ||
    typeof value === 'boolean' ||
    (typeof value === 'number' && Number.isFinite(value))
  );
}

function getLiteralValues(schema: unknown): JSONPrimitive[] | undefined {
  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return undefined;

  const record = schema as Record<string, unknown>;
  if ('const' in record && isJSONPrimitive(record['const'])) {
    return [record['const']];
  }

  const enumValues = record['enum'];
  if (Array.isArray(enumValues) && enumValues.length > 0 && enumValues.every(isJSONPrimitive)) {
    return enumValues;
  }

  return undefined;
}

function getLiteralSchemaTypes(schema: unknown): Set<string> | undefined {
  const literalValues = getLiteralValues(schema);
  if (!literalValues) return undefined;

  return new Set(
    literalValues.map((value) => {
      if (value === null) return 'null';
      return typeof value;
    }),
  );
}

function haveDisjointLiteralValues(left: unknown, right: unknown): boolean {
  const leftValues = getLiteralValues(left);
  const rightValues = getLiteralValues(right);
  if (!leftValues || !rightValues) return false;

  return leftValues.every((leftValue) => !rightValues.some((rightValue) => leftValue === rightValue));
}

function schemaTypesOverlap(left: string, right: string): boolean {
  return (
    left === right || (left === 'integer' && right === 'number') || (left === 'number' && right === 'integer')
  );
}

function isObjectOnlySchema(schema: unknown): boolean {
  const types = getSchemaTypes(schema);
  return types?.size === 1 && types.has('object');
}

function haveDisjointObjectDiscriminator(left: unknown, right: unknown, root: JSONSchema): boolean {
  if (!isObjectOnlySchema(left) || !isObjectOnlySchema(right)) return false;

  const leftRecord = left as Record<string, unknown>;
  const rightRecord = right as Record<string, unknown>;
  const leftProperties = leftRecord['properties'];
  const rightProperties = rightRecord['properties'];
  const leftRequired = leftRecord['required'];
  const rightRequired = rightRecord['required'];
  if (
    !leftProperties ||
    typeof leftProperties !== 'object' ||
    Array.isArray(leftProperties) ||
    !rightProperties ||
    typeof rightProperties !== 'object' ||
    Array.isArray(rightProperties) ||
    !Array.isArray(leftRequired) ||
    !Array.isArray(rightRequired)
  ) {
    return false;
  }

  for (const property of leftRequired) {
    if (
      typeof property === 'string' &&
      rightRequired.includes(property) &&
      haveDisjointLiteralValues(
        resolveLocalRefForExclusivity((leftProperties as Record<string, unknown>)[property], root),
        resolveLocalRefForExclusivity((rightProperties as Record<string, unknown>)[property], root),
      )
    ) {
      return true;
    }
  }

  return false;
}

function getClosedObjectPropertySet(
  schema: unknown,
): { properties: Set<string>; required: string[] } | undefined {
  if (!isObjectOnlySchema(schema)) return undefined;

  const record = schema as Record<string, unknown>;
  const properties = record['properties'];
  const required = record['required'];
  if (
    record['additionalProperties'] !== false ||
    !properties ||
    typeof properties !== 'object' ||
    Array.isArray(properties) ||
    !Array.isArray(required) ||
    required.some((property) => typeof property !== 'string')
  ) {
    return undefined;
  }

  const propertySet = new Set(Object.keys(properties));
  const requiredProperties = required as string[];
  // A required undeclared property makes a closed branch unsatisfiable, but
  // the strictifier rejects that shape rather than representing it. Keep this
  // exclusivity proof conservative and let the normal validation path fail.
  if (requiredProperties.some((property) => !propertySet.has(property))) {
    return undefined;
  }

  return { properties: propertySet, required: requiredProperties };
}

function haveDisjointClosedObjectPropertySets(left: unknown, right: unknown): boolean {
  const leftShape = getClosedObjectPropertySet(left);
  const rightShape = getClosedObjectPropertySet(right);
  if (!leftShape || !rightShape) return false;

  // If either closed branch requires a property the other branch does not
  // declare, every instance satisfying the first is rejected by the second as
  // an additional property. This proves oneOf exclusivity without widening
  // overlapping closed shapes.
  return (
    leftShape.required.some((property) => !rightShape.properties.has(property)) ||
    rightShape.required.some((property) => !leftShape.properties.has(property))
  );
}

function areMutuallyExclusive(left: unknown, right: unknown, root: JSONSchema): boolean {
  const leftTypes = getSchemaTypes(left);
  const rightTypes = getSchemaTypes(right);
  if (
    leftTypes &&
    rightTypes &&
    [...leftTypes].every((leftType) =>
      [...rightTypes].every((rightType) => !schemaTypesOverlap(leftType, rightType)),
    )
  ) {
    return true;
  }

  return (
    haveDisjointLiteralValues(left, right) ||
    haveDisjointObjectDiscriminator(left, right, root) ||
    haveDisjointClosedObjectPropertySets(left, right)
  );
}

function resolveLocalRefForExclusivity(
  schema: unknown,
  root: JSONSchema,
  seenRefs: Set<string> = new Set(),
): unknown | undefined {
  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema;

  const record = schema as Record<string, unknown>;
  const ref = record['$ref'];
  if (ref !== undefined) {
    // Annotation keywords do not affect Draft 7 validation, so they are safe
    // to retain while proving the referenced branches are mutually exclusive.
    // Keep the proof conservative for every other sibling constraint.
    if (typeof ref !== 'string' || !hasOnlyRefAndAnnotations(record as JSONSchema)) {
      return undefined;
    }
    if (seenRefs.has(ref)) return undefined;

    const resolved = resolveLocalRef(root, ref);
    if (resolved === undefined) return undefined;

    return resolveLocalRefForExclusivity(resolved, root, new Set([...seenRefs, ref]));
  }

  if (record['allOf'] !== undefined) {
    if (!Array.isArray(record['allOf'])) return undefined;
    const normalized = normalizeObjectAllOfForExclusivity(record as JSONSchema, root);
    if (normalized === undefined) return undefined;

    // Flattening a singleton allOf can expose a bare local ref. Feed that
    // result through this same resolver so URI-fragment decoding and the
    // existing local-ref cycle guard still apply before exclusivity analysis.
    return resolveLocalRefForExclusivity(normalized, root, seenRefs);
  }

  return schema;
}

function areOneOfBranchesMutuallyExclusive(branches: unknown[], root: JSONSchema): boolean {
  for (let index = 0; index < branches.length; index++) {
    for (let otherIndex = index + 1; otherIndex < branches.length; otherIndex++) {
      const left = resolveLocalRefForExclusivity(branches[index], root);
      const right = resolveLocalRefForExclusivity(branches[otherIndex], root);
      if (left === undefined || right === undefined || !areMutuallyExclusive(left, right, root)) {
        return false;
      }
    }
  }

  return true;
}

function normalizeStructuredOutputSchema(schema: JSONSchema): JSONSchema {
  assertNoNestedSchemaIds(schema);
  const normalizedSchema = structuredClone(schema);
  const oneOfSchemas: Record<string, unknown>[] = [];
  const visitedSchemas = new Set<Record<string, unknown>>();

  const visitSchema = (value: unknown): void => {
    if (!value || typeof value !== 'object' || Array.isArray(value)) return;
    const record = value as Record<string, unknown>;
    if (visitedSchemas.has(record)) return;
    visitedSchemas.add(record);

    if (record['oneOf'] !== undefined) {
      if (!Array.isArray(record['oneOf'])) {
        throw new OpenAIError(
          'Standard JSON Schema generated an invalid `oneOf`, which cannot be represented in an OpenAI strict schema',
        );
      }
      if (record['anyOf'] !== undefined) {
        throw new OpenAIError(
          'Standard JSON Schema generated both `anyOf` and `oneOf`, which cannot be represented in an OpenAI strict schema',
        );
      }
      // `false` can never validate, so it cannot overlap another oneOf
      // branch. Keep it in place until the existing anyOf normalization runs
      // so local refs into surviving branch indices can be rewritten before
      // the impossible alternatives are removed.
      const possibleBranches = record['oneOf'].filter((branch) => branch !== false);
      if (!areOneOfBranchesMutuallyExclusive(possibleBranches, normalizedSchema)) {
        throw new OpenAIError(
          'Standard JSON Schema generated a `oneOf` whose branches are not provably mutually exclusive. OpenAI strict schemas do not support `oneOf`; use `anyOf` or add a discriminator with distinct literal values.',
        );
      }
      oneOfSchemas.push(record);
    }

    forEachJSONSchemaChild(record, [], (child) => visitSchema(child));
  };

  visitSchema(normalizedSchema);
  rewriteLocalRefsIntoMovedOneOfBranches(normalizedSchema);
  for (const record of oneOfSchemas) {
    record['anyOf'] = record['oneOf'];
    delete record['oneOf'];
  }
  return normalizedSchema;
}

function parseStandardSchema<Schema extends StandardSchemaLike>(
  standardSchema: Schema,
  content: string,
): InferStandardOutput<Schema> {
  const result = standardSchema['~standard'].validate(JSON.parse(content));

  if (isPromiseLike(result)) {
    void Promise.resolve(result).catch(() => undefined);
    throw new OpenAIError(
      'Standard Schema helpers only support synchronous validation. Use a schema with a synchronous `~standard.validate()` implementation.',
    );
  }

  if (result.issues) {
    throw new OpenAIError(`Standard Schema validation failed: ${formatStandardSchemaIssues(result.issues)}`);
  }

  return result.value as InferStandardOutput<Schema>;
}

function resolveStandardJSONSchema(
  standardSchema: StandardSchemaLike,
  schemaOverride?: JSONSchema | Record<string, unknown> | undefined,
): Record<string, unknown> {
  const schema = (schemaOverride ?? standardSchema['~standard'].jsonSchema?.input({ target: 'draft-07' })) as
    | JSONSchema
    | undefined;

  if (!schema) {
    throw new OpenAIError(
      'Standard Schema helpers require a JSON Schema. Pass `schema` or use a schema that implements `~standard.jsonSchema.input()`.',
    );
  }

  return toStrictJsonSchema(normalizeStructuredOutputSchema(schema)) as unknown as Record<string, unknown>;
}

/**
 * Creates a chat completion `JSONSchema` response format from a Standard
 * Schema validator.
 *
 * The helper uses `~standard.jsonSchema.input()` for the model-facing schema
 * and `~standard.validate()` for parsed output. Validation must be
 * synchronous because the SDK's parse helpers are synchronous.
 */
export function standardResponseFormat<Schema extends StandardSchemaLike>(
  standardSchema: Schema,
  name: string,
  props?: StandardResponseFormatProps,
): AutoParseableResponseFormat<InferStandardOutput<Schema>> {
  const { schema, ...formatProps } = props ?? {};

  return makeParseableResponseFormat<InferStandardOutput<Schema>>(
    {
      type: 'json_schema',
      json_schema: {
        ...formatProps,
        name,
        strict: true,
        schema: resolveStandardJSONSchema(standardSchema, schema),
      },
    },
    (content) => parseStandardSchema(standardSchema, content),
  );
}

/**
 * Creates a Responses API `json_schema` text format from a Standard Schema
 * validator.
 */
export function standardTextFormat<Schema extends StandardSchemaLike>(
  standardSchema: Schema,
  name: string,
  props?: StandardTextFormatProps,
): AutoParseableTextFormat<InferStandardOutput<Schema>> {
  const { schema, ...formatProps } = props ?? {};

  return makeParseableTextFormat<InferStandardOutput<Schema>>(
    {
      type: 'json_schema',
      ...formatProps,
      name,
      strict: true,
      schema: resolveStandardJSONSchema(standardSchema, schema),
    },
    (content) => parseStandardSchema(standardSchema, content),
  );
}

/**
 * Creates a chat completion `function` tool from a Standard Schema
 * validator.
 */
export function standardFunction<
  Parameters extends StandardSchemaLike,
  Function extends StandardToolFunction<Parameters>,
>(
  options: StandardToolOptions<Parameters> & { function: Function },
): AutoParseableTool<StandardToolReturnOptions<Parameters, Function>>;
export function standardFunction<Parameters extends StandardSchemaLike>(
  options: StandardToolOptions<Parameters> & { function?: undefined },
): AutoParseableTool<StandardToolReturnOptions<Parameters, undefined>>;
export function standardFunction<Parameters extends StandardSchemaLike>(
  options: StandardToolOptions<Parameters>,
): AutoParseableTool<StandardToolReturnOptions<Parameters, StandardToolFunction<Parameters> | undefined>>;
export function standardFunction<Parameters extends StandardSchemaLike>(
  options: StandardToolOptions<Parameters>,
) {
  return makeParseableTool<any>(
    {
      type: 'function',
      function: {
        name: options.name,
        parameters: resolveStandardJSONSchema(options.parameters, options.schema),
        strict: true,
        ...(options.description ? { description: options.description } : undefined),
      },
    },
    {
      callback: options.function,
      parser: (args) => parseStandardSchema(options.parameters, args),
    },
  );
}

/**
 * Creates a Responses API `function` tool from a Standard Schema validator.
 */
export function standardResponsesFunction<
  Parameters extends StandardSchemaLike,
  Function extends StandardToolFunction<Parameters>,
>(
  options: StandardToolOptions<Parameters> & { function: Function },
): AutoParseableResponseTool<StandardToolReturnOptions<Parameters, Function>>;
export function standardResponsesFunction<Parameters extends StandardSchemaLike>(
  options: StandardToolOptions<Parameters> & { function?: undefined },
): AutoParseableResponseTool<StandardToolReturnOptions<Parameters, undefined>>;
export function standardResponsesFunction<Parameters extends StandardSchemaLike>(
  options: StandardToolOptions<Parameters>,
): AutoParseableResponseTool<
  StandardToolReturnOptions<Parameters, StandardToolFunction<Parameters> | undefined>
>;
export function standardResponsesFunction<Parameters extends StandardSchemaLike>(
  options: StandardToolOptions<Parameters>,
) {
  return makeParseableResponseTool<any>(
    {
      type: 'function',
      name: options.name,
      parameters: resolveStandardJSONSchema(options.parameters, options.schema),
      strict: true,
      ...(options.description ? { description: options.description } : undefined),
    },
    {
      callback: options.function,
      parser: (args) => parseStandardSchema(options.parameters, args),
    },
  );
}
