/**
 * SchemaRegistry
 *
 * Builds and caches merged Zod schemas from workspace config schema_extensions.
 * Supports additive extensions to existing entity types and fully custom new types.
 */
import { z } from 'zod';
import { ENTITY_SCHEMAS, EntityBaseSchema } from './validation.js';
import { BUILTIN_BODY_TEMPLATES } from './entity-body-templates.js';
/**
 * Build a record of Zod field entries from an array of FieldDefinitions.
 */
function buildZodFields(fields) {
    const result = {};
    for (const field of fields) {
        let zodType;
        switch (field.type) {
            case 'string':
                zodType = z.string();
                break;
            case 'number':
                zodType = z.number();
                break;
            case 'boolean':
                zodType = z.boolean();
                break;
            case 'enum': {
                const values = field.enum_values;
                if (!values || values.length === 0) {
                    zodType = z.string(); // fallback if enum_values missing
                }
                else {
                    zodType = z.enum(values);
                }
                break;
            }
            case 'string[]':
                zodType = z.array(z.string());
                break;
            default:
                zodType = z.string();
        }
        if (field.description) {
            zodType = zodType.describe(field.description);
        }
        if (!field.required) {
            zodType = zodType.optional();
        }
        result[field.name] = zodType;
    }
    return result;
}
/** Unwrap optional/default/nullable wrappers to expose the inner type. */
function unwrapZod(schema) {
    let current = schema;
    while (current instanceof z.ZodOptional ||
        current instanceof z.ZodDefault ||
        (current && typeof current === 'object' && 'innerType' in (current._def ?? {}))) {
        const inner = current._def?.innerType ?? current.def?.innerType;
        if (!inner)
            break;
        current = inner;
    }
    return current;
}
/** Infer a simple type string from a Zod schema */
function inferZodType(schema) {
    const inner = unwrapZod(schema);
    if (inner instanceof z.ZodString) {
        const format = readStringFormat(inner);
        if (format)
            return format;
        return 'string';
    }
    if (inner instanceof z.ZodNumber)
        return 'number';
    if (inner instanceof z.ZodBoolean)
        return 'boolean';
    if (inner instanceof z.ZodEnum)
        return 'enum';
    if (inner instanceof z.ZodArray) {
        const element = (inner._def?.element ?? inner.def?.element);
        if (element instanceof z.ZodEnum)
            return 'enum[]';
        return inferZodType(element ?? z.string()) + '[]';
    }
    if (inner instanceof z.ZodLiteral)
        return 'string';
    return 'string';
}
/** Detect ZodString format checks (url, datetime, date, email). */
function readStringFormat(schema) {
    const checks = schema._def?.checks;
    if (!Array.isArray(checks))
        return null;
    for (const c of checks) {
        const fmt = c?.def?.format ?? c?.type;
        if (fmt === 'url' || fmt === 'email' || fmt === 'datetime' || fmt === 'date')
            return fmt;
    }
    return null;
}
/** Read `.meta()` (Zod v4 global registry) off a field, tolerating both the
 *  outer wrapper and unwrapped inner node. Used to surface the date
 *  `dateRole` + `label` that ride on the entity date vocabulary. */
function readMeta(schema) {
    const fn = schema?.meta;
    if (typeof fn === 'function') {
        const m = fn.call(schema);
        if (m && typeof m === 'object')
            return m;
    }
    return undefined;
}
/**
 * Whether a Zod field is system-managed (`.meta({ internal: true })`) and so
 * must NOT be surfaced as a user/agent-editable field or seeded with an empty
 * default. The single source of truth for "this field is set by the system,
 * not by people" — consulted by `describeZodObject` (which feeds the
 * `/api/schema/:type/fields` endpoint, the property panel, and the agent's
 * `get_entity_schema`) and by the graph create/changeType field seeders.
 */
export function isInternalField(schema) {
    return readMeta(schema)?.internal === true;
}
/** Read enum values off a ZodEnum (Zod v4 stores entries as { value: value }). */
function readEnumValues(schema) {
    const inner = unwrapZod(schema);
    if (!(inner instanceof z.ZodEnum))
        return null;
    if (Array.isArray(inner.options))
        return [...inner.options];
    const entries = (inner._def?.entries ?? inner.def?.entries);
    if (entries)
        return Object.values(entries);
    return null;
}
/**
 * Read the default value off a ZodDefault wrapper, if present. Used by
 * describeEntitySchema so the agent can see "stage defaults to qualification".
 */
