import { createHash, randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { basename, extname, join, relative, resolve } from 'node:path';
const SQL_EXTENSIONS = new Set(['.sql']);
const ACTIVE_IMPORT_KINDS = new Set(['raw-sql-file', 'raw-sql-folder']);
export function createBlockStudioImportSession(projectRoot, options) {
    const inputMode = options.inputMode ?? (options.sources?.length ? 'upload' : 'path');
    const sourceBundle = collectSqlSources(projectRoot, options);
    const sourceKind = sourceBundle.sourceKind;
    if (!ACTIVE_IMPORT_KINDS.has(sourceKind)) {
        throw new Error(`${sourceKind} import is planned but not implemented in this build.`);
    }
    const now = new Date().toISOString();
    const defaults = {
        domain: sanitizeDomain(options.domain || 'imported'),
        owner: options.owner?.trim() ?? '',
        tags: normalizeTags(['imported', 'raw-sql', ...(options.tags ?? [])]),
    };
    const statements = collectSqlStatements(sourceBundle.sources);
    const importId = buildImportId(sourceKind, sourceBundle.inputPath, now);
    const candidates = statements.map((statement) => buildSqlCandidate({
        importId,
        sourceKind,
        projectRoot,
        statement,
        defaults,
    }));
    const manifest = {
        id: importId,
        sourceKind,
        inputPath: sourceBundle.inputPath,
        inputMode,
        sourceFiles: sourceBundle.sources.map((source) => source.path),
        createdAt: now,
        updatedAt: now,
        defaults,
        candidateIds: candidates.map((candidate) => candidate.id),
    };
    const session = { ...manifest, candidates };
    writeBlockStudioImportSession(projectRoot, session);
    return session;
}
export function loadBlockStudioImportSession(projectRoot, importId) {
    const root = importRoot(projectRoot, importId);
    const manifestPath = join(root, 'manifest.json');
    if (!existsSync(manifestPath))
        throw new Error(`Import session not found: ${importId}`);
    const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
    const candidates = manifest.candidateIds.map((candidateId) => readBlockStudioImportCandidate(projectRoot, importId, candidateId));
    return { ...manifest, candidates };
}
export function listBlockStudioImportSessions(projectRoot) {
    const root = join(projectRoot, '.dql', 'imports');
    if (!existsSync(root))
        return [];
    const summaries = [];
    for (const entry of readdirSync(root, { withFileTypes: true })) {
        if (!entry.isDirectory())
            continue;
        try {
            const session = loadBlockStudioImportSession(projectRoot, entry.name);
            summaries.push({
                id: session.id,
                sourceKind: session.sourceKind,
                inputMode: session.inputMode ?? 'path',
                inputPath: session.inputPath,
                sourceFiles: session.sourceFiles ?? [],
                createdAt: session.createdAt,
                updatedAt: session.updatedAt,
                defaults: session.defaults,
                candidateCount: session.candidates.length,
                savedCount: session.candidates.filter((candidate) => candidate.reviewStatus === 'saved').length,
                rejectedCount: session.candidates.filter((candidate) => candidate.reviewStatus === 'rejected').length,
                warningCount: session.candidates.reduce((sum, candidate) => sum + (candidate.warnings?.length ?? candidate.lineage.warnings.length), 0),
            });
        }
        catch {
            // Ignore partial sessions so one bad cache entry does not break Imports.
        }
    }
    return summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}
export function deleteBlockStudioImportSession(projectRoot, importId) {
    rmSync(importRoot(projectRoot, importId), { recursive: true, force: true });
}
export function clearBlockStudioImportSessions(projectRoot) {
    const root = join(projectRoot, '.dql', 'imports');
    if (!existsSync(root))
        return 0;
    let removed = 0;
    for (const entry of readdirSync(root, { withFileTypes: true })) {
        if (!entry.isDirectory())
            continue;
        if (!/^imp_[A-Za-z0-9_-]+$/.test(entry.name))
            continue;
        rmSync(join(root, entry.name), { recursive: true, force: true });
        removed += 1;
    }
    return removed;
}
export function writeBlockStudioImportSession(projectRoot, session) {
    const root = importRoot(projectRoot, session.id);
    const candidatesDir = join(root, 'candidates');
    mkdirSync(candidatesDir, { recursive: true });
    const manifest = {
        id: session.id,
        sourceKind: session.sourceKind,
        inputPath: session.inputPath,
        inputMode: session.inputMode ?? 'path',
        sourceFiles: session.sourceFiles ?? [],
        createdAt: session.createdAt,
        updatedAt: new Date().toISOString(),
        defaults: session.defaults,
        candidateIds: session.candidates.map((candidate) => candidate.id),
    };
    writeFileSync(join(root, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
    for (const candidate of session.candidates) {
        writeBlockStudioImportCandidate(projectRoot, session.id, candidate);
    }
}
export function writeBlockStudioImportCandidate(projectRoot, importId, candidate) {
    const candidatesDir = join(importRoot(projectRoot, importId), 'candidates');
    mkdirSync(candidatesDir, { recursive: true });
    writeFileSync(join(candidatesDir, `${candidate.id}.json`), JSON.stringify(candidate, null, 2) + '\n', 'utf-8');
}
export function readBlockStudioImportCandidate(projectRoot, importId, candidateId) {
    const candidatePath = join(importRoot(projectRoot, importId), 'candidates', `${candidateId}.json`);
    if (!existsSync(candidatePath))
        throw new Error(`Import candidate not found: ${candidateId}`);
    return JSON.parse(readFileSync(candidatePath, 'utf-8'));
}
export function updateBlockStudioImportCandidate(projectRoot, importId, candidateId, patch) {
    const candidate = readBlockStudioImportCandidate(projectRoot, importId, candidateId);
    const next = { ...candidate };
    if (patch.name !== undefined)
        next.name = patch.name;
    if (patch.domain !== undefined)
        next.domain = sanitizeDomain(patch.domain);
    if (patch.description !== undefined)
        next.description = patch.description;
    if (patch.owner !== undefined)
        next.owner = patch.owner;
    if (patch.tags !== undefined)
        next.tags = normalizeTags(patch.tags);
    if (patch.sql !== undefined)
        next.sql = patch.sql;
    if (patch.reviewStatus !== undefined)
        next.reviewStatus = patch.reviewStatus;
    if (patch.name || patch.domain || patch.description || patch.owner || patch.tags || patch.sql) {
        next.dqlSource = candidateToDqlSource(next);
        next.lineage = {
            ...next.lineage,
            sourceTables: extractSourceTables(next.sql),
            parameters: extractSqlParameters(next.sql),
            warnings: analyzeSqlWarnings(next.sql, next.lineage.totalStatements),
        };
        next.warnings = next.lineage.warnings;
        next.confidence = scoreSqlCandidate(next.sql, next.lineage);
    }
    writeBlockStudioImportCandidate(projectRoot, importId, next);
    return next;
}
export function candidateToDqlSource(candidate) {
    const tags = normalizeTags(candidate.tags).map((tag) => dqlString(tag)).join(', ');
    const sql = candidate.sql.trim().replace(/"""/g, '\\"\\"\\"');
    return `block ${dqlString(candidate.name)} {
    status = "draft"
    domain = ${dqlString(sanitizeDomain(candidate.domain))}
    type = "custom"
    description = ${dqlString(candidate.description)}
    tags = [${tags}]
    owner = ${dqlString(candidate.owner)}

    query = """
${sql}
    """

    visualization {
        chart = "table"
    }

    tests {
        assert row_count > 0
    }
}
`;
}
function collectSqlStatements(sources) {
    const statements = [];
    for (const source of sources) {
        const split = splitSqlStatements(source.content);
        const executableStatements = split.statements.filter(hasExecutableSql);
        const sharedWarnings = executableStatements.length === 1 ? analyzeSqlWarnings(executableStatements[0], executableStatements.length) : [];
        executableStatements.forEach((sql, index) => {
            statements.push({
                sourcePath: source.path,
                sql,
                statementIndex: index + 1,
                totalStatements: executableStatements.length,
                splitStrategy: split.strategy,
                warnings: index === 0 ? sharedWarnings : [],
            });
        });
    }
    if (statements.length === 0)
        throw new Error('No SQL statements found to import.');
    return statements;
}
function buildSqlCandidate(options) {
    const statement = options.statement;
    const sourcePath = displayPath(options.projectRoot, statement.sourcePath);
    const metadata = extractStatementMetadata(statement.sql);
    const baseName = metadata.name || basename(statement.sourcePath, extname(statement.sourcePath));
    const name = statement.totalStatements > 1 && !metadata.name
        ? `${baseName} ${statement.statementIndex}`
        : baseName;
    const sourceTables = extractSourceTables(statement.sql);
    const parameters = extractSqlParameters(statement.sql);
    const warnings = [
        ...(parameters.length > 0 ? [`Contains parameters: ${parameters.join(', ')}`] : []),
        ...(statement.sql.includes('"""') ? ['SQL contains triple quotes that were escaped in the DQL draft.'] : []),
        ...statement.warnings,
    ];
    const lineage = {
        sourceTables,
        parameters,
        warnings,
        statementIndex: statement.statementIndex,
        totalStatements: statement.totalStatements,
    };
    const candidate = {
        id: buildCandidateId(sourcePath, statement.statementIndex, statement.sql),
        sourceKind: options.sourceKind,
        sourcePath,
        name: titleizeName(name),
        domain: metadata.domain ? sanitizeDomain(metadata.domain) : options.defaults.domain,
        description: metadata.description || `Imported from ${sourcePath}`,
        owner: options.defaults.owner,
        tags: normalizeTags([...options.defaults.tags, ...metadata.tags]),
        sql: statement.sql.trim(),
        dqlSource: '',
        validation: null,
        preview: null,
        lineage,
        confidence: scoreSqlCandidate(statement.sql, lineage),
        splitStrategy: statement.splitStrategy,
        warnings,
        conversionNotes: [
            'Deterministic SQL extraction created this DQL draft locally.',
            'Name, description, domain, and tags come from leading SQL comments when present; otherwise DQL uses the source path and import defaults.',
            'Multiple scripts in one file are split by semicolon, GO batch separator, or repeated name/title header comments.',
            'The SQL statement is wrapped into query = """ ... """ without LLM rewriting.',
            'Visualization defaults to table until a reviewer chooses a chart type.',
            'The default test is assert row_count > 0.',
            'Optional AI assist is review-gated and only receives this candidate context.',
        ],
        aiAssistance: [],
        reviewStatus: 'draft',
    };
    candidate.dqlSource = candidateToDqlSource(candidate);
    return candidate;
}
function splitSqlStatements(source) {
    const semicolonStatements = splitSqlBySemicolonAndGo(source);
    if (semicolonStatements.length > 1) {
        return { statements: semicolonStatements, strategy: 'semicolon-go' };
    }
    const metadataStatements = splitSqlByMetadataHeaders(source);
    if (metadataStatements.length > 1) {
        return { statements: metadataStatements, strategy: 'metadata-comment' };
    }
    return { statements: semicolonStatements, strategy: 'semicolon-go' };
}
function splitSqlBySemicolonAndGo(source) {
    const statements = [];
    let start = 0;
    let single = false;
    let double = false;
    let backtick = false;
    let lineComment = false;
    let blockComment = false;
    let dollarQuote = null;
    for (let i = 0; i < source.length; i += 1) {
        const char = source[i];
        const next = source[i + 1];
        if (lineComment) {
            if (char === '\n')
                lineComment = false;
            continue;
        }
        if (blockComment) {
            if (char === '*' && next === '/') {
                blockComment = false;
                i += 1;
            }
            continue;
        }
        if (dollarQuote) {
            if (source.startsWith(dollarQuote, i)) {
                i += dollarQuote.length - 1;
                dollarQuote = null;
            }
            continue;
        }
        if (!single && !double && !backtick) {
            if (char === '-' && next === '-') {
                lineComment = true;
                i += 1;
                continue;
            }
            if (char === '/' && next === '*') {
                blockComment = true;
                i += 1;
                continue;
            }
            if (char === '$') {
                const tag = source.slice(i).match(/^\$[A-Za-z_][A-Za-z0-9_]*\$|^\$\$/)?.[0];
                if (tag) {
                    dollarQuote = tag;
                    i += tag.length - 1;
                    continue;
                }
            }
        }
        if (!double && !backtick && char === "'" && source[i - 1] !== '\\')
            single = !single;
        else if (!single && !backtick && char === '"' && source[i - 1] !== '\\')
            double = !double;
        else if (!single && !double && char === '`')
            backtick = !backtick;
        if (!single && !double && !backtick) {
            const goMatch = matchGoBatchSeparator(source, i);
            if (goMatch) {
                const statement = source.slice(start, i).trim();
                if (statement)
                    statements.push(statement);
                start = goMatch.end;
                i = goMatch.end - 1;
                continue;
            }
        }
        if (!single && !double && !backtick && char === ';') {
            const statement = source.slice(start, i).trim();
            if (statement)
                statements.push(statement);
            start = i + 1;
        }
    }
    const trailing = source.slice(start).trim();
    if (trailing)
        statements.push(trailing);
    return statements;
}
function splitSqlByMetadataHeaders(source) {
    const chunks = [];
    const lines = source.split(/\r?\n/);
    let current = [];
    const flush = () => {
        const chunk = current.join('\n').trim();
        if (chunk)
            chunks.push(chunk);
        current = [];
    };
    for (const line of lines) {
        const isHeader = /^\s*(?:--|\/\*)\s*(?:name|block|title|query)\s*:/i.test(line);
        if (isHeader && current.some((item) => stripSqlComments(item).trim().length > 0)) {
            flush();
        }
        current.push(line);
    }
    flush();
    return chunks.filter((chunk) => /\b(select|with)\b/i.test(stripSqlComments(chunk)));
}
function matchGoBatchSeparator(source, index) {
    const char = source[index];
    if (char !== 'g' && char !== 'G')
        return null;
    const lineStart = Math.max(source.lastIndexOf('\n', index - 1) + 1, 0);
    if (source.slice(lineStart, index).trim())
        return null;
    const rest = source.slice(index);
    const match = rest.match(/^go(?:\s+\d+)?\s*(?:--[^\r\n]*)?(?:\r?\n|$)/i);
    if (!match)
        return null;
    return { end: index + match[0].length };
}
function analyzeSqlWarnings(sql, totalStatements) {
    const warnings = [];
    if (totalStatements === 1) {
        const cleaned = stripSqlComments(sql);
        const starts = cleaned.match(/\b(?:select|with)\b/gi) ?? [];
        if (starts.length > 1) {
            warnings.push('Only one candidate was created, but multiple SELECT/WITH clauses were detected. Add semicolons, GO batch separators, or split manually if this file contains multiple scripts.');
        }
    }
    return warnings;
}
function extractStatementMetadata(sql) {
    const leading = sql.split(/\r?\n/).slice(0, 12).join('\n');
    const name = leading.match(/(?:--|\/\*)\s*(?:name|block|title|query)\s*:\s*([^*\n]+)/i)?.[1]?.trim() ?? '';
    const description = leading.match(/(?:--|\/\*)\s*(?:description|desc)\s*:\s*([^*\n]+)/i)?.[1]?.trim() ?? '';
    const domain = leading.match(/(?:--|\/\*)\s*domain\s*:\s*([^*\n]+)/i)?.[1]?.trim() ?? '';
    const tagText = leading.match(/(?:--|\/\*)\s*tags?\s*:\s*([^*\n]+)/i)?.[1]?.trim() ?? '';
    const tags = normalizeTags(tagText ? tagText.split(',') : []);
    if (description)
        return { name, description, domain, tags };
    const firstComment = leading.match(/^\s*--\s*(?!name\s*:|block\s*:)(.+)$/im)?.[1]?.trim() ?? '';
    return { name, description: firstComment, domain, tags };
}
function extractSourceTables(sql) {
    const tables = new Set();
    const cleaned = stripSqlComments(sql);
    const cteNames = extractCteNames(cleaned);
    const regex = /\b(?:from|join|update|into)\s+([`"[]?[A-Za-z0-9_./:-]+(?:\.[A-Za-z0-9_./:-]+)*[`"\]]?)/gi;
    let match;
    while ((match = regex.exec(cleaned))) {
        const raw = match[1].replace(/^[`"[]|[`"\]]$/g, '');
        if (!raw || raw.startsWith('('))
            continue;
        if (/^(select|values|unnest|lateral)$/i.test(raw))
            continue;
        if (cteNames.has(raw.toLowerCase()))
            continue;
        tables.add(raw);
    }
    return Array.from(tables);
}
function extractCteNames(sql) {
    const names = new Set();
    const withIndex = sql.search(/\bwith\b/i);
    if (withIndex < 0)
        return names;
    const prefix = sql.slice(withIndex);
    const regex = /(?:\bwith\b|,)\s+([A-Za-z_][A-Za-z0-9_$]*)\s+as\s*\(/gi;
    let match;
    while ((match = regex.exec(prefix)))
        names.add(match[1].toLowerCase());
    return names;
}
function hasExecutableSql(sql) {
    return stripSqlComments(sql).trim().length > 0;
}
function extractSqlParameters(sql) {
    const params = new Set();
    const cleaned = stripSqlStrings(stripSqlComments(sql));
    let match;
    const handlebars = /\{\{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*\}\}/g;
    while ((match = handlebars.exec(cleaned)))
        params.add(match[1]);
    const colon = /(^|[^:]):([A-Za-z_][A-Za-z0-9_]*)\b/g;
    while ((match = colon.exec(cleaned)))
        params.add(match[2]);
    const dollar = /(^|[^$])\$([A-Za-z_][A-Za-z0-9_]*)\b/g;
    while ((match = dollar.exec(cleaned)))
        params.add(match[2]);
    return Array.from(params);
}
function stripSqlComments(sql) {
    return sql
        .replace(/\/\*[\s\S]*?\*\//g, ' ')
        .replace(/--[^\n\r]*/g, ' ');
}
function stripSqlStrings(sql) {
    return sql
        .replace(/'(?:''|[^'])*'/g, "''")
        .replace(/"(?:\\"|[^"])*"/g, '""');
}
function scoreSqlCandidate(sql, lineage) {
    let score = /\bselect\b/i.test(sql) ? 0.76 : 0.62;
    if (lineage.sourceTables.length > 0)
        score += 0.1;
    if (/\bgroup\s+by\b/i.test(sql))
        score += 0.04;
    if (lineage.parameters.length > 0)
        score -= 0.12;
    if (lineage.totalStatements > 1)
        score -= 0.03;
    return Math.max(0.35, Math.min(0.92, Number(score.toFixed(2))));
}
function walkSqlFiles(root) {
    const files = [];
    const stack = [root];
    while (stack.length > 0) {
        const dir = stack.pop();
        for (const entry of readdirSync(dir, { withFileTypes: true })) {
            if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.dql')
                continue;
            const full = join(dir, entry.name);
            if (entry.isDirectory())
                stack.push(full);
            else if (entry.isFile() && SQL_EXTENSIONS.has(extname(entry.name).toLowerCase()))
                files.push(full);
        }
    }
    return files.sort();
}
function collectSqlSources(projectRoot, options) {
    if (options.sources?.length) {
        const sources = options.sources
            .map((source, index) => ({
            path: sanitizeSourcePath(source.path || `pasted-${index + 1}.sql`),
            content: source.content ?? '',
        }))
            .filter((source) => source.content.trim().length > 0);
        if (sources.length === 0)
            throw new Error('No SQL content was provided.');
        return {
            sourceKind: sources.length > 1 ? 'raw-sql-folder' : 'raw-sql-file',
            inputPath: options.inputMode === 'paste' ? 'pasted SQL' : `${sources.length} uploaded SQL file(s)`,
            sources,
        };
    }
    const inputPath = resolveInputPath(projectRoot, options.inputPath ?? '');
    const sourceKind = resolveSourceKind(inputPath, options.sourceKind);
    const stats = statSync(inputPath);
    const files = stats.isDirectory() ? walkSqlFiles(inputPath) : [inputPath];
    if (files.length === 0)
        throw new Error('No .sql files found to import.');
    return {
        sourceKind,
        inputPath: displayPath(projectRoot, inputPath),
        sources: files.map((file) => ({
            path: displayPath(projectRoot, file),
            content: readFileSync(file, 'utf-8'),
        })),
    };
}
function sanitizeSourcePath(path) {
    const clean = path.replaceAll('\\', '/').replace(/^\/+/, '').trim();
    return clean.endsWith('.sql') ? clean : `${clean || 'pasted'}.sql`;
}
function resolveInputPath(projectRoot, inputPath) {
    if (!inputPath.trim())
        throw new Error('Import path is required.');
    const resolved = resolve(projectRoot, inputPath);
    if (!existsSync(resolved))
        throw new Error(`Import path not found: ${inputPath}`);
    return resolved;
}
function resolveSourceKind(inputPath, requested) {
    if (requested && requested !== 'raw-sql')
        return requested;
    const stats = statSync(inputPath);
    if (stats.isDirectory())
        return 'raw-sql-folder';
    if (extname(inputPath).toLowerCase() === '.sql')
        return 'raw-sql-file';
    throw new Error('Only .sql files and folders are supported in this import build.');
}
function importRoot(projectRoot, importId) {
    if (!/^imp_[A-Za-z0-9_-]+$/.test(importId)) {
        throw new Error(`Invalid import session id: ${importId}`);
    }
    return join(projectRoot, '.dql', 'imports', importId);
}
function buildImportId(sourceKind, inputPath, createdAt) {
    const hash = createHash('sha1').update(`${sourceKind}:${inputPath}:${createdAt}:${randomUUID()}`).digest('hex').slice(0, 10);
    return `imp_${hash}`;
}
function buildCandidateId(sourcePath, statementIndex, sql) {
    const hash = createHash('sha1').update(`${sourcePath}:${statementIndex}:${sql}`).digest('hex').slice(0, 12);
    return `cand_${hash}`;
}
function displayPath(projectRoot, absPath) {
    const rel = relative(projectRoot, absPath).replaceAll('\\', '/');
    return rel && !rel.startsWith('..') ? rel : absPath.replaceAll('\\', '/');
}
function sanitizeDomain(domain) {
    return domain
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9/_-]+/g, '-')
        .replace(/^\/+|\/+$/g, '') || 'imported';
}
function normalizeTags(tags) {
    return Array.from(new Set(tags.map((tag) => tag.trim().toLowerCase()).filter(Boolean)));
}
function titleizeName(name) {
    return name
        .replace(/\.[^.]+$/, '')
        .replace(/[_-]+/g, ' ')
        .replace(/\s+/g, ' ')
        .trim()
        .replace(/\b\w/g, (char) => char.toUpperCase()) || 'Imported Query';
}
function dqlString(value) {
    return JSON.stringify(value ?? '');
}
//# sourceMappingURL=block-studio-import.js.map