// Permission button handler - Shows buttons for permission requests.
// When OpenCode asks for permission, this module renders 3 buttons:
// Accept, Accept Always, and Deny.
import { ButtonBuilder, ButtonStyle, ActionRowBuilder, MessageFlags, } from 'discord.js';
import crypto from 'node:crypto';
import { getOpencodeClient } from '../opencode.js';
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
import { createLogger, LogPrefix } from '../logger.js';
const logger = createLogger(LogPrefix.PERMISSIONS);
function wildcardMatch({ value, pattern, }) {
    let escapedPattern = pattern
        .replace(/[.+^${}()|[\]\\]/g, '\\$&')
        .replace(/\*/g, '.*')
        .replace(/\?/g, '.');
    if (escapedPattern.endsWith(' .*')) {
        escapedPattern = escapedPattern.slice(0, -3) + '( .*)?';
    }
    return new RegExp(`^${escapedPattern}$`, 's').test(value);
}
export function arePatternsCoveredBy({ patterns, coveringPatterns, }) {
    return patterns.every((pattern) => {
        return coveringPatterns.some((coveringPattern) => {
            return wildcardMatch({ value: pattern, pattern: coveringPattern });
        });
    });
}
export function compactPermissionPatterns(patterns) {
    const uniquePatterns = Array.from(new Set(patterns));
    return uniquePatterns.filter((pattern, index) => {
        return !uniquePatterns.some((candidate, candidateIndex) => {
            if (candidateIndex === index) {
                return false;
            }
            return wildcardMatch({ value: pattern, pattern: candidate });
        });
    });
}
// Store pending permission contexts by hash.
// TTL prevents unbounded growth if user never clicks a permission button.
const PERMISSION_CONTEXT_TTL_MS = 10 * 60 * 1000;
export const pendingPermissionContexts = new Map();
// Atomic take: removes context from Map and returns it. Only the first caller
// (TTL expiry or button click) wins, preventing duplicate permission replies.
function takePendingPermissionContext(contextHash) {
    const ctx = pendingPermissionContexts.get(contextHash);
    if (!ctx) {
        return undefined;
    }
    pendingPermissionContexts.delete(contextHash);
    return ctx;
}
/**
 * Show permission buttons for a permission request.
 * Displays 3 buttons in a row: Accept, Accept Always, Deny.
 * Returns the message ID and context hash for tracking.
 */