function readDefaultValue(schema) {
    let current = schema;
    while (current instanceof z.ZodOptional) {
        current = current._def?.innerType;
    }
    if (current instanceof z.ZodDefault) {
        const def = current._def?.defaultValue ?? current.def?.defaultValue;
        return typeof def === 'function' ? def() : def;
    }
    return undefined;
}
/**
 * Walk a Zod object schema and emit per-field descriptors. Surfaces enums,
 * array element types, string formats (url/email/datetime), defaults, and
 * descriptions — the metadata the agent needs to write valid frontmatter
 * without memorizing schemas.
 */
export function describeZodObject(schema) {
    const out = {};
    if (!(schema instanceof z.ZodObject))
        return out;
    const shape = schema.shape;
    for (const [name, field] of Object.entries(shape)) {
        // System-managed fields (e.g. asset.indexable, note/dataset.source_file)
        // are never user/agent-editable, so they're omitted from the descriptor
        // map entirely — this is the single chokepoint that keeps them out of the
        // /api/schema/:type/fields endpoint, the property panel, and the agent's
        // get_entity_schema in one place.
        if (isInternalField(field))
            continue;
        const inner = unwrapZod(field);
        const descriptor = {
            type: inferZodType(field),
            optional: typeof field.isOptional === 'function' ? field.isOptional() : false,
            description: field.description ?? inner?.description ?? '',
        };
        if (inner instanceof z.ZodEnum) {
            const values = readEnumValues(inner);
            if (values)
                descriptor.enum = values;
        }
        else if (inner instanceof z.ZodArray) {
            const element = (inner._def?.element ?? inner.def?.element);
            if (element) {
                descriptor.itemType = inferZodType(element);
                const enumValues = readEnumValues(element);
                if (enumValues)
                    descriptor.itemEnum = enumValues;
            }
        }
        const defaultValue = readDefaultValue(field);
        if (defaultValue !== undefined)
            descriptor.default = defaultValue;
        const meta = readMeta(field);
        if (meta?.dateRole)
            descriptor.role = meta.dateRole;
        if (typeof meta?.label === 'string')
            descriptor.label = meta.label;
        if (typeof meta?.refType === 'string')
            descriptor.refType = meta.refType;
        out[name] = descriptor;
    }
    return out;
}
/**
 * Built-in reference fields that must resolve to a specific entity type,
 * keyed by entity type → field → required target type. Derived once from the
 * `refType` `.meta()` markers on the built-in schemas (today: `task.assignee`
 * → `person`). The write-path validator in entity-mutations.ts reads this to
 * reject a wikilink that resolves to the wrong type. Extension fields aren't
 * scanned here — only the built-in Zod schemas carry `refType` markers.
 */
