/** Drizzle column type function → SpecVerse vocabulary type. */
const TYPE_MAP = {
    // pg-core
    text: 'String',
    varchar: 'String',
    char: 'String',
    uuid: 'UUID',
    serial: 'Integer',
    bigserial: 'Integer',
    smallserial: 'Integer',
    integer: 'Integer',
    smallint: 'Integer',
    bigint: 'Integer',
    boolean: 'Boolean',
    timestamp: 'DateTime',
    date: 'Date',
    time: 'String',
    real: 'Number',
    doublePrecision: 'Number',
    numeric: 'Number',
    decimal: 'Number',
    jsonb: 'Json',
    json: 'Json',
    // sqlite-core
    blob: 'Json',
    // mysql-core extras
    datetime: 'DateTime',
    float: 'Number',
    double: 'Number',
    int: 'Integer',
    tinyint: 'Integer',
};
const TABLE_FN_DIALECT = {
    pgTable: 'postgresql',
    mysqlTable: 'mysql',
    sqliteTable: 'sqlite',
};
/**
 * Run the Drizzle adapter against a structural pre-pass.
 *
 * Discovers schema files by reading any `drizzle.config.*` for its `schema:`
 * path(s), unioned with common Drizzle layout globs (`**\/schema.ts`,
 * `**\/schema\/**\/*.ts`, `**\/db\/schema*.ts`). Candidate files are then
 * content-filtered to those declaring a Drizzle table — so an unrelated
 * `schema.ts` (e.g. a Zod schema module) contributes nothing.
 */
