import jsYaml from 'js-yaml';
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { extractPivotNameFromSeparator, isPivotSeparator, parsePivotMetadataRows, } from '../transformers/pivot-transformer.js';
import { sheetRowToProperty } from '../transformers/property-transformer.js';
// Sheets to exclude from module processing (system sheets)
const EXCLUDED_SHEETS = new Set(['DATA', 'MODULES', 'TEMPLATE']);
/** Pull schemas from one BC's Google Sheet into local YAML files. */
export async function pullBoundedContext(context, options = {}) {
    const result = {
        messages: [],
        modulesFailed: [],
        modulesProcessed: 0,
        success: true,
    };
    try {
        const bcPath = join(context.cliterPath, context.boundedContextName);
        if (options.backup !== false) {
            const backupPath = createBackup(context.backupsPath, context.cliterPath, context.boundedContextName);
            if (backupPath) {
                result.backupPath = backupPath;
                result.messages.push(`Backup created: ${backupPath}`);
            }
        }
        const { sheetsApi, spreadsheetId } = context;
        // Get all sheets in the spreadsheet
        const spreadsheet = await sheetsApi.spreadsheets.get({ spreadsheetId });
        const sheets = spreadsheet.data.sheets || [];
        const moduleSheets = sheets.filter((s) => s.properties?.title && !EXCLUDED_SHEETS.has(s.properties.title));
        if (options.dryRun) {
            result.messages.push(`[DRY RUN] Would pull ${moduleSheets.length} modules from spreadsheet`);
            result.modulesProcessed = moduleSheets.length;
            return result;
        }
        // Ensure output directory exists
        mkdirSync(bcPath, { recursive: true });
        // Process each module sheet
        for (const sheet of moduleSheets) {
            const sheetName = sheet.properties.title;
            try {
                // Try to read existing YAML to preserve metadata and fields not in spreadsheet
                const existingFilePath = join(bcPath, `${sheetName}.aurora.yaml`);
                let existingSchema;
                let existingYamlContent;
                if (existsSync(existingFilePath)) {
                    existingYamlContent = readFileSync(existingFilePath, 'utf8');
                    existingSchema = jsYaml.load(existingYamlContent);
                }
                const schema = await readModuleSheet(sheetsApi, spreadsheetId, sheetName, existingSchema);
                if (!schema) {
                    result.messages.push(`Skipped: ${sheetName} (empty or invalid)`);
                    continue;
                }
                const filePath = join(bcPath, `${sheetName}.aurora.yaml`);
                // Ensure descriptions end with newline for proper YAML folded style
                normalizeDescriptions(schema);
                let yamlContent = jsYaml.dump(schema, {
                    indent: 2,
                    lineWidth: 500,
                    noRefs: true,
                    quotingType: '"',
                    sortKeys: false,
                });
                yamlContent = convertArraysToInlineFormat(yamlContent);
                if (existingYamlContent && existingSchema) {
                    yamlContent = restoreOriginalDescriptions(yamlContent, existingYamlContent, schema, existingSchema);
                }
                if (!yamlContent.endsWith('\n')) {
                    yamlContent += '\n';
                }
                // Check if content actually changed
                if (existingYamlContent) {
                    const existingNormalized = normalizeYamlForComparison(existingYamlContent);
                    const newNormalized = normalizeYamlForComparison(yamlContent);
                    if (existingNormalized === newNormalized) {
                        result.modulesProcessed++;
                        result.messages.push(`Pulled: ${sheetName} (no changes)`);
                        continue;
                    }
                }
                writeFileSync(filePath, yamlContent, 'utf8');
                result.modulesProcessed++;
                result.messages.push(`Pulled: ${sheetName}`);
            }
            catch (error) {
                const err = error;
                result.modulesFailed.push(sheetName);
                result.messages.push(`Failed: ${sheetName} - ${err.message}`);
                result.success = false;
            }
        }
    }
    catch (error) {
        const err = error;
        result.success = false;
        result.messages.push(`Error: ${err.message}`);
    }
    return result;
}
async function readModuleSheet(api, spreadsheetId, sheetName, existingSchema) {
    const response = await api.spreadsheets.values.get({
        range: `'${sheetName}'!A:Z`,
        spreadsheetId,
    });
    const rows = response.data.values || [];
    if (rows.length < 2)
        return null;
    const headers = rows[0];
    if (!headers.includes('name') || !headers.includes('type')) {
        return null;
    }
    const properties = [];
    const pivots = new Map();
    for (let i = 1; i < rows.length; i++) {
        const row = rows[i];
        if (!row[0]?.trim())
            continue;
        if (isPivotSeparator(row)) {
            const pivotName = extractPivotNameFromSeparator(row);
            if (pivotName) {
                const { endIdx, pivot } = parsePivotSection(rows, i);
                if (pivot) {
                    pivots.set(pivotName, pivot);
                }
                i = endIdx - 1;
            }
            continue;
        }
        const propObj = {};
        for (const [j, header] of headers.entries()) {
            propObj[header] = row[j] || '';
        }
        const sheetProperty = sheetRowToProperty(propObj);
        if (existingSchema) {
            const existingProp = existingSchema.aggregateProperties?.find((p) => p.name === sheetProperty.name);
            if (existingProp) {
                const merged = mergeProperties(existingProp, sheetProperty, headers);
                properties.push(merged);
            }
            else {
                properties.push(sheetProperty);
            }
        }
        else {
            properties.push(sheetProperty);
        }
    }
    const schema = existingSchema
        ? {
            ...existingSchema,
            aggregateProperties: properties,
        }
        : {
            aggregateName: toPascalCase(sheetName),
            aggregateProperties: properties,
            boundedContextName: '',
            moduleName: sheetName,
            moduleNames: sheetName + 's',
            version: '0.0.1',
        };
    if (existingSchema?.description && schema.description) {
        const existingNorm = normalizeText(existingSchema.description);
        const currentNorm = normalizeText(schema.description);
        if (existingNorm === currentNorm) {
            schema.description = existingSchema.description;
        }
    }
    mergePivotsIntoProperties(schema.aggregateProperties, pivots);
    return schema;
}
/** Convert multiline arrays to inline format for `enumOptions`, `decimals`, `defaultValue`. */
function convertArraysToInlineFormat(yamlContent) {
    const inlineArrayKeys = ['enumOptions', 'decimals', 'defaultValue'];
    const lines = yamlContent.split('\n');
    const result = [];
    let i = 0;
    while (i < lines.length) {
        const line = lines[i];
        const trimmed = line.trim();
        let foundKey = false;
        for (const key of inlineArrayKeys) {
            if ((trimmed === `${key}:` || trimmed.startsWith(`${key}: `)) && i + 1 < lines.length && lines[i + 1].trim().startsWith('- ')) {
                const keyIndent = line.length - line.trimStart().length;
                const expectedArrayItemIndent = keyIndent + 2;
                const items = [];
                let j = i + 1;
                while (j < lines.length) {
                    const arrayLine = lines[j];
                    const arrayTrimmed = arrayLine.trim();
                    const lineIndent = arrayLine.length - arrayLine.trimStart().length;
                    if (arrayTrimmed.startsWith('- ') &&
                        lineIndent === expectedArrayItemIndent) {
                        items.push(arrayTrimmed.slice(2).trim());
                        j++;
                    }
                    else if (arrayTrimmed === '') {
                        j++;
                    }
                    else {
                        break;
                    }
                }
                result.push(`${' '.repeat(keyIndent)}${key}: [${items.join(', ')}]`);
                i = j;
                foundKey = true;
                break;
            }
        }
        if (!foundKey) {
            result.push(line);
            i++;
        }
    }
    return result.join('\n');
}
/** Ensure descriptions end with `\n` so js-yaml emits proper folded style. */
function normalizeDescriptions(schema) {
    if (schema.description && !schema.description.endsWith('\n')) {
        schema.description += '\n';
    }
    for (const prop of schema.aggregateProperties || []) {
        if (prop.description && !prop.description.endsWith('\n')) {
            prop.description += '\n';
        }
    }
}
/** Restore original description blocks from existing YAML when normalized content matches. */
function restoreOriginalDescriptions(newYaml, existingYaml, newSchema, existingSchema) {
    let result = newYaml;
    const existingDescriptions = extractDescriptionBlocks(existingYaml);
    if (newSchema.description && existingSchema.description) {
        const newNorm = normalizeText(newSchema.description);
        const existNorm = normalizeText(existingSchema.description);
        if (newNorm === existNorm && existingDescriptions.has('__module__')) {
            const newBlock = extractModuleDescriptionBlock(result);
            const existBlock = existingDescriptions.get('__module__');
            if (newBlock && existBlock) {
                result = result.replace(newBlock, existBlock);
            }
        }
    }
    for (const prop of newSchema.aggregateProperties || []) {
        if (!prop.description)
            continue;
        const newNorm = normalizeText(prop.description);
        let existingProp = existingSchema.aggregateProperties?.find((p) => p.name === prop.name);
        if (!existingProp?.description) {
            existingProp = existingSchema.aggregateProperties?.find((p) => p.description && normalizeText(p.description) === newNorm);
        }
        if (!existingProp?.description)
            continue;
        const existNorm = normalizeText(existingProp.description);
        if (newNorm === existNorm && existingDescriptions.has(existingProp.name)) {
            const newBlock = extractPropertyDescriptionBlock(result, prop.name);
            const existBlock = existingDescriptions.get(existingProp.name);
            if (newBlock && existBlock) {
                result = result.replace(newBlock, existBlock);
            }
        }
    }
    return result;
}
function extractDescriptionBlocks(yamlContent) {
    const blocks = new Map();
    const lines = yamlContent.split('\n');
    const moduleBlock = extractModuleDescriptionBlock(yamlContent);
    if (moduleBlock) {
        blocks.set('__module__', moduleBlock);
    }
    let currentPropName = null;
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const propMatch = line.match(/^\s{2}- name:\s*(.+)$/);
        if (propMatch) {
            currentPropName = propMatch[1].trim();
            continue;
        }
        if (currentPropName && /^\s{4}description:\s*[>|][-+]?\s*$/.test(line)) {
            let block = line + '\n';
            let j = i + 1;
            while (j < lines.length) {
                const contentLine = lines[j];
                if (/^\s{6}/.test(contentLine) || contentLine.trim() === '') {
                    block += contentLine + '\n';
                    j++;
                }
                else {
                    break;
                }
            }
            block = block.replace(/\n+$/, '\n');
            blocks.set(currentPropName, block);
        }
    }
    return blocks;
}
function extractModuleDescriptionBlock(yamlContent) {
    const lines = yamlContent.split('\n');
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (/^description:\s*[>|][-+]?\s*$/.test(line)) {
            let block = line + '\n';
            let j = i + 1;
            while (j < lines.length) {
                const contentLine = lines[j];
                if (/^\s{2}\S/.test(contentLine) && !/^\s{2}-/.test(contentLine)) {
                    block += contentLine + '\n';
                    j++;
                }
                else if (contentLine.trim() === '') {
                    j++;
                }
                else {
                    break;
                }
            }
            block = block.replace(/\n+$/, '\n');
            return block;
        }
    }
    return null;
}
function extractPropertyDescriptionBlock(yamlContent, propName) {
    const lines = yamlContent.split('\n');
    let foundProp = false;
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const propMatch = line.match(/^\s{2}- name:\s*(.+)$/);
        if (propMatch && propMatch[1].trim() === propName) {
            foundProp = true;
            continue;
        }
        if (foundProp && /^\s{2}- name:/.test(line)) {
            return null;
        }
        if (foundProp && /^\s{4}description:\s*[>|][-+]?\s*$/.test(line)) {
            let block = line + '\n';
            let j = i + 1;
            while (j < lines.length) {
                const contentLine = lines[j];
                if (/^\s{6}/.test(contentLine) || contentLine.trim() === '') {
                    block += contentLine + '\n';
                    j++;
                }
                else {
                    break;
                }
            }
            block = block.replace(/\n+$/, '\n');
            return block;
        }
    }
    return null;
}
function toPascalCase(str) {
    return str
        .split('-')
        .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
        .join('');
}
const HEADER_TO_PROPERTY_FIELD = {
    'arrayOptions.enumOptions': 'arrayOptions',
    'arrayOptions.maxLength': 'arrayOptions',
    'arrayOptions.type': 'arrayOptions',
    autoIncrement: 'autoIncrement',
    decimals: 'decimals',
    defaultValue: 'defaultValue',
    description: 'description',
    enumOptions: 'enumOptions',
    example: 'example',
    index: 'index',
    indexUsing: 'indexUsing',
    isI18n: 'isI18n',
    master: 'relationship',
    maxLength: 'maxLength',
    name: 'name',
    nullable: 'nullable',
    primaryKey: 'primaryKey',
    'rel.aggregateName': 'relationship',
    'rel.avoidConstraint': 'relationship',
    'rel.field': 'relationship',
    'rel.key': 'relationship',
    'rel.modulePath': 'relationship',
    'rel.singularName': 'relationship',
    'rel.type': 'relationship',
    relationship: 'relationship',
    subtype: 'arrayOptions',
    type: 'type',
    values: 'enumOptions',
};
function normalizeText(text) {
    if (!text)
        return '';
    return text.replaceAll(/\s+/g, ' ').trim();
}
function normalizeYamlForComparison(content) {
    try {
        const parsed = jsYaml.load(content);
        return jsYaml.dump(parsed, {
            indent: 2,
            lineWidth: -1,
            noRefs: true,
            sortKeys: true,
        });
    }
    catch {
        return content;
    }
}
function normalizeDefaultValue(val) {
    if (val === undefined || val === null)
        return '';
    if (Array.isArray(val)) {
        return val.map(String).join(',').toUpperCase();
    }
    if (typeof val === 'boolean') {
        return val ? 'TRUE' : 'FALSE';
    }
    return String(val).toUpperCase().trim();
}
function areDefaultValuesEquivalent(existing, fromSheet) {
    return normalizeDefaultValue(existing) === normalizeDefaultValue(fromSheet);
}
/** Merge a YAML property with the same property pulled from the sheet, honoring sheet-controlled fields. */
function mergeProperties(existing, fromSheet, headers) {
    const merged = { ...existing };
    const sheetControlledFields = new Set();
    for (const header of headers) {
        const field = HEADER_TO_PROPERTY_FIELD[header];
        if (field) {
            sheetControlledFields.add(field);
        }
    }
    for (const field of sheetControlledFields) {
        switch (field) {
            case 'arrayOptions': {
                if (fromSheet.arrayOptions) {
                    merged.arrayOptions = fromSheet.arrayOptions;
                }
                else if (headers.includes('subtype') ||
                    headers.includes('arrayOptions.type')) {
                    delete merged.arrayOptions;
                }
                break;
            }
            case 'defaultValue': {
                if (existing.defaultValue !== undefined &&
                    fromSheet.defaultValue !== undefined) {
                    merged.defaultValue = areDefaultValuesEquivalent(existing.defaultValue, fromSheet.defaultValue) ? existing.defaultValue : fromSheet.defaultValue;
                }
                else if (fromSheet.defaultValue !== undefined) {
                    merged.defaultValue = fromSheet.defaultValue;
                }
                break;
            }
            case 'description': {
                const existingNorm = normalizeText(existing.description);
                const sheetNorm = normalizeText(fromSheet.description);
                if (existingNorm === sheetNorm && existing.description) {
                    merged.description = existing.description;
                }
                else if (fromSheet.description) {
                    merged.description = fromSheet.description;
                }
                break;
            }
            case 'relationship': {
                if (fromSheet.relationship) {
                    merged.relationship = existing.relationship ? {
                        ...existing.relationship,
                        ...Object.fromEntries(Object.entries(fromSheet.relationship).filter(([, v]) => v !== undefined && v !== null && v !== '')),
                    } : fromSheet.relationship;
                }
                else if ((headers.includes('relationship') ||
                    headers.includes('rel.type')) && !existing.relationship) {
                    delete merged.relationship;
                }
                break;
            }
            default: {
                const sheetValue = fromSheet[field];
                if (sheetValue !== undefined) {
                    merged[field] = sheetValue;
                }
            }
        }
    }
    return merged;
}
function parsePivotSection(rows, startIdx) {
    const pivotName = extractPivotNameFromSeparator(rows[startIdx]);
    if (!pivotName)
        return { endIdx: startIdx + 1, pivot: null };
    let i = startIdx + 1;
    const metadataRows = [];
    let headerIdx = -1;
    while (i < rows.length) {
        const row = rows[i];
        if (row[0] === 'name' && row[1] === 'type') {
            headerIdx = i;
            break;
        }
        if (row[0]?.trim() && !isPivotSeparator(row)) {
            metadataRows.push(row);
        }
        i++;
    }
    if (headerIdx === -1)
        return { endIdx: i, pivot: null };
    const metadata = parsePivotMetadataRows(metadataRows);
    const pivotProperties = [];
    const headers = rows[headerIdx];
    i = headerIdx + 1;
    while (i < rows.length) {
        const row = rows[i];
        if (isPivotSeparator(row) || (!row[0]?.trim() && !row[1]?.trim())) {
            break;
        }
        if (row[0]?.trim()) {
            const propObj = {};
            for (const [j, header] of headers.entries()) {
                propObj[header] = row[j] || '';
            }
            pivotProperties.push(sheetRowToProperty(propObj));
        }
        i++;
    }
    const pivot = {
        aggregateName: metadata.aggregateName || '',
        aggregateProperties: pivotProperties,
        boundedContextName: metadata.boundedContextName || '',
        hasAuditing: metadata.hasAuditing,
        hasOAuth: metadata.hasOAuth,
        hasTenant: metadata.hasTenant,
        moduleName: metadata.moduleName || pivotName,
        moduleNames: metadata.moduleNames || '',
    };
    return { endIdx: i, pivot };
}
/** Merge parsed pivot rows back into the matching many-to-many `relationship.pivot`. */
function mergePivotsIntoProperties(properties, pivots) {
    for (const prop of properties) {
        if (prop.relationship?.type === 'many-to-many' &&
            !prop.relationship.pivot) {
            const possiblePivotNames = [
                `${prop.name}-${prop.relationship.aggregateName?.toLowerCase()}`,
                `${prop.relationship.aggregateName?.toLowerCase()}-${prop.name}`,
            ];
            for (const pivotName of possiblePivotNames) {
                const pivot = pivots.get(pivotName);
                if (pivot) {
                    prop.relationship.pivot = pivot;
                    break;
                }
            }
            for (const [, pivot] of pivots) {
                if (pivot.aggregateProperties?.some((p) => p.relationship?.modulePath === prop.relationship?.modulePath)) {
                    prop.relationship.pivot = pivot;
                    break;
                }
            }
        }
    }
}
function createBackup(backupsPath, cliterPath, boundedContextName) {
    const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
    const backupDir = join(backupsPath, boundedContextName, timestamp);
    const sourcePath = join(cliterPath, boundedContextName);
    if (existsSync(sourcePath)) {
        mkdirSync(backupDir, { recursive: true });
        cpSync(sourcePath, backupDir, { recursive: true });
        return backupDir;
    }
    return null;
}
/** List module-sheet names in the spreadsheet (excludes DATA / MODULES / TEMPLATE). */
export async function listModulesInSheet(sheetsApi, spreadsheetId) {
    const spreadsheet = await sheetsApi.spreadsheets.get({ spreadsheetId });
    return (spreadsheet.data.sheets || [])
        .map((s) => s.properties?.title)
        .filter((title) => typeof title === 'string' && !EXCLUDED_SHEETS.has(title));
}
