import { type ZodTypeAny, z } from "zod";
import { getObjectSchemaShape } from "../contracts/schema-shape.js";

export type SchemaIO = "input" | "output";

type ConvertedSchemaObject = Record<string, unknown>;

/**
 * Context passed to an OpenAPI schema converter.
 */
export type SchemaConverterContext = {
  /**
   * Whether the schema is documenting request input or response output.
   */
  io: SchemaIO;
  /**
   * Component name hint Beignet will use when registering the converted schema.
   */
  nameHint: string;
};

/**
 * Converts a validation schema into an OpenAPI-compatible JSON Schema object.
 */
export interface SchemaConverter {
  /**
   * Human-readable converter name used in diagnostics.
   */
  name: string;
  /**
   * Return true when this converter owns the schema value.
   */
  canConvert(schema: unknown): boolean;
  /**
   * Convert the schema into an OpenAPI-compatible JSON Schema object.
   */
  toJSONSchema(
    schema: unknown,
    context: SchemaConverterContext,
  ): ConvertedSchemaObject;
}

/**
 * Schema introspection adapter.
 *
 * Abstracts the details of reading metadata from a schema library (e.g. Zod)
 * so that the OpenAPI generator is not directly coupled to `_def` internals.
 *
 * A default Zod implementation is provided via `createZodIntrospector()`.
 * To support a different schema library, implement this interface.
 */
export interface SchemaIntrospector {
  /**
   * Extract the shape (field name → field schema) from an object schema.
   * Returns undefined if the schema is not an object type or cannot be inspected.
   */
  getShape(schema: unknown): Record<string, unknown> | undefined;

  /**
   * Extract the user-supplied `.describe()` string from a schema.
   */
  getDescription(schema: unknown): string | undefined;

  /**
   * Return true if the schema represents an optional wrapper.
   */
  isOptional(schema: unknown): boolean;

  /**
   * If the schema is an optional wrapper, return the inner (unwrapped) schema.
   * Otherwise return the original schema unchanged.
   */
  unwrapOptional(schema: unknown): unknown;
}

/**
 * Create the default schema converter for Zod schemas.
 */
export function createZodSchemaConverter(): SchemaConverter {
  return {
    name: "zod",
    canConvert(schema: unknown): boolean {
      return schema instanceof z.ZodType;
    },
    toJSONSchema(
      schema: unknown,
      context: SchemaConverterContext,
    ): ConvertedSchemaObject {
      const jsonSchema = z.toJSONSchema(schema as ZodTypeAny, {
        target: "draft-2020-12",
        unrepresentable: "any",
        io: context.io,
      }) as ConvertedSchemaObject;

      delete jsonSchema.$schema;
      return jsonSchema;
    },
  };
}

/**
 * Create a schema introspector for Zod schemas.
 *
 * This accesses Zod's internal `_def` property, which is a common pattern in
 * Zod ecosystem libraries but may break with major Zod updates. Each helper
 * gracefully returns a safe default if the structure is unexpected.
 */
export function createZodIntrospector(): SchemaIntrospector {
  return {
    getShape(schema: unknown): Record<string, unknown> | undefined {
      return getObjectSchemaShape(schema);
    },

    getDescription(schema: unknown): string | undefined {
      if (!schema || typeof schema !== "object") return undefined;

      const directDescription = (schema as { description?: unknown })
        .description;
      if (typeof directDescription === "string") {
        return directDescription;
      }

      const schemaDef = (schema as Record<string, unknown>)?._def;
      if (!schemaDef || typeof schemaDef !== "object") return undefined;

      const description = (schemaDef as Record<string, unknown>).description;
      return typeof description === "string" ? description : undefined;
    },

    isOptional(schema: unknown): boolean {
      if (!schema || typeof schema !== "object") return false;

      const schemaWithOptional = schema as {
        isOptional?: () => boolean;
      };
      if (typeof schemaWithOptional.isOptional === "function") {
        return schemaWithOptional.isOptional();
      }

      const schemaDef = (schema as Record<string, unknown>)?._def;
      if (!schemaDef || typeof schemaDef !== "object") return false;

      const def = schemaDef as Record<string, unknown>;
      return def.typeName === "ZodOptional" || def.type === "optional";
    },

    unwrapOptional(schema: unknown): unknown {
      const schemaDef = (schema as Record<string, unknown>)?._def;
      const def = schemaDef as Record<string, unknown> | undefined;
      if (def?.typeName !== "ZodOptional" && def?.type !== "optional") {
        return schema;
      }

      const innerType = def?.innerType;
      return innerType ?? schema;
    },
  };
}
