import fs from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { Option } from 'commander';
import chalk from 'chalk';
import { PrettyPrint } from '../output/pretty_print.js';

const API_DOCS_BASE_URL = 'https://api.openrainbow.org';

/**
 * ApiDocs — charge et indexe les fichiers OpenAPI/Swagger du répertoire api-docs/
 * pour permettre la recherche textuelle dans les endpoints Rainbow.
 */
export class ApiDocs {
    constructor(program, logger, services) {
        this.logger = logger;
        this.services = services;
        services['apidocs'] = this;

        // Index plat de tous les endpoints
        // Chaque entrée : { portal, method, path, summary, description, tags, operationId, permissions, searchText }
        this.index = [];
        // Specs brutes par portail (pour get_endpoint_details)
        this.specs = {};
        // Métadonnées par portail { title, version, description }
        this.portals = {};

        this.logger.debug('ApiDocs.constructor');

        // --- Commandes CLI ---

        const apiDocsCmd = program
            .command('api-docs')
            .description('Search and browse Rainbow REST API documentation');

        apiDocsCmd
            .command('search')
            .argument('<query...>', 'search keywords (e.g., "add user room")')
            .description('Search API endpoints by keywords')
            .addOption(new Option('-p, --portal <portal>', 'filter by portal name (e.g., admin, enduser, room)'))
            .addOption(new Option('-m, --method <method>', 'filter by HTTP method (GET, POST, PUT, DELETE)'))
            .addOption(new Option('-l, --limit <n>', 'max results to return').default(15))
            .addOption(new Option('-j, --json', 'output raw JSON'))
            .action(async (query, options) => {
                this.services['telemetry']?.sendTelemetry('api-docs', 'search', '');
                this._cmdSearch(query.join(' '), options);
            });

        apiDocsCmd
            .command('portals')
            .description('List all available API documentation portals')
            .addOption(new Option('-j, --json', 'output raw JSON'))
            .action(async (options) => {
                this.services['telemetry']?.sendTelemetry('api-docs', 'portals', '');
                this._cmdPortals(options);
            });

        apiDocsCmd
            .command('endpoint')
            .argument('<portal>', 'portal name (e.g., admin, enduser, room)')
            .argument('<method>', 'HTTP method (GET, POST, PUT, DELETE)')
            .argument('<path>', 'full API path (e.g., /api/rainbow/room/v1.0/rooms/{roomId}/users)')
            .description('Get full details of a specific API endpoint')
            .addOption(new Option('-j, --json', 'output raw JSON'))
            .action(async (portal, method, endpointPath, options) => {
                this.services['telemetry']?.sendTelemetry('api-docs', 'endpoint', '');
                this._cmdEndpoint(portal, method, endpointPath, options);
            });

        apiDocsCmd
            .command('browse')
            .argument('[portal]', 'browse a specific portal (e.g., admin, enduser)')
            .description('Interactive browser for API documentation')
            .action(async (portal, options) => {
                this.services['telemetry']?.sendTelemetry('api-docs', 'browse', '');
                await this._cmdBrowse(portal, options);
            });

        // Commande update — dev only (même pattern que services/debug.js)
        const devMode = existsSync(fileURLToPath(new URL('./debug.js', import.meta.url)));
        if (devMode) {
            apiDocsCmd
                .command('update')
                .description('[DEV] Download/update all Swagger specs from the Rainbow API documentation server')
                .addOption(new Option('--portal <portal>', 'update only a specific portal'))
                .action(async (options) => {
                    this.services['telemetry']?.sendTelemetry('api-docs', 'update', '');
                    await this._cmdUpdate(options);
                });
        }
    }

    // --- Affichage CLI ---

    _cmdSearch(query, options) {
        if (this.index.length === 0) {
            console.log(chalk.red('No API documentation loaded. Place OpenAPI JSON files in the api-docs/ directory.'));
            return;
        }

        const opts = {
            limit: parseInt(options.limit) || 15,
            portal: options.portal || null,
            method: options.method || null
        };
        const result = this.search(query, opts);

        if (options.json) {
            console.log(JSON.stringify(result, null, 2));
            return;
        }

        if (result.results.length === 0) {
            console.log(chalk.yellow('No matching endpoints found.'));
            return;
        }

        console.log('');
        for (const r of result.results) {
            const methodColor = { GET: 'green', POST: 'yellow', PUT: 'blue', DELETE: 'red', PATCH: 'magenta' }[r.method] || 'white';
            const version = this.portals[r.portal]?.version || '';
            console.log(
                `  ${chalk[methodColor].bold(r.method.padEnd(7))} ${chalk.white(r.path)}`
            );
            console.log(
                `  ${chalk.dim(r.portal.padEnd(7))} ${r.summary}  ${chalk.dim('v' + version)}`
            );
            if (r.permissions.length > 0) {
                console.log(
                    `  ${' '.repeat(7)} ${chalk.dim('roles: ' + r.permissions.join(', '))}`
                );
            }
            console.log('');
        }

        if (result.total > result.results.length) {
            console.log(chalk.dim(`  Showing ${result.results.length} of ${result.total} results. Use --limit to see more.`));
            console.log('');
        }
    }

