"use strict";
/**
 * CMATE.md Parser
 * Issue #294: Schedule execution feature
 *
 * Parses CMATE.md files (Markdown table format) from worktree root directories.
 * Provides a generic table parser and a specialized schedule section parser.
 *
 * Security:
 * - Path traversal prevention (realpath + worktree directory validation)
 * - Unicode control character sanitization
 * - Name validation with strict pattern matching
 * - Cron expression validation
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MIN_CRON_INTERVAL = exports.CONTROL_CHAR_REGEX = exports.isValidCronExpression = exports.MAX_SCHEDULE_ENTRIES = exports.MAX_CRON_EXPRESSION_LENGTH = exports.NAME_PATTERN = exports.CMATE_FILENAME = void 0;
exports.sanitizeMessageContent = sanitizeMessageContent;
exports.validateCmatePath = validateCmatePath;
exports.parseCmateFile = parseCmateFile;
exports.parseSchedulesSection = parseSchedulesSection;
exports.readCmateFile = readCmateFile;
const promises_1 = require("fs/promises");
const path_1 = __importDefault(require("path"));
const types_1 = require("../lib/cli-tools/types");
const schedule_config_1 = require("../config/schedule-config");
const cmate_constants_1 = require("../config/cmate-constants");
Object.defineProperty(exports, "CMATE_FILENAME", { enumerable: true, get: function () { return cmate_constants_1.CMATE_FILENAME; } });
Object.defineProperty(exports, "NAME_PATTERN", { enumerable: true, get: function () { return cmate_constants_1.NAME_PATTERN; } });
Object.defineProperty(exports, "MAX_CRON_EXPRESSION_LENGTH", { enumerable: true, get: function () { return cmate_constants_1.MAX_CRON_EXPRESSION_LENGTH; } });
Object.defineProperty(exports, "MAX_SCHEDULE_ENTRIES", { enumerable: true, get: function () { return cmate_constants_1.MAX_SCHEDULE_ENTRIES; } });
Object.defineProperty(exports, "isValidCronExpression", { enumerable: true, get: function () { return cmate_constants_1.isValidCronExpression; } });
const logger_1 = require("../lib/logger");
const logger = (0, logger_1.createLogger)('cmate-parser');
/**
 * @deprecated Use CONTROL_CHAR_PATTERN from '../config/cmate-constants' instead.
 * Kept for backward compatibility with existing tests.
 */
exports.CONTROL_CHAR_REGEX = new RegExp(cmate_constants_1.CONTROL_CHAR_PATTERN.source, 'g');
/** Minimum cron interval pattern (every minute) */
exports.MIN_CRON_INTERVAL = '* * * * *';
// =============================================================================
// Sanitization
// =============================================================================
/**
 * Remove Unicode control characters from a string.
 * Preserves tabs (\t), newlines (\n), and carriage returns (\r).
 *
 * @param content - Raw string to sanitize
 * @returns Sanitized string with control characters removed
 */
function sanitizeMessageContent(content) {
    return (0, cmate_constants_1.sanitizeContent)(content);
}
// =============================================================================
// Path Validation
// =============================================================================
/**
 * Validate that a CMATE.md file path is within the expected worktree directory.
 * Prevents path traversal attacks by resolving symlinks and verifying containment.
 *
 * @param filePath - Path to CMATE.md file
 * @param worktreeDir - Expected worktree directory
 * @returns true if path is valid and within worktree directory
 * @throws Error if path traversal is detected
 */
async function validateCmatePath(filePath, worktreeDir) {
    const realFilePath = await (0, promises_1.realpath)(filePath);
    const realWorktreeDir = await (0, promises_1.realpath)(worktreeDir);
    // Ensure the file is within the worktree directory
    if (!realFilePath.startsWith(realWorktreeDir + path_1.default.sep) &&
        realFilePath !== path_1.default.join(realWorktreeDir, cmate_constants_1.CMATE_FILENAME)) {
        throw new Error(`Path traversal detected: ${filePath} is not within ${worktreeDir}`);
    }
    return true;
}
// =============================================================================
// Generic Markdown Table Parser
// =============================================================================
/**
 * Parse a CMATE.md file into a generic structure.
 * Returns a Map where keys are section names (from ## headers)
 * and values are arrays of row data (each row is an array of cell values).
 *
 * [S1-010] Generic design: returns Map<string, string[][]>
 *
 * @param content - Raw CMATE.md file content
 * @returns Map of section name to table rows
 */
