import jsYaml from 'js-yaml';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { CHECKMARK, getSchemaPath } from '../domain/types.js';
import { propertyToSheetRow } from '../transformers/property-transformer.js';
import { createPropertyWithoutPivot } from '../transformers/relationship-transformer.js';
import { formatValidationResult, validateSchema, } from '../validators/schema-validator.js';
const TEMPLATE_SHEET_NAME = 'TEMPLATE';
const INDEX_SHEET_NAME = 'MODULES';
/** Push YAML schemas of one bounded context to its Google Sheet. */
export async function pushBoundedContext(context, options = {}) {
    const result = {
        messages: [],
        modulesFailed: [],
        modulesProcessed: 0,
        success: true,
    };
    try {
        const bcPath = join(context.cliterPath, context.boundedContextName);
        if (!existsSync(bcPath)) {
            throw new Error(`Bounded context path not found: ${bcPath}`);
        }
        const yamlFiles = readdirSync(bcPath);
        const schemas = [];
        for (const file of yamlFiles) {
            if (!file.endsWith('.aurora.yaml'))
                continue;
            const filePath = join(bcPath, file);
            const content = readFileSync(filePath, 'utf8');
            const schema = jsYaml.load(content);
            const validation = validateSchema(schema);
            if (!validation.isValid) {
                result.messages.push(`Validation failed for ${file}:`, formatValidationResult(validation));
                result.modulesFailed.push(schema.moduleName);
                result.success = false;
                continue;
            }
            schemas.push(schema);
        }
        if (options.dryRun) {
            result.messages.push(`[DRY RUN] Would push ${schemas.length} modules to spreadsheet ${context.spreadsheetId}`);
            result.modulesProcessed = schemas.length;
            return result;
        }
        const { sheetsApi, spreadsheetId } = context;
        // Module sheets must exist BEFORE we collect IDs for hyperlinks.
        for (const schema of schemas) {
            await ensureSheetExists(sheetsApi, spreadsheetId, schema.moduleName);
        }
        const moduleSheetIds = await getSheetIdMap(sheetsApi, spreadsheetId);
        const hyperlinkContext = {
            allBcConfigs: context.allBcConfigs,
            currentBcName: context.boundedContextName,
            externalSheetIdsCache: new Map(),
            sheetsApi,
        };
        for (const schema of schemas) {
            try {
                await updateModuleSheet(sheetsApi, spreadsheetId, schema, moduleSheetIds, hyperlinkContext);
                result.modulesProcessed++;
                result.messages.push(`Updated: ${schema.moduleName}`);
            }
            catch (error) {
                const err = error;
                result.modulesFailed.push(schema.moduleName);
                result.messages.push(`Failed: ${schema.moduleName} - ${err.message}`);
                result.success = false;
            }
        }
        // MODULES index must update AFTER module sheets exist (it references them).
        await updateIndexSheet(sheetsApi, spreadsheetId, schemas);
        result.messages.push(`Updated MODULES sheet with ${schemas.length} modules`);
        // Reorder sheets: MODULES -> [modules alphabetically] -> TEMPLATE -> DATA
        await reorderSheets(sheetsApi, spreadsheetId, schemas);
    }
    catch (error) {
        const err = error;
        result.success = false;
        result.messages.push(`Error: ${err.message}`);
    }
    return result;
}
async function readIndexHeaders(api, spreadsheetId) {
    const response = await api.spreadsheets.values.get({
        range: `'${INDEX_SHEET_NAME}'!1:1`,
        spreadsheetId,
    });
    return (response.data.values?.[0] || []);
}
/** Read a value from `schema` at the given path (uses `:` as nested separator). */
function getSchemaValue(schema, path) {
    const schemaPath = getSchemaPath(path);
    const parts = schemaPath.split(':');
    let value = schema;
    for (const part of parts) {
        if (value && typeof value === 'object') {
            value = value[part];
        }
        else {
            return undefined;
        }
    }
    return value;
}
function getBoundedContextFromPath(modulePath) {
    const parts = modulePath.split('/');
    return parts[0] || '';
}
function formatCellValue(value, header, sheetId, schema) {
    if (header === 'moduleName') {
        return `=HYPERLINK("#gid=${sheetId}", "${schema.moduleName}")`;
    }
    if (typeof value === 'boolean') {
        return value ? CHECKMARK : '';
    }
    return value?.toString() || '';
}
async function updateIndexSheet(api, spreadsheetId, schemas) {
    await ensureSheetExists(api, spreadsheetId, INDEX_SHEET_NAME);
    const headers = await readIndexHeaders(api, spreadsheetId);
    if (headers.length === 0) {
        throw new Error(`No headers found in ${INDEX_SHEET_NAME} sheet. Please add headers in row 1.`);
    }
    const dataRows = [];
    for (const schema of schemas) {
        const sheetId = await getSheetIdByName(api, spreadsheetId, schema.moduleName);
        const row = headers.map((header) => {
            const value = getSchemaValue(schema, header);
            return formatCellValue(value, header, sheetId, schema);
        });
        dataRows.push(row);
    }
    const lastColumn = String.fromCodePoint(64 + headers.length);
    await api.spreadsheets.values.clear({
        range: `'${INDEX_SHEET_NAME}'!A2:${lastColumn}`,
        spreadsheetId,
    });
    if (dataRows.length > 0) {
        await api.spreadsheets.values.update({
            range: `'${INDEX_SHEET_NAME}'!A2`,
            requestBody: { values: dataRows },
            spreadsheetId,
            valueInputOption: 'USER_ENTERED',
        });
        const modulesSheetId = await getSheetIdByName(api, spreadsheetId, INDEX_SHEET_NAME);
        await api.spreadsheets.batchUpdate({
            requestBody: {
                requests: [
                    {
                        updateDimensionProperties: {
                            fields: 'pixelSize',
                            properties: { pixelSize: 20 },
                            range: {
                                dimension: 'ROWS',
                                endIndex: 1 + dataRows.length,
                                sheetId: modulesSheetId,
                                startIndex: 1,
                            },
                        },
                    },
                    {
                        clearBasicFilter: { sheetId: modulesSheetId },
                    },
                    {
                        setBasicFilter: {
                            filter: {
                                range: {
                                    endColumnIndex: headers.length,
                                    endRowIndex: 1 + dataRows.length,
                                    sheetId: modulesSheetId,
                                    startColumnIndex: 0,
                                    startRowIndex: 0,
                                },
                            },
                        },
                    },
                ],
            },
            spreadsheetId,
        });
    }
}
/** Reorder: MODULES → [modules alphabetically] → TEMPLATE → DATA. */
async function reorderSheets(api, spreadsheetId, schemas) {
    const spreadsheet = await api.spreadsheets.get({ spreadsheetId });
    const sheets = spreadsheet.data.sheets || [];
    const moduleNames = schemas.map((s) => s.moduleName).sort();
    const desiredOrder = [
        INDEX_SHEET_NAME,
        ...moduleNames,
        TEMPLATE_SHEET_NAME,
        'DATA',
    ];
    const requests = [];
    for (const [i, sheetName] of desiredOrder.entries()) {
        const sheet = sheets.find((s) => s.properties?.title === sheetName);
        if (sheet?.properties?.sheetId !== undefined) {
            requests.push({
                updateSheetProperties: {
                    fields: 'index',
                    properties: {
                        index: i,
                        sheetId: sheet.properties.sheetId,
                    },
                },
            });
        }
    }
    if (requests.length > 0) {
        await api.spreadsheets.batchUpdate({
            requestBody: { requests },
            spreadsheetId,
        });
    }
}
async function readModuleHeaders(api, spreadsheetId, sheetName) {
    const response = await api.spreadsheets.values.get({
        range: `'${sheetName}'!1:1`,
        spreadsheetId,
    });
    return (response.data.values?.[0] || []);
}
async function mapPropertyToRow(prop, headers, moduleSheetIds, hyperlinkContext) {
    const propRow = propertyToSheetRow(prop);
    const row = [];
    for (const header of headers) {
        const value = propRow[header];
        // `master` column → hyperlink (internal `#gid=` or cross-BC external URL)
        if (header === 'master' && value) {
            const internalSheetId = moduleSheetIds.get(value);
            if (internalSheetId !== undefined) {
                row.push(`=HYPERLINK("#gid=${internalSheetId}", "${value}")`);
                continue;
            }
            const modulePath = prop.relationship?.modulePath;
            if (modulePath) {
                const externalBcName = getBoundedContextFromPath(modulePath);
                const externalBcConfig = hyperlinkContext.allBcConfigs[externalBcName];
                if (externalBcConfig &&
                    externalBcName !== hyperlinkContext.currentBcName) {
                    let externalSheetIds = hyperlinkContext.externalSheetIdsCache.get(externalBcName);
                    if (!externalSheetIds) {
                        externalSheetIds = await getSheetIdMap(hyperlinkContext.sheetsApi, externalBcConfig.spreadsheetId);
                        hyperlinkContext.externalSheetIdsCache.set(externalBcName, externalSheetIds);
                    }
                    const externalSheetId = externalSheetIds.get(value);
                    if (externalSheetId !== undefined) {
                        const externalUrl = `https://docs.google.com/spreadsheets/d/${externalBcConfig.spreadsheetId}/edit#gid=${externalSheetId}`;
                        row.push(`=HYPERLINK("${externalUrl}", "${value}")`);
                        continue;
                    }
                }
            }
            row.push(value);
            continue;
        }
        row.push(value || '');
    }
    return row;
}
async function getSheetIdMap(api, spreadsheetId) {
    const spreadsheet = await api.spreadsheets.get({ spreadsheetId });
    const map = new Map();
    for (const sheet of spreadsheet.data.sheets || []) {
        const title = sheet.properties?.title;
        const sheetId = sheet.properties?.sheetId;
        if (title && sheetId !== null && sheetId !== undefined) {
            map.set(title, sheetId);
        }
    }
    return map;
}
async function updateModuleSheet(api, spreadsheetId, schema, moduleSheetIds, hyperlinkContext) {
    const sheetName = schema.moduleName;
    await ensureSheetExists(api, spreadsheetId, sheetName);
    const headers = await readModuleHeaders(api, spreadsheetId, sheetName);
    if (headers.length === 0) {
        throw new Error(`No headers found in sheet '${sheetName}'. Please ensure TEMPLATE has headers in row 1.`);
    }
    const dataRows = [];
    for (const prop of schema.aggregateProperties || []) {
        const processedProp = createPropertyWithoutPivot(prop);
        dataRows.push(await mapPropertyToRow(processedProp, headers, moduleSheetIds, hyperlinkContext));
    }
    const lastColumn = String.fromCodePoint(64 + headers.length);
    await api.spreadsheets.values.clear({
        range: `'${sheetName}'!A2:${lastColumn}`,
        spreadsheetId,
    });
    if (dataRows.length > 0) {
        await api.spreadsheets.values.update({
            range: `'${sheetName}'!A2`,
            requestBody: { values: dataRows },
            spreadsheetId,
            valueInputOption: 'USER_ENTERED',
        });
        const moduleSheetId = await getSheetIdByName(api, spreadsheetId, sheetName);
        await api.spreadsheets.batchUpdate({
            requestBody: {
                requests: [
                    {
                        clearBasicFilter: { sheetId: moduleSheetId },
                    },
                    {
                        setBasicFilter: {
                            filter: {
                                range: {
                                    endColumnIndex: headers.length,
                                    endRowIndex: 1 + dataRows.length,
                                    sheetId: moduleSheetId,
                                    startColumnIndex: 0,
                                    startRowIndex: 0,
                                },
                            },
                        },
                    },
                ],
            },
            spreadsheetId,
        });
    }
}
async function getTemplateSheetId(api, spreadsheetId) {
    const spreadsheet = await api.spreadsheets.get({ spreadsheetId });
    const templateSheet = spreadsheet.data.sheets?.find((s) => s.properties?.title === TEMPLATE_SHEET_NAME);
    return templateSheet?.properties?.sheetId ?? null;
}
/** Ensure a sheet with `sheetName` exists, duplicating TEMPLATE when present, else creating empty. */
async function ensureSheetExists(api, spreadsheetId, sheetName) {
    const spreadsheet = await api.spreadsheets.get({ spreadsheetId });
    const exists = spreadsheet.data.sheets?.some((s) => s.properties?.title === sheetName);
    if (exists) {
        return;
    }
    const templateSheetId = await getTemplateSheetId(api, spreadsheetId);
    await (templateSheetId === null ? api.spreadsheets.batchUpdate({
        requestBody: {
            requests: [
                {
                    addSheet: {
                        properties: { title: sheetName },
                    },
                },
            ],
        },
        spreadsheetId,
    }) : api.spreadsheets.batchUpdate({
        requestBody: {
            requests: [
                {
                    duplicateSheet: {
                        insertSheetIndex: 1,
                        newSheetName: sheetName,
                        sourceSheetId: templateSheetId,
                    },
                },
            ],
        },
        spreadsheetId,
    }));
}
async function getSheetIdByName(api, spreadsheetId, sheetName) {
    const spreadsheet = await api.spreadsheets.get({ spreadsheetId });
    const sheet = spreadsheet.data.sheets?.find((s) => s.properties?.title === sheetName);
    return sheet?.properties?.sheetId || 0;
}