    _cmdPortals(options) {
        if (this.index.length === 0) {
            console.log(chalk.red('No API documentation loaded. Place OpenAPI JSON files in the api-docs/ directory.'));
            return;
        }

        const portals = this.listPortals();

        if (options.json) {
            console.log(JSON.stringify({ portals, totalEndpoints: this.index.length }, null, 2));
            return;
        }

        // Compter les endpoints par portail
        const counts = {};
        for (const entry of this.index) {
            counts[entry.portal] = (counts[entry.portal] || 0) + 1;
        }

        console.log('');
        console.log(chalk.bold(`  ${portals.length} portals, ${this.index.length} endpoints`));
        console.log('');

        for (const p of portals) {
            const count = counts[p.name] || 0;
            console.log(
                `  ${chalk.cyan(p.name.padEnd(28))} ${chalk.dim('v' + p.version.padEnd(10))} ${chalk.dim(String(count).padStart(4) + ' endpoints')}  ${p.title}`
            );
        }
        console.log('');
    }

    _cmdEndpoint(portal, method, endpointPath, options) {
        if (this.index.length === 0) {
            console.log(chalk.red('No API documentation loaded. Place OpenAPI JSON files in the api-docs/ directory.'));
            return;
        }

        const details = this.getEndpointDetails(portal, method, endpointPath);

        if (details.error) {
            console.log(chalk.red(details.error));
            return;
        }

        if (options.json) {
            console.log(JSON.stringify(details, null, 2));
            return;
        }

        const methodColor = { GET: 'green', POST: 'yellow', PUT: 'blue', DELETE: 'red', PATCH: 'magenta' }[details.method] || 'white';

        console.log('');
        console.log(`  ${chalk[methodColor].bold(details.method)} ${chalk.white.bold(details.path)}`);
        const portalVersion = this.portals[details.portal]?.version || '';
        console.log(`  ${chalk.dim('Portal:')} ${details.portal} (${details.portalTitle} v${portalVersion})`);
        console.log(`  ${chalk.dim('Summary:')} ${details.summary}`);
        if (details.tags.length > 0) {
            console.log(`  ${chalk.dim('Tags:')} ${details.tags.join(', ')}`);
        }
        if (details.permissions.length > 0) {
            console.log(`  ${chalk.dim('Permissions:')} ${details.permissions.join(', ')}`);
        }
        if (details.operationId) {
            console.log(`  ${chalk.dim('Operation ID:')} ${details.operationId}`);
        }
        console.log('');

        if (details.description) {
            console.log(`  ${chalk.bold('Description')}`);
            for (const line of details.description.split('\n')) {
                console.log(`  ${line}`);
            }
            console.log('');
        }

        if (details.parameters?.length > 0) {
            console.log(`  ${chalk.bold('Parameters')}`);
            for (const p of details.parameters) {
                const req = p.required ? chalk.red('*') : ' ';
                const enumStr = p.enum ? chalk.dim(` [${p.enum.join('|')}]`) : '';
                console.log(`  ${req} ${chalk.cyan(p.name)} ${chalk.dim('(' + p.in + ', ' + p.type + ')')}${enumStr}`);
                if (p.description) {
                    console.log(`    ${chalk.dim(p.description)}`);
                }
            }
            console.log('');
        }

        if (details.requestBody) {
            console.log(`  ${chalk.bold('Request Body')} ${details.requestBody.required ? chalk.red('(required)') : chalk.dim('(optional)')}`);
            this._printSchema(details.requestBody.schema, 2);
            console.log('');
        }

        if (details.responses) {
            console.log(`  ${chalk.bold('Responses')}`);
            for (const [code, resp] of Object.entries(details.responses)) {
                const codeColor = code.startsWith('2') ? 'green' : code.startsWith('4') ? 'yellow' : code.startsWith('5') ? 'red' : 'white';
                console.log(`  ${chalk[codeColor].bold(code)} ${resp.description}`);
                if (resp.schema?.properties) {
                    this._printSchema(resp.schema, 2, true);
                }
            }
            console.log('');
        }
    }