export function builtinRefTypeFields(entityType) {
    const schema = ENTITY_SCHEMAS[entityType];
    if (!schema)
        return {};
    const out = {};
    for (const [name, d] of Object.entries(describeZodObject(schema))) {
        if (d.refType)
            out[name] = d.refType;
    }
    return out;
}
export class SchemaRegistry {
    extensions;
    schemaCache = new Map();
    fieldDefsCache = new Map();
    constructor(extensions) {
        this.extensions = extensions ?? {};
        this.build();
    }
    build() {
        const builtinTypes = new Set(Object.keys(ENTITY_SCHEMAS));
        for (const [typeKey, extension] of Object.entries(this.extensions)) {
            if (builtinTypes.has(typeKey)) {
                // Extend an existing built-in schema
                const base = ENTITY_SCHEMAS[typeKey];
                const additionalFields = extension.additional_fields ?? [];
                if (additionalFields.length > 0) {
                    const zodFields = buildZodFields(additionalFields);
                    const extended = base.extend(zodFields);
                    this.schemaCache.set(typeKey, extended);
                }
                this.fieldDefsCache.set(typeKey, additionalFields);
            }
            else {
                // Build a new custom type from EntityBaseSchema
                const customFields = extension.fields ?? [];
                const zodFields = buildZodFields(customFields);
                const schema = EntityBaseSchema.extend(zodFields).passthrough();
                this.schemaCache.set(typeKey, schema);
                this.fieldDefsCache.set(typeKey, customFields);
            }
        }
    }
    /**
     * Get the Zod schema for an entity type (built-in + extensions, or custom).
     * Falls back to the built-in schema, then EntityBaseSchema.
     */
    getSchema(entityType) {
        if (this.schemaCache.has(entityType)) {
            return this.schemaCache.get(entityType);
        }
        if (entityType in ENTITY_SCHEMAS) {
            return ENTITY_SCHEMAS[entityType];
        }
        return EntityBaseSchema;
    }
    /**
     * Get the declared field definitions for an entity type.
     * For extended built-in types: returns additional_fields only.
     * For custom types: returns all fields.
     * For types with no extension: returns [].
     */
    getFieldDefs(entityType) {
        return this.fieldDefsCache.get(entityType) ?? [];
    }
    /**
     * Get the body template for an entity type.
     * Workspace extension body_template overrides built-in defaults.
     * Returns null for types with no template (e.g., note, dataset).
     */
    getBodyTemplate(entityType) {
        // Workspace override takes priority
        const ext = this.extensions[entityType];
        if (ext?.body_template)
            return ext.body_template;
        // Fall back to built-in
        return BUILTIN_BODY_TEMPLATES[entityType] ?? null;
    }
    /**
     * Get field metadata for an entity type by introspecting the Zod schema.
     * Returns field name, inferred type, required status, and description.
     * Enum values, defaults, and array element metadata are surfaced when
     * present so the agent can write valid frontmatter without memorizing
     * schemas.
     */
    getFieldMetadata(entityType) {
        const schema = this.getSchema(entityType);
        const fields = [];
        if (schema instanceof z.ZodObject) {
            const descriptors = describeZodObject(schema);
            for (const [name, d] of Object.entries(descriptors)) {
                fields.push({
                    name,
                    type: d.type,
                    required: !d.optional,
                    description: d.description || undefined,
                    ...(d.enum ? { enum: d.enum } : {}),
                    ...(d.default !== undefined ? { default: d.default } : {}),
                    ...(d.itemType ? { itemType: d.itemType } : {}),
                    ...(d.itemEnum ? { itemEnum: d.itemEnum } : {}),
                    ...(d.role ? { role: d.role } : {}),
                    ...(d.label ? { label: d.label } : {}),
                    ...(d.refType ? { refType: d.refType } : {}),
                });
            }
        }
        // Add extension fields
        const extFields = this.getFieldDefs(entityType);
        for (const f of extFields) {
            if (!fields.some(existing => existing.name === f.name)) {
                fields.push({
                    name: f.name,
                    type: f.type,
                    required: f.required ?? false,
                    description: f.description,
                    ...(f.type === 'enum' && f.enum_values?.length ? { enum: [...f.enum_values] } : {}),
                });
            }
        }
        return fields;
    }
    /**
     * The custom display label for an entity type, if one is defined in the
     * workspace's schema extensions (e.g. a custom type "client" labelled
     * "Client Account"). Returns undefined for built-in types and unlabelled
     * extensions — callers fall back to title-casing the type key.
     */
    getCustomTypeLabel(type) {
        return this.extensions[type]?.label;
    }
    /**
     * Get all entity type names: built-in types + custom extension types.
     */
    getAllTypes() {
        const builtin = Object.keys(ENTITY_SCHEMAS);
        const customTypes = Object.keys(this.extensions).filter(k => !(k in ENTITY_SCHEMAS));
        return [...new Set([...builtin, ...customTypes])];
    }
    /**
     * Return a formatted prompt section describing the workspace's schema extensions.
     * Returns "" when no extensions are defined.
     */
    toPromptSection() {
        if (Object.keys(this.extensions).length === 0) {
            return '';
        }
        const builtinTypes = new Set(Object.keys(ENTITY_SCHEMAS));
        const lines = ['## Workspace Schema Extensions', ''];
        for (const [typeKey, extension] of Object.entries(this.extensions)) {
            const isBuiltin = builtinTypes.has(typeKey);
            const fields = isBuiltin
                ? (extension.additional_fields ?? [])
                : (extension.fields ?? []);
            if (fields.length === 0)
                continue;
            if (isBuiltin) {
                lines.push(`**${typeKey}** — additional fields:`);
            }
            else {
                const desc = extension.description
                    ? ` (${extension.description})`
                    : '';
                const label = extension.label ?? typeKey;
                lines.push(`**${typeKey}** — custom entity type${desc}:`);
                if (extension.label && extension.label !== typeKey) {
                    lines[lines.length - 1] = `**${typeKey}** (${label}) — custom entity type${desc}:`;
                }
            }
            for (const field of fields) {
                const req = field.required ? 'required' : 'optional';
                const typeStr = field.type === 'enum' && field.enum_values?.length
                    ? `enum: ${field.enum_values.join('|')}`
                    : field.type;
                const descPart = field.description ? `: ${field.description}` : '';
                lines.push(`- \`${field.name}\` (${typeStr}, ${req})${descPart}`);
            }
            lines.push('');
        }
        // Remove trailing empty line
        while (lines.length > 0 && lines[lines.length - 1] === '') {
            lines.pop();
        }
        return lines.join('\n');
    }
}
//# sourceMappingURL=schema-registry.js.map