import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Define the paths
const COMPONENTS_DIR = path.join(__dirname, '../../src/Components');
const SDK_OUTPUT_DIR = path.join(__dirname, '../../src/sdk/components/generated');
const COMPONENT_TEMPLATE_PATH = path.join(__dirname, './templates/Component.ts.tpl');
const INDEX_TEMPLATE_PATH = path.join(__dirname, './templates/index.ts.tpl');

console.log(COMPONENTS_DIR);
console.log(SDK_OUTPUT_DIR);
console.log(COMPONENT_TEMPLATE_PATH);
console.log(INDEX_TEMPLATE_PATH);

// Components to exclude from SDK generation
const EXCLUDED_COMPONENTS = ['APIEndpoint', 'ComponentHost'];

/**
 * Simple template engine that replaces {{variable}} with values
 */
function renderTemplate(templateContent, variables) {
    const warning = '//!!! DO NOT EDIT THIS FILE, IT IS AUTO-GENERATED !!!//';
    let result = templateContent;

    for (const [key, value] of Object.entries(variables)) {
        const placeholder = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
        result = result.replace(placeholder, value);
    }

    result = warning + '\n\n' + result;

    return result;
}

/**
 * Load template file
 */
function loadTemplate(templatePath) {
    try {
        return fs.readFileSync(templatePath, 'utf-8');
    } catch (error) {
        console.error(`Error loading template from ${templatePath}:`, error);
        throw error;
    }
}

/**
 * Find matching closing brace for an opening brace
 */
function findMatchingBrace(content, startIndex) {
    let braceCount = 1;
    let index = startIndex + 1;

    while (index < content.length && braceCount > 0) {
        const char = content[index];
        if (char === '{') {
            braceCount++;
        } else if (char === '}') {
            braceCount--;
        }
        index++;
    }

    return braceCount === 0 ? index - 1 : -1;
}

/**
 * Parse a JavaScript object string into a JavaScript object
 * This is a simplified parser that handles basic object structures
 */