    _printSchema(schema, indent = 2, compact = false) {
        if (!schema?.properties) return;
        const pad = ' '.repeat(indent);
        const required = new Set(schema.required || []);
        for (const [name, prop] of Object.entries(schema.properties)) {
            const req = required.has(name) ? chalk.red('*') : ' ';
            const enumStr = prop.enum ? chalk.dim(` [${prop.enum.join('|')}]`) : '';
            console.log(`${pad}${req} ${chalk.cyan(name)} ${chalk.dim('(' + (prop.type || 'object') + ')')}${enumStr}`);
            if (!compact && prop.description) {
                console.log(`${pad}  ${chalk.dim(prop.description)}`);
            }
            if (prop.properties) {
                this._printSchema(prop, indent + 4, compact);
            }
        }
    }

    // --- Browser TUI ---

    async _cmdBrowse(portalFilter, options) {
        if (this.index.length === 0) {
            console.log(chalk.red('No API documentation loaded. Place OpenAPI JSON files in the api-docs/ directory.'));
            return;
        }

        if (portalFilter && !this.portals[portalFilter]) {
            console.log(chalk.red(`Portal "${portalFilter}" not found.`));
            console.log(chalk.dim(`Available portals: ${Object.keys(this.portals).sort().join(', ')}`));
            return;
        }

        const blessed = (await import('blessed')).default;
        const { ApiDocsBrowser } = await import('../output/ApiDocsBrowser.js');

        // Récupérer les infos user pour pré-remplir companyId / userId dans les formulaires
        const defaults = {};
        let connectedUser = null;
        try {
            const me = await this.services['enduser'].whoami();
            const d = me?.data;
            if (d?.companyId) defaults.companyId = d.companyId;
            if (d?.id)        defaults.userId    = d.id;
            if (d) {
                const name = [d.firstName, d.lastName].filter(Boolean).join(' ') || d.loginEmail || d.id;
                const roles = (d.roles || []).filter(r => r !== 'user'); // 'user' est le rôle de base, peu utile à afficher

                let companyName = null;
                if (d.companyId && this.services['admin']) {
                    try {
                        const company = await this.services['admin'].getCompany(d.companyId, {});
                        companyName = company?.data?.name || null;
                    } catch { /* ignore */ }
                }

                let host = null;
                try {
                    const creds = await this.services['configManager'].getFullCredentials();
                    host = creds?.host ?? null;
                } catch { /* ignore */ }
                connectedUser = { name, roles, companyName, host };
            }
        } catch { /* ignore : defaults vides si whoami échoue */ }

        const screen = blessed.screen({
            smartCSR: true,
            title: 'Rainbow API Documentation Browser'
        });

        const treeData = this.buildTreeData(portalFilter);

        const browserOptions = {
            screen,
            treeData,
            defaults,
            connectedUser,
            getEndpointDetails: (portal, method, endpointPath) => this.getEndpointDetails(portal, method, endpointPath),
            executeEndpoint: (portal, method, endpointPath, pathParams, queryParams, bodyStr) =>
                this.executeEndpoint(portal, method, endpointPath, pathParams, queryParams, bodyStr)
        };

        // Mode portail unique : activer la navigation par version
        if (portalFilter) {
            browserOptions.portal = portalFilter;
            browserOptions.getVersions = () => this.getArchivedVersions(portalFilter);
            browserOptions.switchVersion = async (portal, version) => {
                await this.switchPortalVersion(portal, version);
                return this.buildTreeData(portal);
            };
        }

        const browser = new ApiDocsBrowser(browserOptions);

        screen.render();
    }

