import { Json } from "./json.js";
import { createJsonSchemaDocument } from "./json-schema-document.js";
import { OneOf } from "./oneof.js";
import { Record as RecordBuilder } from "./record.js";
import { Union } from "./union.js";
import { validate } from "./validate.js";
function withMetadata(schema, jsonSchema) {
    if (schema.description !== undefined) {
        jsonSchema.description = schema.description;
    }
    if (schema.default !== undefined) {
        jsonSchema.default = schema.default;
    }
    if (schema.nullable === true) {
        jsonSchema.nullable = true;
    }
    return jsonSchema;
}
function withStringMetadata(schema, jsonSchema) {
    if (schema.minLength !== undefined) {
        jsonSchema.minLength = schema.minLength;
    }
    if (schema.maxLength !== undefined) {
        jsonSchema.maxLength = schema.maxLength;
    }
    if (schema.pattern !== undefined) {
        jsonSchema.pattern = schema.pattern;
    }
    if (schema.format !== undefined) {
        jsonSchema.format = schema.format;
    }
    return withMetadata(schema, jsonSchema);
}
function withNumberMetadata(schema, jsonSchema) {
    if (schema.minimum !== undefined) {
        jsonSchema.minimum = schema.minimum;
    }
    if (schema.maximum !== undefined) {
        jsonSchema.maximum = schema.maximum;
    }
    return withMetadata(schema, jsonSchema);
}
function withArrayMetadata(schema, jsonSchema) {
    if (schema.minItems !== undefined) {
        jsonSchema.minItems = schema.minItems;
    }
    if (schema.maxItems !== undefined) {
        jsonSchema.maxItems = schema.maxItems;
    }
    return withMetadata(schema, jsonSchema);
}
function withObjectMetadata(schema, jsonSchema) {
    jsonSchema.additionalProperties = schema.additionalProperties ?? false;
    return withMetadata(schema, jsonSchema);
}
function getEnumJsonType(values) {
    const [firstValue] = values;
    if (firstValue === undefined) {
        return undefined;
    }
    const firstType = typeof firstValue;
    const isSinglePrimitiveType = values.every((value) => typeof value === firstType);
    if (!isSinglePrimitiveType) {
        return undefined;
    }
    if (firstType === "string" || firstType === "number" || firstType === "boolean") {
        return firstType;
    }
    return undefined;
}
function isOptionalSchema(schema) {
    return schema.kind === "optional";
}
function assertValidEnumValues(values) {
    if (values.length === 0) {
        throw new Error("Enum schema requires at least one value");
    }
    const uniqueValues = new Set(values);
    if (uniqueValues.size !== values.length) {
        throw new Error("Enum schema values must be unique");
    }
    if (values.some((value) => typeof value === "number" && !Number.isFinite(value))) {
        throw new Error("Enum schema numeric values must be finite");
    }
}
function assertNonNegativeInteger(value, name) {
    if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
        throw new Error(`${name} must be a non-negative integer`);
    }
}
function assertFiniteNumber(value, name) {
    if (value !== undefined && !Number.isFinite(value)) {
        throw new Error(`${name} must be finite`);
    }
}
function assertMinMaxOrder(minimum, maximum, minimumName, maximumName) {
    if (minimum !== undefined && maximum !== undefined && minimum > maximum) {
        throw new Error(`${minimumName} must be less than or equal to ${maximumName}`);
    }
}
function assertValidDefault(schema) {
    if (schema.default === undefined) {
        return;
    }
    const result = validate(schema, schema.default);
    if (!result.ok) {
        throw new Error(`default must satisfy schema: ${result.issues[0]?.message ?? "invalid default"}`);
    }
}
function assertPattern(pattern) {
    if (pattern === undefined) {
        return;
    }
    try {
        new RegExp(pattern);
    }
    catch {
        throw new Error("pattern must be a valid regular expression");
    }
}
function unwrapOptional(schema) {
    if (isOptionalSchema(schema)) {
        return unwrapOptional(schema.inner);
    }
    return schema;
}
function withInjectedDiscriminator(schema, discriminator, branchName) {
    const branchJsonSchema = toJsonSchema(schema);
    const properties = {
        ...(branchJsonSchema.properties ?? {}),
        [discriminator]: {
            type: "string",
            enum: [branchName]
        }
    };
    const required = [...new Set([...(branchJsonSchema.required ?? []), discriminator])];
    return {
        ...branchJsonSchema,
        type: "object",
        properties,
        required
    };
}
export const S = {
    String(options = {}) {
        assertNonNegativeInteger(options.minLength, "minLength");
        assertNonNegativeInteger(options.maxLength, "maxLength");
        assertMinMaxOrder(options.minLength, options.maxLength, "minLength", "maxLength");
        assertPattern(options.pattern);
        const schema = {
            kind: "string",
            ...options
        };
        assertValidDefault(schema);
        return schema;
    },
    Number(options = {}) {
        assertFiniteNumber(options.minimum, "minimum");
        assertFiniteNumber(options.maximum, "maximum");
        assertMinMaxOrder(options.minimum, options.maximum, "minimum", "maximum");
        assertFiniteNumber(options.default, "default");
        if (options.jsonType === "integer" &&
            options.default !== undefined &&
            !Number.isInteger(options.default)) {
            throw new Error("default must be an integer");
        }
        const schema = {
            kind: "number",
            ...options
        };
        assertValidDefault(schema);
        return schema;
    },
    Boolean(options = {}) {
        const schema = {
            kind: "boolean",
            ...options
        };
        assertValidDefault(schema);
        return schema;
    },
    Enum(values, options = {}) {
        assertValidEnumValues(values);
        if (options.jsonType === "integer" &&
            values.some((value) => typeof value !== "number" || !Number.isInteger(value))) {
            throw new Error("Integer enum values must be integers");
        }
        const schema = {
            kind: "enum",
            values,
            ...options
        };
        assertValidDefault(schema);
        return schema;
    },
    Array(item, options = {}) {
        assertNonNegativeInteger(options.minItems, "minItems");
        assertNonNegativeInteger(options.maxItems, "maxItems");
        assertMinMaxOrder(options.minItems, options.maxItems, "minItems", "maxItems");
        const schema = {
            kind: "array",
            item,
            ...options
        };
        assertValidDefault(schema);
        return schema;
    },
    Object(shape, options = {}) {
        const schema = {
            kind: "object",
            shape,
            ...options
        };
        assertValidDefault(schema);
        return schema;
    },
    Optional(inner) {
        return {
            kind: "optional",
            inner
        };
    },
    OneOf,
    Union,
    Record: RecordBuilder,
    Json
};
export function toJsonSchema(schema) {
    const unwrappedSchema = unwrapOptional(schema);
    switch (unwrappedSchema.kind) {
        case "string":
            return withStringMetadata(unwrappedSchema, { type: "string" });
        case "number":
            return withNumberMetadata(unwrappedSchema, { type: unwrappedSchema.jsonType ?? "number" });
        case "boolean":
            return withMetadata(unwrappedSchema, { type: "boolean" });
        case "enum": {
            const jsonSchema = {
                enum: unwrappedSchema.nullable === true
                    ? [...unwrappedSchema.values, null]
                    : [...unwrappedSchema.values]
            };
            const enumType = unwrappedSchema.jsonType ?? getEnumJsonType(unwrappedSchema.values);
            if (enumType !== undefined) {
                jsonSchema.type = enumType;
            }
            return withMetadata(unwrappedSchema, jsonSchema);
        }
        case "array":
            return withArrayMetadata(unwrappedSchema, {
                type: "array",
                items: toJsonSchema(unwrappedSchema.item)
            });
        case "object": {
            const properties = {};
            const required = [];
            for (const [key, propertySchema] of Object.entries(unwrappedSchema.shape)) {
                Object.defineProperty(properties, key, {
                    enumerable: true,
                    configurable: true,
                    writable: true,
                    value: toJsonSchema(propertySchema)
                });
                if (!isOptionalSchema(propertySchema)) {
                    required.push(key);
                }
            }
            return withObjectMetadata(unwrappedSchema, {
                type: "object",
                properties,
                required
            });
        }
        case "oneOf":
            return withMetadata(unwrappedSchema, {
                oneOf: Object.entries(unwrappedSchema.branches).map(([branchName, branchSchema]) => withInjectedDiscriminator(branchSchema, unwrappedSchema.discriminator, branchName))
            });
        case "union":
            return withMetadata(unwrappedSchema, {
                oneOf: unwrappedSchema.branches.map((branchSchema) => toJsonSchema(branchSchema))
            });
        case "record":
            return withMetadata(unwrappedSchema, {
                type: "object",
                additionalProperties: toJsonSchema(unwrappedSchema.value)
            });
        case "json":
            return withMetadata(unwrappedSchema, {});
    }
}
export function toJsonSchemaDocument(schema, options = {}) {
    return createJsonSchemaDocument(toJsonSchema(schema), options);
}
export { Json, OneOf, RecordBuilder as Record, Union, validate };