function parseObjectString(objStr) {
    try {
        // Clean up the string and make it valid JSON-like
        let cleanStr = objStr
            .replace(/(\w+):/g, '"$1":') // Quote property names
            .replace(/'/g, '"') // Convert single quotes to double quotes
            .replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas
            .replace(/:\s*true/g, ': true')
            .replace(/:\s*false/g, ': false');

        // Try to parse as JSON first
        try {
            return JSON.parse('{' + cleanStr + '}');
        } catch (e) {
            // If JSON parsing fails, use eval (less safe but more flexible)
            // Only use this for trusted code
            return eval('({' + objStr + '})');
        }
    } catch (error) {
        console.warn('Failed to parse object string:', objStr.substring(0, 100) + '...');
        return {};
    }
}

/**
 * Extract Joi configSchema from a component class file and convert to settings format
 */
function extractJoiConfigSchema(filePath) {
    try {
        const content = fs.readFileSync(filePath, 'utf-8');

        // Look for protected configSchema = Joi.object({ ... }) pattern
        const configSchemaStartMatch = content.match(/protected\s+configSchema\s*=\s*Joi\.object\s*\(\s*\{/);

        if (!configSchemaStartMatch) {
            return null;
        }

        const configSchemaStartIndex = configSchemaStartMatch.index + configSchemaStartMatch[0].length - 1;
        const configSchemaEndIndex = findMatchingBrace(content, configSchemaStartIndex);

        if (configSchemaEndIndex === -1) {
            console.warn('Could not find matching closing brace for configSchema');
            return null;
        }

        const configSchemaContent = content.substring(configSchemaStartIndex + 1, configSchemaEndIndex);

        // Parse the Joi schema content and convert to settings format
        const settings = parseJoiSchemaContentForSettings(configSchemaContent);

        return settings;
    } catch (error) {
        console.error(`Error extracting Joi configSchema from ${filePath}:`, error);
        return null;
    }
}

/**
 * Parse Joi schema content and convert to settings format
 */
function parseJoiSchemaContentForSettings(schemaContent) {
    const settings = {};

    const lines = schemaContent.split('\n');
    let currentField = '';
    let inMultilineField = false;
    let braceCount = 0;

    for (let line of lines) {
        line = line.trim();
        if (!line || line.startsWith('//')) continue;

        braceCount += (line.match(/\{/g) || []).length;
        braceCount -= (line.match(/\}/g) || []).length;

        if (inMultilineField) {
            currentField += ' ' + line;
            if (braceCount === 0 && (line.endsWith(',') || line.endsWith('}'))) {
                parseJoiFieldDefinitionForSettings(currentField, settings);
                currentField = '';
                inMultilineField = false;
            }
        } else {
            const fieldMatch = line.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*Joi\./);
            if (fieldMatch) {
                const fieldName = fieldMatch[1];
                if (fieldName.startsWith('_')) continue; // Skip internal fields

                if (braceCount > 0 || (!line.endsWith(',') && !line.endsWith('}'))) {
                    currentField = line;
                    inMultilineField = true;
                } else {
                    parseJoiFieldDefinitionForSettings(line, settings);
                }
            }
        }
    }

    return settings;
}

/**
 * Parse a single Joi field definition and convert to settings format
 */
function parseJoiFieldDefinitionForSettings(fieldDef, settings) {
    try {
        const fieldMatch = fieldDef.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*Joi\./);
        if (!fieldMatch) return;

        const fieldName = fieldMatch[1];

        const typeMatch = fieldDef.match(/Joi\.(\w+)/);
        const joiType = typeMatch ? typeMatch[1] : 'any';

        const isRequired = fieldDef.includes('.required()');

        const labelMatch = fieldDef.match(/\.label\(['"`]([^'"`]+)['"`]\)/);
        const label = labelMatch ? labelMatch[1] : undefined;

        const validMatch = fieldDef.match(/\.valid\(['"`]?([^)]+)['"`]?\)/);
        let validValues = null;
        if (validMatch) {
            validValues = validMatch[1].split(',').map((v) => v.trim().replace(/['"`]/g, ''));
        }

        settings[fieldName] = {
            type: mapJoiTypeToSchemaType(joiType),
            required: isRequired,
            description: label,
            label: label,
            valid: validValues,
        };
    } catch (error) {
        console.warn('Failed to parse Joi field definition:', fieldDef.substring(0, 100));
    }
}

/**
 * Map Joi types to our schema types
 */
function mapJoiTypeToSchemaType(joiType) {
    switch (joiType) {
        case 'string':
            return 'Text';
        case 'number':
            return 'Number';
        case 'boolean':
            return 'Boolean';
        case 'object':
            return 'Object';
        case 'array':
            return 'Array';
        case 'binary':
            return 'Binary';
        case 'any':
            return 'Any';
        default:
            return 'Any';
    }
}

/**
 * Extract schema from a component class file using improved parsing
 * Flow: Extract legacy schema (inputs + settings), then override settings with Joi configSchema
 */
function extractSchemaFromFile(filePath) {
    try {
        const content = fs.readFileSync(filePath, 'utf-8');
        const fileName = path.basename(filePath, '.class.ts');

        let componentName = fileName;
        let inputs = {};
        let outputs = {};
        let settings = {};

        // First, try to extract legacy schema field
        const schemaStartMatch = content.match(/protected\s+schema\s*=\s*\{/);

        if (schemaStartMatch) {
            // Use existing logic for legacy schema
            const schemaStartIndex = schemaStartMatch.index + schemaStartMatch[0].length - 1;
            const schemaEndIndex = findMatchingBrace(content, schemaStartIndex);

            if (schemaEndIndex !== -1) {
                const schemaContent = content.substring(schemaStartIndex + 1, schemaEndIndex);
                const schemaObj = parseObjectString(schemaContent);

                if (schemaObj.name) {
                    componentName = schemaObj.name;

                    // Extract and normalize inputs from legacy schema
                    if (schemaObj.inputs) {
                        for (const [inputName, inputDef] of Object.entries(schemaObj.inputs)) {
                            inputs[inputName] = {
                                type: inputDef.type || 'Any',
                                optional: inputDef.optional || false,
                                default: inputDef.default || false,
                                description: inputDef.description,
                            };
                        }
                    }

                    // Extract and normalize outputs from legacy schema
                    if (schemaObj.outputs) {
                        for (const [outputName, outputDef] of Object.entries(schemaObj.outputs)) {
                            outputs[outputName] = {
                                ...outputDef,
                            };
                        }
                    }

                    // Extract settings from legacy schema
                    if (schemaObj.settings) {
                        settings = { ...schemaObj.settings };
                    }
                }
            }
        }

        // Then, try to extract Joi configSchema to override settings
        const joiSettings = extractJoiConfigSchema(filePath);
        if (joiSettings) {
            // Override settings with Joi configSchema
            settings = { ...settings, ...joiSettings };
        }

        // Only return a schema if we have either inputs, outputs, or meaningful settings
        const hasInputsOrOutputs = (inputs && Object.keys(inputs).length > 0) || (outputs && Object.keys(outputs).length > 0);

        const hasMeaningfulSettings = joiSettings && Object.keys(joiSettings).length > 0;

        if (hasInputsOrOutputs || hasMeaningfulSettings) {
            return {
                name: componentName,
                inputs,
                outputs,
                settings,
            };
        }

        return null;
    } catch (error) {
        console.error(`Error extracting schema from ${filePath}:`, error);
        return null;
    }
}

/**
 * Map schema type to TypeScript type
 */
function mapSchemaTypeToTS(schemaType) {
    switch (schemaType) {
        case 'Text':
        case 'string':
            return 'string';
        case 'Number':
        case 'number':
            return 'number';
        case 'Boolean':
        case 'boolean':
            return 'boolean';
        case 'Object':
        case 'object':
            return 'object';
        case 'Array':
        case 'array':
            return 'any[]';
        case 'Binary':
        case 'binary':
            return 'ArrayBuffer | Uint8Array | string';
        case 'Any':
        case 'any':
        default:
            return 'any';
    }
}

/**
 * Generate TypeScript interface for inputs
 */
function generateInputsType(componentName, inputs) {
    if (!inputs || Object.keys(inputs).length === 0) {
        return `export type T${componentName}Inputs = {
    [key: string]: any;
};`;
    }

    const inputsEntries = Object.entries(inputs);
    const properties = inputsEntries.map(([inputName, inputDef]) => {
        const tsType = mapSchemaTypeToTS(inputDef.type);

        // Add JSDoc comment if description exists
        let jsDocComment = '';
        if (inputDef.description) {
            jsDocComment = `    /** ${inputDef.description} */\n`;
        }

        // All inputs are optional in the type
        return `${jsDocComment}    ${inputName}?: ${tsType};`;
    });

    return `export type T${componentName}Inputs = {
${properties.join('\n')}
    [key: string]: any;
};`;
}

/**
 * Generate TypeScript interface for outputs
 */
function generateOutputsType(componentName, outputs) {
    if (!outputs || Object.keys(outputs).length === 0) {
        return `export type T${componentName}Outputs = {
    [key: string]: any;
};`;
    }

    const outputsEntries = Object.entries(outputs);
    const properties = outputsEntries.map(([outputName, outputDef]) => {
        // Add JSDoc comment if description exists
        let jsDocComment = '';
        if (outputDef.description) {
            jsDocComment = `    /** ${outputDef.description} */\n`;
        }

        // All outputs are typed as any for now, but can be extended later
        return `${jsDocComment}    ${outputName}: any;`;
    });

    return `export type T${componentName}Outputs = {
${properties.join('\n')}
    [key: string]: any;
};`;
}

/**
 * Generate TypeScript interface for settings
 */
function generateSettingsType(componentName, settings) {
    if (!settings || Object.keys(settings).length === 0) {
        return `export interface T${componentName}Settings {
    name?: string;
}`;
    }

    const settingsEntries = Object.entries(settings);
    const properties = settingsEntries.map(([settingName, settingDef]) => {
        const tsType = mapSchemaTypeToTS(settingDef.type);
        const isRequired = settingDef.required === true;
        const optional = isRequired ? '' : '?';

        // Add JSDoc comment if description or label exists
        let jsDocComment = '';
        const description = settingDef.description || settingDef.label;
        if (description) {
            jsDocComment = `    /** ${description} */\n`;
        }

        // Handle union types for valid values
        let finalType = tsType;
        if (settingDef.valid && Array.isArray(settingDef.valid)) {
            finalType = settingDef.valid.map((val) => `'${val}'`).join(' | ');
        }

        return `${jsDocComment}    ${settingName}${optional}: ${finalType};`;
    });

    // Only add default name field if there's no 'name' field in the settings
    const hasNameField = settings.hasOwnProperty('name');
    const defaultNameField = hasNameField ? '' : '    name?: string;\n';

    return `export interface T${componentName}Settings {
${defaultNameField}${properties.join('\n')}
}`;
}

/**
 * Generate template variables for a component schema
 */
function generateTemplateVariables(schema) {
    const { name, inputs = {}, outputs = {}, settings = {} } = schema;

    // Generate settings type
    const settingsType = generateSettingsType(name, settings);

    // Generate inputs type
    const inputsType = generateInputsType(name, inputs);

    // Generate outputs type
    const outputsType = generateOutputsType(name, outputs);

    // Generate outputs object
    const outputEntries = Object.keys(outputs);
    const outputsCode =
        outputEntries.length > 0
            ? outputEntries
                  .map(
                      (outputName) =>
                          `        ${outputName}: createSafeAccessor({}, component, '${outputName}', ${JSON.stringify(outputs[outputName])}),`
                  )
                  .join('\n')
            : '        // No outputs defined';

    // Generate inputs object
    const inputEntries = Object.keys(inputs);
    const inputsCode =
        inputEntries.length > 0
            ? inputEntries
                  .map((inputName) => {
                      const input = inputs[inputName];
                      return `        ${inputName}: {
            component,
            type: '${input.type}',
            optional: ${input.optional || false},
            default: ${input.default || false},
        },`;
                  })
                  .join('\n')
            : '        // No inputs defined';

    return {
        componentName: name,
        settingsType,
        inputsType,
        outputsType,
        outputsCode,
        inputsCode,
    };
}

/**
 * Generate SDK component code using template
 */
function generateSDKComponent(schema, template) {
    const variables = generateTemplateVariables(schema);
    return renderTemplate(template, variables);
}

/**
 * Generate index file using template
 */
function generateIndexFile(componentNames, template) {
    const imports = componentNames.map((name) => `import { ${name} } from './${name}';`).join('\n');

    const exports = `const Components = { ${componentNames.map((name) => `${name},`).join(' ')} };\n export default Components;`;

    return renderTemplate(template, { imports, exports });
}

/**
 * Recursively find all component files in a directory
 */
function findComponentFilesRecursively(dir) {
    const componentFiles = [];
    let totalFiles = 0;
    let skippedFiles = 0;

    function searchDirectory(currentDir) {
        const items = fs.readdirSync(currentDir, { withFileTypes: true });

        for (const item of items) {
            const fullPath = path.join(currentDir, item.name);

            if (item.isDirectory()) {
                // Recursively search subdirectories
                searchDirectory(fullPath);
            } else if (item.isFile() && item.name.endsWith('.class.ts') && item.name !== 'Component.class.ts') {
                totalFiles++;
                // Check if the file actually extends Component
                try {
                    const fileContent = fs.readFileSync(fullPath, 'utf-8');
                    if (fileContent.includes('extends Component')) {
                        componentFiles.push(fullPath);
                    } else {
                        skippedFiles++;
                    }
                } catch (error) {
                    // If we can't read the file, skip it gracefully
                    skippedFiles++;
                    console.warn(`Could not read file ${fullPath}: ${error.message}`);
                }
            }
        }
    }

    searchDirectory(dir);
    if (skippedFiles > 0) {
        console.log(`📊 Found ${componentFiles.length} components, skipped ${skippedFiles} non-component .class.ts files`);
    }
    return componentFiles;
}

/**
 * Main generation function
 */
async function generateSDKComponents() {
    //console.log('🚀 Starting SDK component generation...');

    // Load templates
    //console.log('📄 Loading templates...');
    const componentTemplate = loadTemplate(COMPONENT_TEMPLATE_PATH);
    const indexTemplate = loadTemplate(INDEX_TEMPLATE_PATH);
    //console.log('✅ Templates loaded successfully');

    // Ensure output directory exists
    if (!fs.existsSync(SDK_OUTPUT_DIR)) {
        fs.mkdirSync(SDK_OUTPUT_DIR, { recursive: true });
    }

    // Clear existing generated files
    const existingFiles = fs.readdirSync(SDK_OUTPUT_DIR);
    for (const file of existingFiles) {
        const filePath = path.join(SDK_OUTPUT_DIR, file);
        if (fs.statSync(filePath).isFile()) {
            fs.unlinkSync(filePath);
        }
    }

    // Get all component files
    const componentFiles = findComponentFilesRecursively(COMPONENTS_DIR);

    const generatedComponents = [];
    const skippedComponents = new Set();

    const schemas = [];
    process.stdout.write(`📝 Reading Schemas `);
    for (const filePath of componentFiles) {
        const fileName = path.basename(filePath, '.class.ts');

        const schema = extractSchemaFromFile(filePath);
        if (!schema || !schema.name) {
            process.stdout.write(`!`);
            skippedComponents.add(fileName);
            continue;
        } else {
            process.stdout.write(`.`);
        }
        schemas.push(schema);
    }

    process.stdout.write(`\n📝 Processing schemas `);
    for (const schema of schemas) {
        process.stdout.write(`.`);

        // Skip excluded components
        if (EXCLUDED_COMPONENTS.includes(schema.name)) {
            process.stdout.write(`!`);
            skippedComponents.add(schema.name);
            continue;
        }

        const hasInputsOrOutputs =
            (schema.inputs && Object.keys(schema.inputs).length > 0) || (schema.outputs && Object.keys(schema.outputs).length > 0);

        const hasMeaningfulSettings = schema.settings && Object.keys(schema.settings).length > 0;

        // Generate component if it has inputs/outputs OR meaningful settings (from Joi configSchema)
        if (!hasInputsOrOutputs && !hasMeaningfulSettings) {
            process.stdout.write(`!`);
            skippedComponents.add(schema.name);
            continue;
        }

        try {
            const sdkCode = generateSDKComponent(schema, componentTemplate);
            const outputPath = path.join(SDK_OUTPUT_DIR, `${schema.name}.ts`);

            fs.writeFileSync(outputPath, sdkCode);
            generatedComponents.push(schema.name);

            process.stdout.write(`.`);
        } catch (error) {
            process.stdout.write(`!`);
            skippedComponents.add(schema.name);
        }
    }

    // Generate index file
    if (generatedComponents.length > 0) {
        const indexContent = generateIndexFile(generatedComponents, indexTemplate);
        fs.writeFileSync(path.join(SDK_OUTPUT_DIR, 'index.generated.ts'), indexContent);
        process.stdout.write(`.`);
        //console.log('✅ Generated index.ts');
    }

    process.stdout.write(`\n`);
    // Summary

    if (generatedComponents.length > 0) {
        process.stdout.write(`✅ Generated ${generatedComponents.length} components `);
    }

    if (skippedComponents.size > 0) {
        process.stdout.write(`⚠️ Skipped ${skippedComponents.size} components (missing schema)`);
        const skippedList = Array.from(skippedComponents).sort();
        process.stdout.write(`\n📝 Skipped components (${skippedList.length}): ${skippedList.join(', ')}`);
    }

    process.stdout.write(`\n`);
}

// Run the generator
generateSDKComponents().catch(console.error);