function parseCmateFile(content) {
    const result = new Map();
    const lines = content.split('\n');
    let currentSection = null;
    let headerParsed = false;
    let separatorParsed = false;
    for (const line of lines) {
        const trimmed = line.trim();
        // Detect section headers (## SectionName)
        const headerMatch = trimmed.match(/^##\s+(.+)$/);
        if (headerMatch) {
            currentSection = headerMatch[1].trim();
            headerParsed = false;
            separatorParsed = false;
            if (!result.has(currentSection)) {
                result.set(currentSection, []);
            }
            continue;
        }
        // Skip empty lines
        if (!trimmed) {
            continue;
        }
        // Skip non-table lines
        if (!trimmed.startsWith('|')) {
            continue;
        }
        if (!currentSection) {
            continue;
        }
        // Parse table row
        if (!headerParsed) {
            // First row is header - skip it
            headerParsed = true;
            continue;
        }
        if (!separatorParsed) {
            // Second row is separator (|---|---|) - skip it
            if (trimmed.match(/^\|[\s-:|]+\|$/)) {
                separatorParsed = true;
                continue;
            }
            // If it's not a separator, treat it as data
            separatorParsed = true;
        }
        // Parse data row
        const cells = trimmed
            .split('|')
            .slice(1, -1) // Remove leading and trailing empty strings from split
            .map((cell) => cell.trim());
        if (cells.length > 0) {
            result.get(currentSection).push(cells);
        }
    }
    return result;
}
// =============================================================================
// Schedule Section Parser
// =============================================================================
/**
 * Parse the Schedules section of a CMATE.md file into typed ScheduleEntry objects.
 *
 * Expected table format:
 * | Name | Cron | Message | CLI Tool | Enabled |
 * |------|------|---------|----------|---------|
 * | daily-review | 0 9 * * * | Review code changes | claude | true |
 *
 * Entries with invalid names, cron expressions, or missing required fields
 * are silently skipped with a console.warn.
 *
 * @param rows - Raw table rows from parseCmateFile() for the Schedules section
 * @returns Array of validated ScheduleEntry objects
 */
function parseSchedulesSection(rows) {
    const entries = [];
    for (const row of rows) {
        if (entries.length >= cmate_constants_1.MAX_SCHEDULE_ENTRIES) {
            logger.warn('maximum-schedule-entries');
            break;
        }
        // Minimum required columns: Name, Cron, Message
        if (row.length < 3) {
            logger.warn('parse:insufficient-columns', { columnCount: row.length });
            continue;
        }
        const [name, cronExpression, message, cliToolId, enabledStr, permissionStr] = row;
        // Validate name
        const sanitizedName = sanitizeMessageContent(name);
        if (!cmate_constants_1.NAME_PATTERN.test(sanitizedName)) {
            logger.warn('parse:invalid-name', { name: sanitizedName });
            continue;
        }
        // Validate cron expression
        if (!(0, cmate_constants_1.isValidCronExpression)(cronExpression)) {
            logger.warn('parse:invalid-cron', { name: sanitizedName, cron: cronExpression });
            continue;
        }
        // Sanitize message
        const sanitizedMessage = sanitizeMessageContent(message);
        if (!sanitizedMessage) {
            logger.warn('parse:empty-message', { name: sanitizedName });
            continue;
        }
        // Parse enabled (default: true)
        const enabled = enabledStr === undefined ||
            enabledStr === '' ||
            enabledStr.toLowerCase() === 'true';
        // Parse and validate CLI tool ID [SEC-002]
        const resolvedCliToolId = cliToolId?.trim() || 'claude';
        if (!(0, types_1.isCliToolType)(resolvedCliToolId)) {
            logger.warn('parse:invalid-cli-tool', { name: sanitizedName, cliToolId: resolvedCliToolId });
            continue;
        }
        const defaultPermission = schedule_config_1.DEFAULT_PERMISSIONS[resolvedCliToolId] ?? '';
        let permission = permissionStr?.trim() || defaultPermission;
        // Validate permission against allowed values
        let allowedValues;
        switch (resolvedCliToolId) {
            case 'codex':
                allowedValues = schedule_config_1.CODEX_SANDBOXES;
                break;
            case 'gemini':
            case 'vibe-local':
                // No permission flags for gemini/vibe-local; only empty string is valid
                allowedValues = [];
                if (permission) {
                    logger.warn('parse:permission-ignored', { name: sanitizedName, cliToolId: resolvedCliToolId, permission });
                    permission = '';
                }
                break;
            default:
                allowedValues = schedule_config_1.CLAUDE_PERMISSIONS;
                break;
        }
        if (allowedValues.length > 0 && permission && !allowedValues.includes(permission)) {
            logger.warn('parse:invalid-permission', { name: sanitizedName, cliToolId: resolvedCliToolId, permission, defaultPermission });
            permission = defaultPermission;
        }
        entries.push({
            name: sanitizedName,
            cronExpression: cronExpression.trim(),
            message: sanitizedMessage,
            cliToolId: resolvedCliToolId,
            enabled,
            permission,
        });
    }
    return entries;
}
/**
 * Read and parse a CMATE.md file from a worktree directory.
 *
 * @param worktreeDir - Path to the worktree directory
 * @returns Parsed CmateConfig, or null if the file doesn't exist
 * @throws Error if path traversal is detected
 */
async function readCmateFile(worktreeDir) {
    const filePath = path_1.default.join(worktreeDir, cmate_constants_1.CMATE_FILENAME);
    try {
        // Validate path before reading
        await validateCmatePath(filePath, worktreeDir);
        const content = await (0, promises_1.readFile)(filePath, 'utf-8');
        return parseCmateFile(content);
    }
    catch (error) {
        if (error instanceof Error &&
            'code' in error &&
            error.code === 'ENOENT') {
            return null;
        }
        throw error;
    }
}