    /**
     * Construit les donnees d'arbre pour le browser TUI.
     * Structure : Portal → Tag → Endpoint
     */
    buildTreeData(portalFilter = null) {
        const methodColors = { GET: 'green', POST: 'yellow', PUT: 'blue', DELETE: 'red', PATCH: 'magenta' };

        // Grouper par portail
        const byPortal = {};
        for (const entry of this.index) {
            if (portalFilter && entry.portal !== portalFilter) continue;
            if (!byPortal[entry.portal]) byPortal[entry.portal] = [];
            byPortal[entry.portal].push(entry);
        }

        const portalChildren = {};

        for (const portalName of Object.keys(byPortal).sort()) {
            const endpoints = byPortal[portalName];
            const portalInfo = this.portals[portalName];

            // Grouper par premier tag
            const byTag = {};
            for (const ep of endpoints) {
                const tag = ep.tags[0] || 'Other';
                if (!byTag[tag]) byTag[tag] = [];
                byTag[tag].push(ep);
            }

            const tagChildren = {};
            for (const tag of Object.keys(byTag).sort()) {
                const tagEndpoints = byTag[tag];
                const epChildren = {};

                tagEndpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));

                for (const ep of tagEndpoints) {
                    const color = methodColors[ep.method] || 'white';
                    // Echapper les accolades dans les paths pour blessed
                    const displayPath = ep.path.replace(/\{/g, '(').replace(/\}/g, ')');
                    const label = `{${color}-fg}${ep.method.padEnd(7)}{/${color}-fg} ${displayPath}`;

                    epChildren[label] = {
                        name: label,
                        portal: ep.portal,
                        method: ep.method,
                        endpointPath: ep.path
                    };
                }

                tagChildren[tag] = {
                    name: tag,
                    extended: false,
                    children: epChildren
                };
            }

            const portalLabel = `${portalName} (${portalInfo?.title || portalName} v${portalInfo?.version || '?'})`;