export async function extractTypeScriptDrizzle(prepass) {
    const empty = { entities: [], relationships: [], enums: {}, schemaFiles: [], dialect: null };
    const candidates = new Set();
    for (const glob of ['**/schema.ts', '**/schema/**/*.ts', '**/db/schema*.ts', '**/*.schema.ts']) {
        try {
            for (const f of await prepass.listFiles({ glob }))
                candidates.add(f);
        }
        catch {
            // glob unsupported by this backend — ignore.
        }
    }
    // Read drizzle.config.* for an explicit schema path (it may point somewhere
    // none of the globs cover, e.g. ./src/lib/tables.ts).
    for (const cfgGlob of ['**/drizzle.config.ts', '**/drizzle.config.js', '**/drizzle.config.mjs']) {
        let cfgs = [];
        try {
            cfgs = await prepass.listFiles({ glob: cfgGlob });
        }
        catch {
            continue;
        }
        for (const cfg of cfgs) {
            const text = await prepass.fileSourceText(cfg).catch(() => '');
            for (const p of schemaPathsFromConfig(text)) {
                // Resolve the config-relative path to a basename-ish glob the backend
                // can match (path bases differ across the FS abstraction; matching by
                // the trailing path segment is robust and over-collects safely since
                // we content-filter below).
                const base = p.replace(/^[./]+/, '');
                try {
                    for (const f of await prepass.listFiles({ glob: `**/${base}` }))
                        candidates.add(f);
                }
                catch {
                    // ignore
                }
            }
        }
    }
    if (candidates.size === 0)
        return empty;
    const enums = {};
    /** pgEnum export-const var name (`orderStatus`) → enum name (`order_status`). */
    const enumVarToName = new Map();
    const entities = [];
    const relationships = [];
    const seen = new Set();
    /** export-const var name (e.g. `orgTrackedCompanies`) → entity name (`OrgTrackedCompany`). */
    const varToEntity = new Map();
    /** pending references to resolve after every table's var→entity is known. */
    const pendingRefs = [];
    const schemaFiles = [];
    const schemaTexts = new Map();
    let dialect = null;
    // First pass: collect every pgEnum across all schema files (an enum may be
    // referenced by a table in a different file), so enum-typed columns resolve
    // regardless of declaration order.
    for (const file of candidates) {
        const text = await prepass.fileSourceText(file).catch(() => '');
        if (!/\b(pgTable|sqliteTable|mysqlTable)\s*\(/.test(text))
            continue;
        schemaFiles.push(file);
        schemaTexts.set(file, text);
        // export const orderStatus = pgEnum("order_status", ["a", "b"])
        for (const m of text.matchAll(/(?:export\s+const\s+(\w+)\s*=\s*)?(?:pgEnum|mysqlEnum)\s*\(\s*["']([^"']+)["']\s*,\s*\[([^\]]*)\]/g)) {
            const varName = m[1];
            const name = m[2];
            const values = [...m[3].matchAll(/["']([^"']+)["']/g)].map((x) => x[1]);
            if (values.length) {
                enums[name] = values;
                if (varName)
                    enumVarToName.set(varName, name);
            }
        }
    }
    // Second pass: parse table declarations.
    for (const file of schemaFiles) {
        const text = schemaTexts.get(file);
        for (const tbl of findTableDeclarations(text)) {
            if (!dialect)
                dialect = TABLE_FN_DIALECT[tbl.tableFn] ?? null;
            const entityName = pascalSingular(tbl.varName);
            if (seen.has(entityName)) {
                console.warn(`[drizzle-adapter] duplicate table "${entityName}" in ${file} — first declaration wins`);
                continue;
            }
            seen.add(entityName);
            varToEntity.set(tbl.varName, entityName);
            const { fields, refs } = parseColumns(tbl.columnsBlock, enumVarToName);
            for (const r of refs) {
                pendingRefs.push({ fromModel: entityName, toVar: r.toVar, cascade: r.cascade, fieldName: r.fieldName });
            }
            // Lifecycle: a column whose type maps to an enum we extracted.
            let lifecycleStates;
            let lifecycleField;
            for (const f of fields) {
                if (enums[f.type]) {
                    lifecycleStates = enums[f.type];
                    lifecycleField = f.name;
                    break;
                }
            }
            entities.push({ name: entityName, filePath: file, fields, lifecycleStates, lifecycleField });
        }
    }
    // Resolve belongsTo references now that var→entity is fully populated.
    // Dedupe by model-pair: a table with two FKs to the SAME target (e.g.
    // saved_searches.user_id AND a deprecated notification_user_id, both →
    // users) must yield ONE belongsTo + ONE inverse hasMany, not two. The
    // skeleton emitter keys relationships by target name, so a second
    // same-target edge collides into a duplicate YAML key and invalidates the
    // spec. First FK wins (source order → keeps the primary, non-deprecated
    // column). Mirrors the prisma adapter's `declaredHasMany` dedupe.
    const seenBelongsTo = new Set();
    const seenHasMany = new Set();
    for (const ref of pendingRefs) {
        const toModel = varToEntity.get(ref.toVar) ?? pascalSingular(ref.toVar);
        const btKey = `${ref.fromModel}→${toModel}`;
        if (!seenBelongsTo.has(btKey)) {
            seenBelongsTo.add(btKey);
            relationships.push({
                fromModel: ref.fromModel,
                toModel,
                type: 'belongsTo',
                cascade: false, // cascade lives on the hasMany side (below)
                fieldName: ref.fieldName,
            });
        }
        // Inverse hasMany, carrying the cascade decision (SpecVerse models cascade
        // on the parent→child edge, mirroring the prisma adapter's third pass).
        const hmKey = `${toModel}→${ref.fromModel}`;
        if (!seenHasMany.has(hmKey)) {
            seenHasMany.add(hmKey);
            relationships.push({
                fromModel: toModel,
                toModel: ref.fromModel,
                type: 'hasMany',
                cascade: ref.cascade,
                fieldName: ref.fieldName,
            });
        }
    }
    return { entities, relationships, enums, schemaFiles, dialect };
}
// ────────────────────────────────────────────────────────────────────
// Drizzle-syntax parsers (regex + balanced-brace scan over raw .ts text)
// ────────────────────────────────────────────────────────────────────
/** Extract `schema:` path(s) from a drizzle.config source (string or array). */
function schemaPathsFromConfig(text) {
    const out = [];
    // Array form: schema: ["./a.ts", "./b/*.ts"]
    const arr = text.match(/schema\s*:\s*\[([^\]]+)\]/);
    if (arr) {
        for (const m of arr[1].matchAll(/["']([^"']+)["']/g))
            out.push(m[1]);
        return out;
    }
    // String form: schema: "./shared/schema.ts"
    const str = text.match(/schema\s*:\s*["']([^"']+)["']/);
    if (str)
        out.push(str[1]);
    return out;
}
/**
 * Find every `export const <var> = <pgTable|…>("name", { <columns> }, …)`
 * declaration and return the var name + the raw columns-object text.
 *
 * Uses a balanced scanner because columns nest braces/parens
 * (`.references(() => x, { onDelete })`, `.default([])`, the optional
 * `(table) => [...]` config arg) — a naive `{[^}]+}` would truncate.
 */
