import type { z } from "zod";

export type JsonSchemaNode = Record<string, unknown>;

export type SchemaField = {
  path: string;
  schema: z.ZodType;
  jsonSchema: JsonSchemaNode;
  optional: boolean;
  nullable: boolean;
  hasUniquenessRefine: boolean;
  relationOwnership: boolean;
};

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function readDef(schema: z.ZodType): Record<string, unknown> {
  const candidate = (schema as { _zod?: { def?: unknown }; def?: unknown })._zod?.def
    ?? (schema as { def?: unknown }).def;
  if (!isRecord(candidate)) {
    throw new Error("schema definition is missing");
  }
  return candidate;
}

function hasUniquenessRefine(schema: z.ZodType): boolean {
  const def = readDef(schema);
  const checks = def.checks;
  if (!Array.isArray(checks)) {
    return false;
  }
  return checks.some((check) => {
    if (!isRecord(check)) {
      return false;
    }
    const checkDef = (check._zod as { def?: unknown } | undefined)?.def ?? check.def;
    if (!isRecord(checkDef) || checkDef.check !== "custom") {
      return false;
    }
    const error = checkDef.error as (() => string) | undefined;
    if (typeof error !== "function") {
      return false;
    }
    try {
      const message = error();
      return typeof message === "string" && /unique/i.test(message);
    } catch {
      return false;
    }
  });
}

function isRelationOwnershipField(path: string): boolean {
  const leaf = path.split(".").at(-1) ?? path;
  return /^(ownerId|resourceOwnerId|tenantId|actorTenantId|resourceTenantId)$/i.test(leaf);
}

export function toJsonSchema(schema: z.ZodType): JsonSchemaNode {
  const toJSONSchema = (schema as { toJSONSchema?: () => unknown }).toJSONSchema;
  if (typeof toJSONSchema !== "function") {
    throw new Error("schema must expose toJSONSchema()");
  }
  const jsonSchema = toJSONSchema.call(schema);
  if (!isRecord(jsonSchema)) {
    throw new Error("schema toJSONSchema() must return an object");
  }
  return jsonSchema;
}

function collectFields(
  schema: z.ZodType,
  path: string,
  optional: boolean,
  nullable: boolean,
  fields: SchemaField[],
): void {
  const def = readDef(schema);
  const type = def.type;

  if (type === "optional") {
    const inner = def.innerType;
    if (!inner || typeof inner !== "object") {
      throw new Error(`optional schema at ${path} is missing innerType`);
    }
    collectFields(inner as z.ZodType, path, true, nullable, fields);
    return;
  }

  if (type === "nullable") {
    const inner = def.innerType;
    if (!inner || typeof inner !== "object") {
      throw new Error(`nullable schema at ${path} is missing innerType`);
    }
    collectFields(inner as z.ZodType, path, optional, true, fields);
    return;
  }

  if (type === "default") {
    const inner = def.innerType;
    if (!inner || typeof inner !== "object") {
      throw new Error(`default schema at ${path} is missing innerType`);
    }
    collectFields(inner as z.ZodType, path, optional, nullable, fields);
    return;
  }

  if (type === "object") {
    const shape = def.shape;
    if (!isRecord(shape)) {
      throw new Error(`object schema at ${path} is missing shape`);
    }
    for (const [key, child] of Object.entries(shape)) {
      if (!child || typeof child !== "object") {
        throw new Error(`object field ${path}.${key} is not a schema`);
      }
      collectFields(child as z.ZodType, path === "$" ? key : `${path}.${key}`, false, false, fields);
    }
    return;
  }

  if (type === "array") {
    const element = def.element;
    if (!element || typeof element !== "object") {
      throw new Error(`array schema at ${path} is missing element`);
    }
    fields.push({
      path,
      schema,
      jsonSchema: toJsonSchema(schema),
      optional,
      nullable,
      hasUniquenessRefine: hasUniquenessRefine(schema),
      relationOwnership: false,
    });
    collectFields(element as z.ZodType, `${path}[]`, false, false, fields);
    return;
  }

  fields.push({
    path,
    schema,
    jsonSchema: toJsonSchema(schema),
    optional,
    nullable,
    hasUniquenessRefine: hasUniquenessRefine(schema),
    relationOwnership: isRelationOwnershipField(path),
  });
}

export function collectSchemaFields(schema: z.ZodType): SchemaField[] {
  const fields: SchemaField[] = [];
  collectFields(schema, "$", false, false, fields);
  return fields;
}

export function readNumberConstraint(
  jsonSchema: JsonSchemaNode,
  key: "minimum" | "maximum" | "multipleOf",
): number | undefined {
  const value = jsonSchema[key];
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}

export function readIntegerConstraint(
  jsonSchema: JsonSchemaNode,
  key: "minLength" | "maxLength" | "minItems" | "maxItems",
): number | undefined {
  const value = jsonSchema[key];
  return typeof value === "number" && Number.isInteger(value) ? value : undefined;
}

export function readEnumValues(jsonSchema: JsonSchemaNode): string[] | undefined {
  const values = jsonSchema.enum;
  if (!Array.isArray(values)) {
    return undefined;
  }
  return values.filter((value): value is string => typeof value === "string");
}

export function readSchemaType(jsonSchema: JsonSchemaNode): string | undefined {
  const value = jsonSchema.type;
  return typeof value === "string" ? value : undefined;
}

export function readSchemaFormat(jsonSchema: JsonSchemaNode): string | undefined {
  const value = jsonSchema.format;
  return typeof value === "string" ? value : undefined;
}

export function readSchemaPattern(jsonSchema: JsonSchemaNode): string | undefined {
  const value = jsonSchema.pattern;
  return typeof value === "string" ? value : undefined;
}

export function schemaCoercesInput(schema: z.ZodType): boolean {
  const def = readDef(schema);
  return def.coerce === true;
}