            portalChildren[portalLabel] = {
                name: portalLabel,
                extended: false,
                children: tagChildren
            };
        }

        // Si filtre sur un portail, aplatir la hierarchie
        if (portalFilter) {
            const portalKey = Object.keys(portalChildren)[0];
            if (!portalKey) {
                return { name: `Portal "${portalFilter}" not found`, extended: true, children: {} };
            }
            const portalNode = portalChildren[portalKey];
            return {
                name: portalNode.name,
                extended: true,
                children: portalNode.children
            };
        }

        return {
            name: 'API Documentation',
            extended: true,
            children: portalChildren
        };
    }

    // --- Mise à jour des specs (dev only) ---

    async _cmdUpdate(options) {
        this.logger.debug('ApiDocs._cmdUpdate');

        // Demander les credentials interactivement
        let input, password;
        try {
            const { default: _input }    = await import('@inquirer/input');
            const { default: _password } = await import('@inquirer/password');
            input = _input; password = _password;
        } catch (err) {
            PrettyPrint.error('Could not load interactive prompt.');
            return;
        }

        const username = await input({ message: 'Username for api.openrainbow.org:', default: 'demo' });
        const pwd = await password({ message: 'Password:', mask: '*' });

        if (!username || !pwd) {
            PrettyPrint.error('Credentials required.');
            return;
        }

        const authHeader = 'Basic ' + Buffer.from(`${username}:${pwd}`).toString('base64');

        // Étape 1 : découvrir les portails depuis la page principale
        console.log('');
        console.log(chalk.bold('  Fetching portal list from api.openrainbow.org...'));

        let portalNames;
        try {
            portalNames = await this._discoverPortals(authHeader);
        } catch (err) {
            PrettyPrint.error(`Failed to fetch portal list: ${err.message}`);
            return;
        }

        if (options.portal) {
            if (!portalNames.includes(options.portal)) {
                PrettyPrint.error(`Portal "${options.portal}" not found. Available: ${portalNames.join(', ')}`);
                return;
            }
            portalNames = [options.portal];
        }

        console.log(chalk.dim(`  Found ${portalNames.length} portals with Swagger documentation`));
        console.log('');

        // Étape 2 : télécharger chaque spec
        const docsDir = fileURLToPath(new URL('../api-docs/', import.meta.url));
        try {
            await fs.mkdir(docsDir, { recursive: true });
        } catch { /* ignore */ }

        let updated = 0;
        let failed = 0;
        let skipped = 0;

        for (const portal of portalNames) {
            const label = portal.padEnd(28);
            try {
                const spec = await this._downloadSpec(portal, authHeader);
                if (!spec) {
                    console.log(`  ${chalk.yellow('SKIP')}  ${label} ${chalk.dim('no swagger.json or openapi.json')}`);
                    skipped++;
                    continue;
                }

                const filePath = path.join(docsDir, `${portal}.json`);
                const archived = await this._archiveCurrentSpec(portal, docsDir, spec.info?.version);
                await fs.writeFile(filePath, JSON.stringify(spec, null, 2), 'utf-8');

                const endpointCount = Object.values(spec.paths || {}).reduce(
                    (sum, methods) => sum + Object.keys(methods).filter(m => !['parameters', 'servers'].includes(m)).length, 0
                );
                const archiveNote = archived ? chalk.dim(` [archived v${archived}]`) : '';
                console.log(`  ${chalk.green('OK')}    ${label} ${chalk.dim(endpointCount + ' endpoints')}  ${spec.info?.title || ''}${archiveNote}`);
                updated++;
            } catch (err) {
                console.log(`  ${chalk.red('FAIL')}  ${label} ${chalk.dim(err.message)}`);
                failed++;
            }
        }

        console.log('');
        console.log(chalk.bold(`  Done: ${updated} updated, ${skipped} skipped, ${failed} failed`));
        console.log('');
    }

    /**
     * Découvre la liste des portails ayant un swagger depuis la page principale.
     */
    async _discoverPortals(authHeader) {
        const res = await fetch(`${API_DOCS_BASE_URL}/`, {
            headers: { 'Authorization': authHeader }
        });

        if (!res.ok) {
            if (res.status === 401) throw new Error('Authentication failed (401). Check credentials.');
            throw new Error(`HTTP ${res.status}`);
        }

        const html = await res.text();

        // Extraire les noms de portails depuis les liens Redoc
        // Pattern: href="/doc/api/{portal}/redoc-index.html"
        const regex = /href="\/doc\/api\/([^/]+)\/redoc-index\.html"/g;
        const portals = [];
        let match;
        while ((match = regex.exec(html)) !== null) {
            portals.push(match[1]);
        }
        return portals.sort();
    }

    /**
     * Télécharge le fichier swagger d'un portail.
     * Essaie swagger.json puis openapi.json en fallback.
     */
    async _downloadSpec(portal, authHeader) {
        for (const filename of ['swagger.json', 'openapi.json']) {
            const url = `${API_DOCS_BASE_URL}/doc/api/${portal}/${filename}`;
            const res = await fetch(url, {
                headers: { 'Authorization': authHeader }
            });

            if (res.ok) {
                return await res.json();
            }

            // 404 → essayer le suivant
            if (res.status === 404) continue;

            // Autre erreur → throw
            throw new Error(`HTTP ${res.status} for ${filename}`);
        }

        return null; // Aucun fichier trouvé
    }

    // --- Archivage des versions ---

    /** Chemin du répertoire d'archive d'un portail. */
    _archiveDir(portal) {
        return path.join(this.docsDir, 'archive', portal);
    }

    /**
     * Archive le fichier courant d'un portail avant qu'il soit écrasé par une mise à jour.
     * newVersion : version de la spec fraîchement téléchargée — si identique, on n'archive pas.
     * Retourne la version archivée (string) si une archive a été créée, null sinon.
     */
    async _archiveCurrentSpec(portal, docsDir, newVersion = null) {
        this.logger.debug(`ApiDocs._archiveCurrentSpec: ${portal}`);
        const currentFilePath = path.join(docsDir, `${portal}.json`);

        let currentSpec;
        try {
            const raw = await fs.readFile(currentFilePath, 'utf-8');
            currentSpec = JSON.parse(raw);
        } catch {
            return null; // Pas encore de fichier courant, rien à archiver
        }

        const version = currentSpec?.info?.version;
        if (!version) return null;

        // Même version que la nouvelle spec : rien de nouveau à archiver
        if (newVersion && version === newVersion) return null;

        const archiveDir = path.join(docsDir, 'archive', portal);
        await fs.mkdir(archiveDir, { recursive: true });

        const archivePath = path.join(archiveDir, `${version}.json`);
        // Ne pas écraser si cette version est déjà archivée
        try {
            await fs.access(archivePath);
            return null; // Déjà archivée
        } catch { /* n'existe pas, on continue */ }

        await fs.writeFile(archivePath, JSON.stringify(currentSpec, null, 2), 'utf-8');

        // Nettoyage : garder au maximum 5 versions archivées
        try {
            const entries = (await fs.readdir(archiveDir)).filter(f => f.endsWith('.json'));
            if (entries.length > 5) {
                const stats = await Promise.all(
                    entries.map(async f => ({ file: f, mtime: (await fs.stat(path.join(archiveDir, f))).mtimeMs }))
                );
                stats.sort((a, b) => a.mtime - b.mtime);
                for (const { file } of stats.slice(0, entries.length - 5)) {
                    await fs.unlink(path.join(archiveDir, file));
                }
            }
        } catch { /* ignore pruning errors */ }

        return version;
    }

    /**
     * Retourne la liste des versions disponibles pour un portail.
     * Format : [{ version, label, isCurrent }], "current" en premier.
     */
    async getArchivedVersions(portal) {
        this.logger.debug(`ApiDocs.getArchivedVersions: ${portal}`);
        const results = [];

        const currentVersion = this.portals[portal]?.version || '?';
        results.push({ version: 'current', label: `current (v${currentVersion})`, isCurrent: true });

        if (!this.docsDir) return results;

        const archiveDir = path.join(this.docsDir, 'archive', portal);
        let entries;
        try {
            entries = await fs.readdir(archiveDir);
        } catch {
            return results; // Pas encore d'archive
        }

        const jsonFiles = entries.filter(f => f.endsWith('.json'));
        if (jsonFiles.length === 0) return results;

        const stats = await Promise.all(
            jsonFiles.map(async f => ({
                version: path.basename(f, '.json'),
                mtime: (await fs.stat(path.join(archiveDir, f))).mtimeMs
            }))
        );
        stats.sort((a, b) => b.mtime - a.mtime); // Plus récent en premier

        for (const { version } of stats) {
            results.push({ version, label: `v${version}`, isCurrent: false });
        }
        return results;
    }

    /**
     * Charge une version archivée (ou la version courante) d'un portail
     * et re-indexe ce portail uniquement.
     */
    async switchPortalVersion(portal, version) {
        this.logger.debug(`ApiDocs.switchPortalVersion: ${portal} -> ${version}`);

        const filePath = version === 'current'
            ? path.join(this.docsDir, `${portal}.json`)
            : path.join(this.docsDir, 'archive', portal, `${version}.json`);

        const raw = await fs.readFile(filePath, 'utf-8');
        const spec = JSON.parse(raw);

        // Supprimer les entrées de l'index pour ce portail
        this.index = this.index.filter(e => e.portal !== portal);
        delete this.specs[portal];
        delete this.portals[portal];

        // Re-indexer avec la nouvelle spec
        this._indexSpec(portal, spec);
    }

    /**
     * Charge tous les fichiers .json du répertoire api-docs/ et construit l'index.
     */
    async load(docsDir) {
        this.logger.debug(`ApiDocs.load: ${docsDir}`);
        this.docsDir = docsDir;

        let files;
        try {
            files = await fs.readdir(docsDir);
        } catch (err) {
            this.logger.warn(`ApiDocs: répertoire api-docs/ introuvable (${docsDir}), fonctionnalité désactivée.`);
            return;
        }

        const jsonFiles = files.filter(f => f.endsWith('.json')).sort();
        this.logger.debug(`ApiDocs: ${jsonFiles.length} fichiers trouvés`);

        for (const file of jsonFiles) {
            const portalName = path.basename(file, '.json');
            try {
                const raw = await fs.readFile(path.join(docsDir, file), 'utf-8');
                const spec = JSON.parse(raw);
                this._indexSpec(portalName, spec);
            } catch (err) {
                this.logger.warn(`ApiDocs: impossible de charger ${file}: ${err.message}`);
            }
        }

        this.logger.debug(`ApiDocs: ${this.index.length} endpoints indexés dans ${Object.keys(this.portals).length} portails`);
    }

    /**
     * Indexe une spec OpenAPI.
     */
    _indexSpec(portalName, spec) {
        this.specs[portalName] = spec;

        this.portals[portalName] = {
            title: spec.info?.title || portalName,
            version: spec.info?.version || '?',
            description: spec.info?.description?.split('\n')[0] || ''
        };

        const paths = spec.paths || {};
        for (const [pathStr, methods] of Object.entries(paths)) {
            for (const [method, operation] of Object.entries(methods)) {
                if (method === 'parameters' || method === 'servers') continue; // Skip shared params

                const tags = operation.tags || [];
                const summary = operation.summary || '';
                const description = operation.description || '';
                const operationId = operation.operationId || '';
                const permissions = (operation['x-permissions'] || []).map(p => p.name || p);

                // Texte de recherche normalisé (lowercase, sans numérotation type "5 - ")
                const searchText = [
                    portalName,
                    method,
                    pathStr,
                    summary.replace(/^\d+\s*-\s*/, ''),
                    description,
                    operationId,
                    ...tags,
                    ...permissions
                ].join(' ').toLowerCase();

                this.index.push({
                    portal: portalName,
                    method: method.toUpperCase(),
                    path: pathStr,
                    summary: summary.replace(/^\d+\s*-\s*/, ''),
                    description,
                    tags,
                    operationId,
                    permissions,
                    searchText
                });
            }
        }
    }

    /**
     * Recherche des endpoints par mots-clés.
     * Retourne les N meilleurs résultats triés par pertinence.
     */
    search(query, { limit = 15, portal = null, method = null } = {}) {
        this.logger.debug(`ApiDocs.search: "${query}" (limit=${limit}, portal=${portal}, method=${method})`);

        if (this.index.length === 0) {
            return { results: [], total: 0, message: 'No API documentation loaded. Place OpenAPI JSON files in the api-docs/ directory.' };
        }

        // Tokeniser la requête
        const tokens = query.toLowerCase()
            .split(/[\s/{}_.,-]+/)
            .filter(t => t.length > 1);

        if (tokens.length === 0) {
            return { results: [], total: 0, message: 'Empty query.' };
        }

        // Filtrage puis scoring
        let candidates = this.index;
        if (portal) {
            candidates = candidates.filter(e => e.portal === portal.toLowerCase());
        }
        if (method) {
            candidates = candidates.filter(e => e.method === method.toUpperCase());
        }

        const scored = [];
        for (const entry of candidates) {
            let score = 0;
            for (const token of tokens) {
                // Pondération : summary et path valent plus que la description
                if (entry.summary.toLowerCase().includes(token)) score += 3;
                if (entry.path.toLowerCase().includes(token)) score += 3;
                if (entry.tags.some(t => t.toLowerCase().includes(token))) score += 2;
                if (entry.operationId.toLowerCase().includes(token)) score += 2;
                if (entry.permissions.some(p => p.toLowerCase().includes(token))) score += 2;
                if (entry.portal.toLowerCase().includes(token)) score += 1;
                if (entry.description.toLowerCase().includes(token)) score += 1;
            }
            if (score > 0) {
                scored.push({ ...entry, score });
            }
        }

        // Tri par score décroissant, puis par portail + path
        scored.sort((a, b) => b.score - a.score || a.portal.localeCompare(b.portal) || a.path.localeCompare(b.path));

        const results = scored.slice(0, limit).map(({ searchText, score, ...rest }) => rest);
        return { results, total: scored.length };
    }

    /**
     * Retourne les détails complets d'un endpoint spécifique.
     */
    getEndpointDetails(portal, method, endpointPath) {
        this.logger.debug(`ApiDocs.getEndpointDetails: ${method} ${endpointPath} (${portal})`);

        const spec = this.specs[portal];
        if (!spec) {
            return { error: `Portal "${portal}" not found. Available: ${Object.keys(this.portals).join(', ')}` };
        }

        const pathObj = spec.paths?.[endpointPath];
        if (!pathObj) {
            return { error: `Path "${endpointPath}" not found in portal "${portal}".` };
        }

        const operation = pathObj[method.toLowerCase()];
        if (!operation) {
            return { error: `Method ${method} not found for path "${endpointPath}" in portal "${portal}".` };
        }

        // Construire la réponse détaillée
        const details = {
            portal,
            portalTitle: this.portals[portal]?.title,
            method: method.toUpperCase(),
            path: endpointPath,
            summary: (operation.summary || '').replace(/^\d+\s*-\s*/, ''),
            description: operation.description || '',
            operationId: operation.operationId || '',
            tags: operation.tags || [],
            permissions: (operation['x-permissions'] || []).map(p => p.name || p),
        };

        // Paramètres (path, query, header)
        if (operation.parameters?.length > 0) {
            details.parameters = operation.parameters
                .filter(p => {
                    if (p.in === 'body' || p.in === 'formData') return false;
                    if (p.in === 'header' && ['accept', 'content-type'].includes(p.name?.toLowerCase())) return false;
                    return true;
                })
                .map(p => ({
                    name: p.name,
                    in: p.in,
                    required: p.required || false,
                    description: p.description || '',
                    // OpenAPI 3.0 : p.schema.type / Swagger 2.0 : p.type
                    type: p.schema?.type || p.type || 'string',
                    enum: p.schema?.enum || p.enum || undefined
                }));

            // Swagger 2.0 : body param dans parameters (in: 'body')
            const swagger2Body = operation.parameters.find(p => p.in === 'body');
            if (swagger2Body?.schema && !operation.requestBody) {
                details.requestBody = {
                    required: swagger2Body.required || false,
                    schema: this._resolveSchema(spec, swagger2Body.schema)
                };
            }
        }

        // Request body OpenAPI 3.0
        if (operation.requestBody) {
            const content = operation.requestBody.content?.['application/json'];
            if (content?.schema) {
                details.requestBody = {
                    required: operation.requestBody.required || false,
                    schema: this._resolveSchema(spec, content.schema)
                };
            }
        }

        // Réponses — résumé des codes et descriptions
        if (operation.responses) {
            details.responses = {};
            for (const [code, resp] of Object.entries(operation.responses)) {
                const respInfo = { description: resp.description || '' };
                const respContent = resp.content?.['application/json'];
                if (respContent?.schema) {
                    respInfo.schema = this._resolveSchema(spec, respContent.schema);
                }
                details.responses[code] = respInfo;
            }
        }

        return details;
    }

    /**
     * Résout un $ref OpenAPI (premier niveau uniquement) pour obtenir les propriétés du schéma.
     */
    _resolveSchema(spec, schema) {
        if (schema.$ref) {
            const refPath = schema.$ref.replace('#/', '').split('/');
            let resolved = spec;
            for (const segment of refPath) {
                resolved = resolved?.[segment];
            }
            if (resolved) {
                return this._flattenSchema(spec, resolved);
            }
            return { $ref: schema.$ref };
        }
        return this._flattenSchema(spec, schema);
    }

    /**
     * Aplatit un schéma pour extraire les propriétés lisibles.
     * Résout allOf et les $ref imbriqués (un niveau).
     */
    _flattenSchema(spec, schema) {
        if (!schema) return {};

        // Résoudre allOf
        if (schema.allOf) {
            const merged = { type: 'object', properties: {}, required: [] };
            for (const part of schema.allOf) {
                const resolved = part.$ref ? this._resolveSchema(spec, part) : this._flattenSchema(spec, part);
                if (resolved.properties) {
                    Object.assign(merged.properties, resolved.properties);
                }
                if (resolved.required) {
                    merged.required.push(...resolved.required);
                }
            }
            if (merged.required.length === 0) delete merged.required;
            if (Object.keys(merged.properties).length === 0) delete merged.properties;
            return merged;
        }

        // Schéma objet avec propriétés
        if (schema.properties) {
            const props = {};
            for (const [name, prop] of Object.entries(schema.properties)) {
                const p = prop.$ref ? this._resolveSchema(spec, prop) : prop;
                if (p.properties || p.allOf) {
                    // Propriété imbriquée : résolution récursive complète
                    const nested = this._flattenSchema(spec, p);
                    props[name] = nested;
                    if (p.description && !props[name].description) props[name].description = p.description;
                } else {
                    props[name] = {
                        type: p.type || 'object',
                        ...(p.description && { description: p.description }),
                        ...(p.enum && { enum: p.enum })
                    };
                }
            }
            const result = { type: 'object', properties: props };
            if (schema.required) result.required = schema.required;
            return result;
        }

        // Type simple
        return { type: schema.type || 'object', description: schema.description || undefined };
    }

    /**
     * Exécute un endpoint via le service REST du CLI (authentification automatique).
     * Retourne { status, data }.
     */
    async executeEndpoint(_portal, method, endpointPath, pathParams = {}, queryParams = {}, bodyStr = null) {
        this.logger.debug(`ApiDocs.executeEndpoint: ${method} ${endpointPath}`);

        // 1. Substituer les path params
        const builtPath = endpointPath.replace(/\{(\w+)\}/g, (_, name) =>
            encodeURIComponent(pathParams[name] ?? `{${name}}`)
        );

        // 2. Query params (ignorer les valeurs vides)
        const queryArr = Object.entries(queryParams)
            .filter(([_, v]) => v.trim() !== '')
            .map(([k, v]) => [k, v]);

        // 3. Parser le body JSON
        let bodyObj = {};
        if (bodyStr?.trim()) {
            try {
                bodyObj = JSON.parse(bodyStr);
            } catch {
                return { status: 0, data: { error: 'Invalid JSON body' } };
            }
        }

        // 4. Appel REST — le logger est mis en sourdine pendant l'appel pour éviter
        //    que les logs d'erreur HTTP ne percent dans l'affichage TUI blessed.
        //    rest.js lance des strings (pas des Error) pour les statuts non-2xx ;
        //    on les capture ici pour toujours retourner { status, data }.
        this.logger.muteConsole();
        try {
            const response = await this.services['rest'].call_api(builtPath, method, queryArr, bodyObj);
            const status = response.status;
            let data;
            try {
                data = await response.json();
            } catch {
                data = await response.text();
            }
            return { status, data };
        } catch (err) {
            // err est un string du type "status 500 - Internal Server Error"
            const errStr = String(err);
            const statusMatch = errStr.match(/^status (\d+)/);
            const status = statusMatch ? parseInt(statusMatch[1]) : 0;
            return { status, data: { error: errStr } };
        } finally {
            this.logger.unmuteConsole();
        }
    }

    /**
     * Liste les portails disponibles.
     */
    listPortals() {
        return Object.entries(this.portals).map(([name, info]) => ({
            name,
            title: info.title,
            version: info.version
        }));
    }
}