export async function showPermissionButtons({ thread, permission, directory, permissionDirectory, subtaskLabel, }) {
    const contextHash = crypto.randomBytes(8).toString('hex');
    const context = {
        permission,
        requestIds: [permission.id],
        directory,
        permissionDirectory,
        thread,
        contextHash,
    };
    pendingPermissionContexts.set(contextHash, context);
    // Auto-reject on TTL expiry so the OpenCode session doesn't hang forever
    // waiting for a permission reply that will never come. Uses atomic take
    // so only one of TTL-expiry or button-click can win.
    setTimeout(async () => {
        const ctx = takePendingPermissionContext(contextHash);
        if (!ctx) {
            return;
        }
        const client = getOpencodeClient(ctx.directory);
        if (client) {
            const requestIds = ctx.requestIds.length > 0
                ? ctx.requestIds
                : [ctx.permission.id];
            await Promise.all(requestIds.map((requestId) => {
                return client.permission.reply({
                    requestID: requestId,
                    directory: ctx.permissionDirectory,
                    reply: 'reject',
                });
            })).catch((error) => {
                logger.error('Failed to auto-reject expired permission:', error);
            });
        }
    }, PERMISSION_CONTEXT_TTL_MS).unref();
    const patternStr = compactPermissionPatterns(permission.patterns).join(', ');
    // Build 3 buttons for permission actions
    const acceptButton = new ButtonBuilder()
        .setCustomId(`permission_once:${contextHash}`)
        .setLabel('Accept')
        .setStyle(ButtonStyle.Success);
    const acceptAlwaysButton = new ButtonBuilder()
        .setCustomId(`permission_always:${contextHash}`)
        .setLabel('Accept Always')
        .setStyle(ButtonStyle.Success);
    const denyButton = new ButtonBuilder()
        .setCustomId(`permission_reject:${contextHash}`)
        .setLabel('Deny')
        .setStyle(ButtonStyle.Secondary);
    const actionRow = new ActionRowBuilder().addComponents(acceptButton, acceptAlwaysButton, denyButton);
    const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : '';
    const externalDirLine = permission.permission === 'external_directory'
        ? `Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n`
        : '';
    const fullContent = `⚠️ **Permission Required**\n` +
        subtaskLine +
        `**Type:** \`${permission.permission}\`\n` +
        externalDirLine +
        (patternStr ? `**Pattern:** \`${patternStr}\`` : '');
    const permissionMessage = await thread.send({
        content: fullContent.slice(0, 1900),
        components: [actionRow],
        flags: NOTIFY_MESSAGE_FLAGS | MessageFlags.SuppressEmbeds,
    });
    context.messageId = permissionMessage.id;
    logger.log(`Showed permission buttons for ${permission.id}`);
    return { messageId: permissionMessage.id, contextHash };
}
function updatePermissionMessage({ context, status, }) {
    if (!context.messageId) {
        return;
    }
    context.thread.messages
        .fetch(context.messageId)
        .then((message) => {
        const patternStr = compactPermissionPatterns(context.permission.patterns).join(', ');
        const externalDirLine = context.permission.permission === 'external_directory'
            ? 'Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n'
            : '';
        return message.edit({
            content: `⚠️ **Permission Required**\n` +
                `**Type:** \`${context.permission.permission}\`\n` +
                externalDirLine +
                (patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
                status,
            components: [],
        });
    })
        .catch((error) => {
        logger.error('Failed to update permission message:', error);
    });
}
export async function cancelPendingPermission(threadId) {
    const contexts = Array.from(pendingPermissionContexts.values()).filter((context) => {
        return context.thread.id === threadId;
    });
    if (contexts.length === 0) {
        return false;
    }
    let cancelledCount = 0;
    for (const context of contexts) {
        const pendingContext = takePendingPermissionContext(context.contextHash);
        if (!pendingContext) {
            continue;
        }
        const client = getOpencodeClient(pendingContext.directory);
        if (!client) {
            pendingPermissionContexts.set(pendingContext.contextHash, pendingContext);
            logger.error('Failed to dismiss pending permission: OpenCode server not found');
            continue;
        }
        const requestIds = pendingContext.requestIds.length > 0
            ? pendingContext.requestIds
            : [pendingContext.permission.id];
        const result = await Promise.all(requestIds.map((requestId) => {
            return client.permission.reply({
                requestID: requestId,
                directory: pendingContext.permissionDirectory,
                reply: 'reject',
            });
        })).then(() => {
            return 'ok';
        }).catch((error) => {
            pendingPermissionContexts.set(pendingContext.contextHash, pendingContext);
            logger.error('Failed to dismiss pending permission:', error);
            return 'error';
        });
        if (result === 'error') {
            continue;
        }
        updatePermissionMessage({
            context: pendingContext,
            status: '_Permission dismissed - user sent a new message._',
        });
        cancelledCount++;
    }
    if (cancelledCount > 0) {
        logger.log(`Dismissed ${cancelledCount} pending permission request(s) for thread ${threadId}`);
    }
    return cancelledCount > 0;
}
/**
 * Handle button click for permission.
 */
export async function handlePermissionButton(interaction) {
    const customId = interaction.customId;
    // Extract action and hash from customId (e.g., "permission_once:abc123")
    const [actionPart, contextHash] = customId.split(':');
    if (!actionPart || !contextHash) {
        return;
    }
    const response = actionPart.replace('permission_', '');
    // Atomic take: if TTL already expired and auto-rejected, context is gone.
    const context = takePendingPermissionContext(contextHash);
    if (!context) {
        await interaction.update({ components: [] });
        return;
    }
    await interaction.deferUpdate();
    try {
        const permClient = getOpencodeClient(context.directory);
        if (!permClient) {
            throw new Error('OpenCode server not found for directory');
        }
        const requestIds = context.requestIds.length > 0
            ? context.requestIds
            : [context.permission.id];
        await Promise.all(requestIds.map((requestId) => {
            return permClient.permission.reply({
                requestID: requestId,
                directory: context.permissionDirectory,
                reply: response,
            });
        }));
        // Context already removed by takePendingPermissionContext above.
        // Update message: show result and remove dropdown
        const resultText = (() => {
            switch (response) {
                case 'once':
                    return '✅ Permission **accepted**';
                case 'always':
                    return '✅ Permission **accepted** (auto-approve similar requests)';
                case 'reject':
                    return '❌ Permission **rejected**';
            }
        })();
        updatePermissionMessage({
            context,
            status: resultText,
        });
        logger.log(`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`);
    }
    catch (error) {
        logger.error('Error handling permission:', error);
        await interaction.editReply({
            content: `Failed to process permission: ${error instanceof Error ? error.message : 'Unknown error'}`,
            components: [],
        });
    }
}
export function addPermissionRequestToContext({ contextHash, requestId, }) {
    const context = pendingPermissionContexts.get(contextHash);
    if (!context) {
        return false;
    }
    if (context.requestIds.includes(requestId)) {
        return false;
    }
    context.requestIds = [...context.requestIds, requestId];
    pendingPermissionContexts.set(contextHash, context);
    return true;
}
/**
 * Clean up a pending permission context (e.g., on auto-reject).
 */
export function cleanupPermissionContext(contextHash) {
    pendingPermissionContexts.delete(contextHash);
}
