/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { execFileSync, spawn } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { homedir, platform } from 'node:os';
import { join } from 'node:path';
import { Messages } from '@salesforce/core';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { getRelevantIntegrations, buildMcpEntry, writeIntegrationToAllTargets } from '../../integrations/index.js';
import { ALL_PROFILES } from '../../profiles/index.js';
import { PROFILE_TOOLSETS } from '../../profiles/toolsets.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@jterrats/setup-agents', 'setup-agents.mcp');
const ALL_TOOLSETS = ['all'];
const PROFILE_HELP_VALUE = ALL_PROFILES.map((p) => p.id).join('|');
async function collectCredentialsAndBuild(integration) {
    const credentials = {};
    const needsPrompt = integration.envVars.filter((v) => !process.env[v.name]);
    for (const envVar of integration.envVars) {
        const fromEnv = process.env[envVar.name];
        if (fromEnv)
            credentials[envVar.name] = fromEnv;
    }
    if (needsPrompt.length > 0 && process.stdin.isTTY) {
        const { input } = await import('@inquirer/prompts');
        for (const envVar of needsPrompt) {
            // Sequential: each prompt must resolve before showing the next (interactive UX).
            // eslint-disable-next-line no-await-in-loop
            const value = await input({
                message: `${envVar.label}: ${envVar.description}`,
                validate: (v) => (v.trim().length > 0 ? true : `${envVar.label} is required`),
            });
            credentials[envVar.name] = value.trim();
        }
    }
    return buildMcpEntry(integration, credentials);
}
async function configureFromEnvVars(relevant, mcpFilePath) {
    const entriesToWrite = {};
    const added = [];
    for (const integration of relevant) {
        const hasEnvToken = integration.envVars.some((v) => !!process.env[v.name]);
        if (!hasEnvToken && integration.envVars.length > 0)
            continue;
        // Sequential: each integration must be processed in order.
        // eslint-disable-next-line no-await-in-loop
        const entry = await collectCredentialsAndBuild(integration);
        entriesToWrite[integration.id] = entry;
        added.push(integration.id);
    }
    if (added.length > 0) {
        const isGlobal = mcpFilePath.startsWith(join(homedir(), '.cursor'));
        writeIntegrationToAllTargets(entriesToWrite, isGlobal, process.cwd());
    }
    return added;
}
function isIntegrationFullyConfigured(integration, entry) {
    if (!entry.headers)
        return integration.envVars.length === 0;
    return Object.values(entry.headers).some((v) => v.length > 0);
}
export default class Mcp extends SfCommand {
    static summary = messages.getMessage('summary');
    static description = messages.getMessage('description');
    static examples = messages.getMessages('examples');
    static flags = {
        'target-org': Flags.string({
            summary: messages.getMessage('flags.target-org.summary'),
            description: messages.getMessage('flags.target-org.description'),
            helpValue: 'myOrgAlias',
        }),
        profile: Flags.string({
            summary: messages.getMessage('flags.profile.summary'),
            description: messages.getMessage('flags.profile.description'),
            helpValue: PROFILE_HELP_VALUE,
        }),
        'all-toolsets': Flags.boolean({
            summary: messages.getMessage('flags.all-toolsets.summary'),
            description: messages.getMessage('flags.all-toolsets.description'),
            default: false,
        }),
        global: Flags.boolean({
            char: 'g',
            summary: messages.getMessage('flags.global.summary'),
            description: messages.getMessage('flags.global.description'),
            default: false,
        }),
    };
    /**
     * Resolves the npx executable to use in the MCP server config.
     * 1. Checks if `npx` is available in PATH.
     * 2. Falls back to the absolute path via `which` / `where`.
     * Returns null if npx cannot be found by either method.
     */
    static resolveNpxCommand() {
        // Option 1: npx available in PATH
        try {
            execFileSync('npx', ['--version'], { stdio: 'ignore' });
            return 'npx';
        }
        catch {
            // not in PATH, try absolute path resolution
        }
        // Option 2: resolve absolute path (execFileSync avoids shell interpretation)
        try {
            const whichBin = platform() === 'win32' ? 'where' : 'which';
            const resolved = execFileSync(whichBin, ['npx'], { encoding: 'utf8' }).trim().split('\n')[0].trim();
            if (resolved)
                return resolved;
        }
        catch {
            // npx not found anywhere
        }
        return null;
    }
    static async spawnWebLogin(alias) {
        return new Promise((resolve) => {
            const child = spawn('sf', ['org', 'login', 'web', '--alias', alias], {
                stdio: 'inherit',
                shell: true,
            });
            child.on('close', (code) => {
                resolve(code === 0);
            });
            child.on('error', () => {
                resolve(false);
            });
        });
    }
    static resolveToolsets(profileFlag) {
        if (!profileFlag)
            return [...ALL_TOOLSETS];
        const ids = profileFlag.split(',').map((s) => s.trim());
        const toolsetSet = new Set();
        for (const id of ids) {
            for (const ts of PROFILE_TOOLSETS[id] ?? ALL_TOOLSETS) {
                toolsetSet.add(ts);
            }
        }
        return [...toolsetSet];
    }
    async run() {
        const { flags } = await this.parse(Mcp);
        const cwd = process.cwd();
        const orgs = await this.resolveOrgs(flags['target-org']);
        if (orgs.length === 0) {
            this.warn(messages.getMessage('warn.noOrgs'));
            return { mcpFile: '', serversAdded: [], cwd };
        }
        const toolsets = flags['all-toolsets'] ? [...ALL_TOOLSETS] : Mcp.resolveToolsets(flags.profile);
        const mcpFilePath = flags.global ? join(homedir(), '.cursor', 'mcp.json') : join(cwd, '.cursor', 'mcp.json');
        const serversAdded = this.writeMcpConfig(mcpFilePath, orgs, toolsets);
        // Phase 2: third-party MCP integrations
        const thirdPartyAdded = await this.configureThirdPartyMcps(mcpFilePath, flags.profile);
        serversAdded.push(...thirdPartyAdded);
        this.log(messages.getMessage('info.done', [mcpFilePath, serversAdded.join(', ')]));
        return { mcpFile: mcpFilePath, serversAdded, cwd };
    }
    async resolveOrgs(targetOrg) {
        if (targetOrg)
            return [targetOrg];
        if (!process.stdin.isTTY) {
            this.warn(messages.getMessage('warn.nonInteractive'));
            return [];
        }
        const orgAliases = this.listAuthenticatedOrgs();
        if (orgAliases.length === 0) {
            return this.offerWebLogin();
        }
        const { checkbox } = await import('@inquirer/prompts');
        return checkbox({
            message: messages.getMessage('prompt.selectOrgs'),
            choices: orgAliases.map((alias) => ({ value: alias, name: alias })),
        });
    }
    listAuthenticatedOrgs() {
        try {
            const raw = execFileSync('sf', ['org', 'list', '--json'], {
                encoding: 'utf8',
                stdio: ['pipe', 'pipe', 'ignore'],
            });
            const parsed = JSON.parse(raw);
            const all = [...(parsed.result?.nonScratchOrgs ?? []), ...(parsed.result?.scratchOrgs ?? [])];
            return all.map((o) => o.alias ?? o.username).filter(Boolean);
        }
        catch {
            this.warn(messages.getMessage('warn.orgListFailed'));
            return [];
        }
    }
    async offerWebLogin() {
        const { confirm, input } = await import('@inquirer/prompts');
        const shouldLogin = await confirm({
            message: messages.getMessage('prompt.offerLogin'),
            default: true,
        });
        if (!shouldLogin)
            return [];
        const alias = await input({
            message: messages.getMessage('prompt.orgAlias'),
            validate: (v) => (v.trim().length > 0 ? true : messages.getMessage('error.aliasRequired')),
        });
        this.log(messages.getMessage('info.loginStarting', [alias.trim()]));
        const loginSuccess = await Mcp.spawnWebLogin(alias.trim());
        if (!loginSuccess) {
            this.warn(messages.getMessage('warn.loginFailed'));
            return [];
        }
        this.log(messages.getMessage('info.loginSuccess', [alias.trim()]));
        return [alias.trim()];
    }
    /**
     * Detects which third-party MCP integrations are relevant for the active
     * profiles and prompts the user to configure them. Writes to all supported
     * LLM config targets (Cursor, VS Code, Windsurf, Claude Desktop, Claude Code).
     */
    async configureThirdPartyMcps(mcpFilePath, profileFlag) {
        const profileIds = profileFlag ? profileFlag.split(',').map((s) => s.trim()) : [];
        const relevant = getRelevantIntegrations(profileIds);
        if (relevant.length === 0)
            return [];
        if (!process.stdin.isTTY) {
            return configureFromEnvVars(relevant, mcpFilePath);
        }
        const { checkbox } = await import('@inquirer/prompts');
        const selected = await checkbox({
            message: 'These third-party integrations are available for your profiles. Which do you want to configure?',
            choices: relevant.map((i) => ({ value: i.id, name: `${i.label} — profiles: ${[...i.profiles].join(', ')}` })),
        });
        if (selected.length === 0)
            return [];
        const { entries, added, needsManualAuth } = await this.buildSelectedEntries(relevant, selected);
        if (added.length > 0) {
            const isGlobal = mcpFilePath.startsWith(join(homedir(), '.cursor'));
            const writtenFiles = writeIntegrationToAllTargets(entries, isGlobal, process.cwd());
            this.log(`Wrote third-party MCP config to: ${writtenFiles.join(', ')}`);
        }
        if (needsManualAuth.length > 0) {
            this.warn(`These integrations need manual auth before tool calls will work: ${needsManualAuth.join(', ')}. ` +
                'Set the corresponding API token env var and re-run, or authenticate via the VS Code extension.');
        }
        return added;
    }
    async buildSelectedEntries(relevant, selected) {
        const entries = {};
        const added = [];
        const needsManualAuth = [];
        for (const integrationId of selected) {
            const integration = relevant.find((i) => i.id === integrationId);
            if (!integration)
                continue;
            // eslint-disable-next-line no-await-in-loop
            const entry = await collectCredentialsAndBuild(integration);
            entries[integration.id] = entry;
            added.push(integration.id);
            if (isIntegrationFullyConfigured(integration, entry)) {
                this.log(`✅ ${integration.label} — fully configured`);
            }
            else {
                needsManualAuth.push(integration.label);
                this.log(`⚠️  ${integration.label} — added (auth required on first use)`);
            }
        }
        return { entries, added, needsManualAuth };
    }
    writeMcpConfig(mcpFilePath, orgs, toolsets) {
        const mcpDir = join(mcpFilePath, '..');
        if (!existsSync(mcpDir))
            mkdirSync(mcpDir, { recursive: true });
        let existing = { mcpServers: {} };
        if (existsSync(mcpFilePath)) {
            try {
                existing = JSON.parse(readFileSync(mcpFilePath, 'utf8'));
                if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
                    existing.mcpServers = {};
                }
            }
            catch {
                this.warn(messages.getMessage('warn.corruptMcpJson', [mcpFilePath]));
            }
        }
        const npxCmd = Mcp.resolveNpxCommand();
        if (!npxCmd) {
            this.warn(messages.getMessage('warn.npxNotFound'));
        }
        const serversAdded = [];
        for (const org of orgs) {
            const serverKey = `salesforce-${org}`;
            existing.mcpServers[serverKey] = {
                command: npxCmd ?? 'npx',
                args: ['@salesforce/mcp@latest', '--orgs', org, '--toolsets', toolsets.join(',')],
                type: 'stdio',
            };
            serversAdded.push(serverKey);
            this.log(messages.getMessage('info.serverAdded', [serverKey]));
        }
        writeFileSync(mcpFilePath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
        return serversAdded;
    }
}
//# sourceMappingURL=mcp.js.map