function findTableDeclarations(text) {
    const out = [];
    const re = /export\s+const\s+(\w+)\s*=\s*(pgTable|sqliteTable|mysqlTable)\s*\(/g;
    let m;
    while ((m = re.exec(text))) {
        const varName = m[1];
        const tableFn = m[2];
        // Position just after the opening paren of the table call.
        const openParen = m.index + m[0].length - 1;
        const callEnd = matchDelimiter(text, openParen, '(', ')');
        if (callEnd < 0)
            continue;
        const callBody = text.slice(openParen + 1, callEnd);
        // The columns object is the first `{ … }` in the call args (after the
        // table-name string literal).
        const braceStart = callBody.indexOf('{');
        if (braceStart < 0)
            continue;
        const absBraceStart = openParen + 1 + braceStart;
        const braceEnd = matchDelimiter(text, absBraceStart, '{', '}');
        if (braceEnd < 0)
            continue;
        out.push({ varName, tableFn, columnsBlock: text.slice(absBraceStart + 1, braceEnd) });
    }
    return out;
}
/**
 * Given the index of an opening delimiter in `text`, return the index of its
 * matching close, accounting for nesting and skipping string/template/comment
 * content. Returns -1 if unbalanced.
 */
function matchDelimiter(text, openIdx, open, close) {
    let depth = 0;
    for (let i = openIdx; i < text.length; i++) {
        const ch = text[i];
        // Skip string literals (', ", `) — Drizzle column names + defaults are strings.
        if (ch === '"' || ch === "'" || ch === '`') {
            i = skipString(text, i, ch);
            continue;
        }
        // Skip line comments.
        if (ch === '/' && text[i + 1] === '/') {
            const nl = text.indexOf('\n', i);
            if (nl < 0)
                return -1;
            i = nl;
            continue;
        }
        if (ch === open)
            depth++;
        else if (ch === close) {
            depth--;
            if (depth === 0)
                return i;
        }
    }
    return -1;
}
function skipString(text, startIdx, quote) {
    for (let i = startIdx + 1; i < text.length; i++) {
        if (text[i] === '\\') {
            i++;
            continue;
        }
        if (text[i] === quote)
            return i;
    }
    return text.length;
}
/**
 * Parse a columns-object block into typed fields + FK references.
 *
 * A Drizzle column is `<key>: <typeFn>("<col>"[, …])<.modifier(...)>*`.
 * Columns are overwhelmingly one-per-line; we process line-by-line (mirroring
 * the prisma adapter's `parseFields`) but join continuation lines so a column
 * whose modifiers wrap is still seen whole.
 */
function parseColumns(block, enumVarToName) {
    const fields = [];
    const refs = [];
    for (const stmt of splitTopLevel(block)) {
        const line = stmt.trim();
        if (!line || line.startsWith('//'))
            continue;
        const head = line.match(/^(\w+)\s*:\s*(\w+)\s*\(/);
        if (!head)
            continue; // skip the `(table) => [...]` config callback + non-column entries
        const [, name, typeFn] = head;
        // A column typed by a pgEnum const (`status: orderStatus("status")`) is a
        // string-valued column; record its type as the ENUM NAME so the caller's
        // lifecycle detector (`enums[f.type]`) matches.
        const sv = TYPE_MAP[typeFn] ?? (enumVarToName.has(typeFn) ? enumVarToName.get(typeFn) : undefined);
        if (!sv)
            continue; // not a recognised column type (e.g. a nested object) — skip
        const isId = /\.primaryKey\s*\(/.test(line);
        const required = isId || /\.notNull\s*\(/.test(line);
        const unique = /\.unique\s*\(/.test(line);
        const def = extractDrizzleDefault(line);
        // Id-generation semantics (Phase 2): serial/bigserial → autoincrement;
        // a uuid column with any default (defaultRandom / sql`gen_random_uuid()`)
        // → uuid4; .defaultNow() → now. Preserved so analyse → realize reproduces
        // the source identity instead of guessing uuid.
        let auto;
        if (/^(serial|bigserial|smallserial)$/.test(typeFn))
            auto = 'autoincrement';
        else if (typeFn === 'uuid' && /\.default/.test(line))
            auto = 'uuid4';
        else if (/\.defaultNow\s*\(/.test(line))
            auto = 'now';
        fields.push({ name, type: sv, required, unique, default: def, auto, isId });
        // belongsTo: .references(() => <var>.<col>, { onDelete: "cascade" })
        const ref = line.match(/\.references\s*\(\s*\(\s*\)\s*=>\s*(\w+)\./);
        if (ref) {
            const cascade = /onDelete\s*:\s*["']cascade["']/i.test(line);
            refs.push({ toVar: ref[1], cascade, fieldName: name });
        }
    }
    return { fields, refs };
}
/**
 * Split a columns block into top-level statements at commas that sit at
 * brace/paren/bracket depth 0 (so commas inside `.references(() => x, {...})`
 * or `.default([1, 2])` don't split a column).
 */
function splitTopLevel(block) {
    const out = [];
    let depth = 0;
    let buf = '';
    for (let i = 0; i < block.length; i++) {
        const ch = block[i];
        if (ch === '"' || ch === "'" || ch === '`') {
            const end = skipString(block, i, ch);
            buf += block.slice(i, end + 1);
            i = end;
            continue;
        }
        if (ch === '/' && block[i + 1] === '/') {
            const nl = block.indexOf('\n', i);
            const stop = nl < 0 ? block.length : nl;
            i = stop - 1;
            continue;
        }
        if (ch === '(' || ch === '{' || ch === '[')
            depth++;
        else if (ch === ')' || ch === '}' || ch === ']')
            depth--;
        if (ch === ',' && depth === 0) {
            out.push(buf);
            buf = '';
            continue;
        }
        buf += ch;
    }
    if (buf.trim())
        out.push(buf);
    return out;
}
function extractDrizzleDefault(line) {
    if (/\.defaultNow\s*\(/.test(line))
        return 'now';
    const m = line.match(/\.default\s*\(([^)]*)\)/);
    if (!m)
        return undefined;
    const raw = m[1].trim();
    return raw === '' ? undefined : raw.replace(/^["']|["']$/g, '');
}
/**
 * Drizzle const names are camelCase plural (`orgTrackedCompanies`, `users`);
 * SpecVerse entities are PascalCase singular (`OrgTrackedCompany`, `User`).
 */
function pascalSingular(varName) {
    const singular = singularize(varName);
    return singular.charAt(0).toUpperCase() + singular.slice(1);
}
function singularize(word) {
    if (/ies$/i.test(word))
        return word.replace(/ies$/i, 'y'); // companies → company
    if (/(ses|xes|zes|ches|shes)$/i.test(word))
        return word.replace(/es$/i, ''); // boxes → box
    if (/ss$/i.test(word))
        return word; // address stays address
    if (/s$/i.test(word))
        return word.replace(/s$/i, ''); // users → user
    return word;
}
//# sourceMappingURL=typescript-drizzle.js.map