/**
 * PII Redaction Hooks
 *
 * Provides PII detection and redaction for transcript logging.
 * Supports configurable patterns for various PII types.
 * Never stores original values - only logs that redaction occurred.
 *
 * @module transcripts/redaction
 */
import { createHash } from "crypto";
// ============================================================================
// Default Patterns
// ============================================================================
/**
 * Default PII detection patterns
 */
export const DEFAULT_PII_PATTERNS = [
    // Email addresses
    {
        name: "email",
        type: "email",
        pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
        placeholder: "[REDACTED:EMAIL]",
        validate: (match) => !match.includes("example.com") &&
            !match.includes("test.com") &&
            !match.includes("localhost"),
    },
    // US Phone numbers
    {
        name: "phone-us",
        type: "phone",
        pattern: /(?:\+1[-.\s]?)?(?:\(\d{3}\)|\d{3})[-.\s]?\d{3}[-.\s]?\d{4}/g,
        placeholder: "[REDACTED:PHONE]",
    },
    // International phone (simple)
    {
        name: "phone-intl",
        type: "phone",
        pattern: /\+\d{1,3}[-.\s]?\d{2,4}[-.\s]?\d{3,4}[-.\s]?\d{3,4}/g,
        placeholder: "[REDACTED:PHONE]",
    },
    // US SSN
    {
        name: "ssn",
        type: "ssn",
        pattern: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/g,
        placeholder: "[REDACTED:SSN]",
        validate: (match) => {
            const clean = match.replace(/[-\s]/g, "");
            const first3 = parseInt(clean.slice(0, 3));
            return first3 > 0 && first3 < 900 && first3 !== 666;
        },
    },
    // Credit card numbers
    {
        name: "credit-card",
        type: "credit-card",
        pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/g,
        placeholder: "[REDACTED:CC]",
        validate: (match) => {
            // Luhn algorithm
            const digits = match.replace(/\D/g, "");
            let sum = 0;
            let isEven = false;
            for (let i = digits.length - 1; i >= 0; i--) {
                let d = parseInt(digits[i]);
                if (isEven) {
                    d *= 2;
                    if (d > 9)
                        d -= 9;
                }
                sum += d;
                isEven = !isEven;
            }
            return sum % 10 === 0;
        },
    },
    // IPv4 addresses (non-private)
    {
        name: "ipv4",
        type: "ip-address",
        pattern: /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g,
        placeholder: "[REDACTED:IP]",
        validate: (match) => {
            // Exclude localhost and private ranges
            return (!match.startsWith("127.") &&
                !match.startsWith("192.168.") &&
                !match.startsWith("10.") &&
                !match.startsWith("172.16.") &&
                !match.startsWith("172.17.") &&
                !match.startsWith("172.18.") &&
                !match.startsWith("172.19.") &&
                !match.startsWith("172.20.") &&
                !match.startsWith("172.21.") &&
                !match.startsWith("172.22.") &&
                !match.startsWith("172.23.") &&
                !match.startsWith("172.24.") &&
                !match.startsWith("172.25.") &&
                !match.startsWith("172.26.") &&
                !match.startsWith("172.27.") &&
                !match.startsWith("172.28.") &&
                !match.startsWith("172.29.") &&
                !match.startsWith("172.30.") &&
                !match.startsWith("172.31.") &&
                match !== "0.0.0.0");
        },
    },
    // API keys (common formats)
    {
        name: "api-key-generic",
        type: "api-key",
        pattern: /\b(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token|bearer)[:\s=]+['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
        placeholder: "[REDACTED:API_KEY]",
    },
    // AWS Access Key
    {
        name: "aws-access-key",
        type: "api-key",
        pattern: /\b(AKIA[0-9A-Z]{16})\b/g,
        placeholder: "[REDACTED:AWS_KEY]",
    },
    // AWS Secret Key
    {
        name: "aws-secret-key",
        type: "api-key",
        pattern: /\b([a-zA-Z0-9/+=]{40})\b/g,
        placeholder: "[REDACTED:AWS_SECRET]",
        validate: (match) => {
            // Basic heuristic: contains mix of alphanumeric and special chars
            return /[a-z]/.test(match) && /[A-Z]/.test(match) && /[0-9]/.test(match);
        },
    },
    // GitHub token
    {
        name: "github-token",
        type: "api-key",
        pattern: /\b(gh[ps]_[a-zA-Z0-9]{36,})\b/g,
        placeholder: "[REDACTED:GH_TOKEN]",
    },
    // OpenAI API key
    {
        name: "openai-key",
        type: "api-key",
        pattern: /\b(sk-[a-zA-Z0-9]{48,})\b/g,
        placeholder: "[REDACTED:OPENAI_KEY]",
    },
    // Anthropic API key
    {
        name: "anthropic-key",
        type: "api-key",
        pattern: /\b(sk-ant-[a-zA-Z0-9-]{40,})\b/g,
        placeholder: "[REDACTED:ANTHROPIC_KEY]",
    },
    // Slack token
    {
        name: "slack-token",
        type: "api-key",
        pattern: /\b(xox[baprs]-[0-9a-zA-Z-]+)\b/g,
        placeholder: "[REDACTED:SLACK_TOKEN]",
    },
    // JWT tokens
    {
        name: "jwt",
        type: "jwt",
        pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g,
        placeholder: "[REDACTED:JWT]",
    },
    // Password patterns (common formats)
    {
        name: "password-field",
        type: "password",
        pattern: /\b(?:password|passwd|pwd|secret)[:\s=]+['"]?([^\s'"]{6,})['"]?/gi,
        placeholder: "[REDACTED:PASSWORD]",
    },
    // UUIDs
    {
        name: "uuid",
        type: "uuid",
        pattern: /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi,
        placeholder: "[REDACTED:UUID]",
    },
];
// ============================================================================
// Redaction Functions
// ============================================================================
/**
 * Redact PII from content
 */
export function redactPII(content, options) {
    if (!content || content.length === 0) {
        return {
            redacted: content,
            redactions: [],
            hadPII: false,
            originalLength: 0,
            redactedLength: 0,
        };
    }
    const originalLength = content.length;
    const redactions = [];
    let redactedContent = content;
    // Combine default and custom patterns
    let patterns = [...DEFAULT_PII_PATTERNS];
    if (options?.customPatterns) {
        patterns = [...patterns, ...options.customPatterns];
    }
    // Filter excluded patterns
    if (options?.excludePatterns) {
        patterns = patterns.filter((p) => !options.excludePatterns.includes(p.name));
    }
    // Track all matches first to handle overlapping patterns
    const matches = [];
    for (const piiPattern of patterns) {
        // Reset regex state
        piiPattern.pattern.lastIndex = 0;
        let match;
        while ((match = piiPattern.pattern.exec(content)) !== null) {
            const matchText = match[0];
            // Apply validator if present
            if (piiPattern.validate && !piiPattern.validate(matchText)) {
                continue;
            }
            matches.push({
                pattern: piiPattern,
                start: match.index,
                end: match.index + matchText.length,
                match: matchText,
            });
        }
    }
    // Sort by start position (descending) to replace from end first
    matches.sort((a, b) => b.start - a.start);
    // Remove overlapping matches (keep longest)
    const filteredMatches = [];
    for (const m of matches) {
        const overlaps = filteredMatches.some((f) => (m.start >= f.start && m.start < f.end) || (m.end > f.start && m.end <= f.end));
        if (!overlaps) {
            filteredMatches.push(m);
        }
    }
    // Apply max redactions limit
    const maxRedactions = options?.maxRedactions ?? 1000;
    const applicableMatches = filteredMatches.slice(0, maxRedactions);
    // Perform redactions from end to start (preserves positions)
    for (const m of applicableMatches) {
        const placeholder = m.pattern.placeholder ||
            (options?.placeholderFormat || "[REDACTED:{type}]").replace("{type}", m.pattern.type.toUpperCase());
        // Replace in content
        redactedContent =
            redactedContent.slice(0, m.start) + placeholder + redactedContent.slice(m.end);
        // Create redaction log entry
        const log = {
            type: m.pattern.type,
            position: m.start,
            length: m.match.length,
            placeholder,
            patternName: m.pattern.name,
        };
        // Hash original if requested (for deduplication)
        if (options?.hashOriginals) {
            log.contentHash = createHash("sha256").update(m.match).digest("hex").slice(0, 16);
        }
        redactions.push(log);
    }
    // Reverse redactions to be in original order
    redactions.reverse();
    return {
        redacted: redactedContent,
        redactions,
        hadPII: redactions.length > 0,
        originalLength,
        redactedLength: redactedContent.length,
    };
}
/**
 * Check if content contains PII without redacting
 */
export function containsPII(content, options) {
    const result = redactPII(content, options);
    return result.hadPII;
}
/**
 * Get PII statistics for content
 */
export function analyzePII(content, options) {
    const result = redactPII(content, options);
    const byType = {};
    const patterns = new Set();
    for (const r of result.redactions) {
        byType[r.type] = (byType[r.type] || 0) + 1;
        if (r.patternName) {
            patterns.add(r.patternName);
        }
    }
    return {
        totalPII: result.redactions.length,
        byType,
        patterns: [...patterns],
    };
}
// ============================================================================
// Batch Operations
// ============================================================================
/**
 * Redact PII from multiple content strings
 */
export function redactPIIBatch(contents, options) {
    return contents.map((content) => redactPII(content, options));
}
/**
 * Create a redaction hook for TranscriptLogger
 */
export function createRedactionHook(options) {
    return (content) => {
        const result = redactPII(content, options);
        if (!result.hadPII) {
            return { content };
        }
        return {
            content: result.redacted,
            redactionLog: result.redactions,
        };
    };
}
// ============================================================================
// Pattern Management
// ============================================================================
/**
 * Create a custom PII pattern
 */
export function createPIIPattern(name, type, pattern, options) {
    const regex = typeof pattern === "string" ? new RegExp(pattern, "gi") : pattern;
    return {
        name,
        type,
        pattern: regex,
        placeholder: options?.placeholder,
        validate: options?.validate,
    };
}
/**
 * Get all default pattern names
 */
export function getDefaultPatternNames() {
    return DEFAULT_PII_PATTERNS.map((p) => p.name);
}
/**
 * Get patterns by type
 */
export function getPatternsByType(type) {
    return DEFAULT_PII_PATTERNS.filter((p) => p.type === type);
}
// ============================================================================
// Summary Generation
// ============================================================================
/**
 * Generate redaction summary for logging
 */
export function generateRedactionSummary(results) {
    const totalRedactions = results.reduce((sum, r) => sum + r.redactions.length, 0);
    const contentWithPII = results.filter((r) => r.hadPII).length;
    if (totalRedactions === 0) {
        return "No PII detected";
    }
    // Aggregate by type
    const byType = {};
    for (const result of results) {
        for (const r of result.redactions) {
            byType[r.type] = (byType[r.type] || 0) + 1;
        }
    }
    const typeSummary = Object.entries(byType)
        .map(([type, count]) => `${type}: ${count}`)
        .join(", ");
    return `Redacted ${totalRedactions} PII instances across ${contentWithPII} entries (${typeSummary})`;
}
/**
 * Generate markdown report of redactions
 */
export function generateRedactionReport(results) {
    const lines = [];
    lines.push("## PII Redaction Report");
    lines.push("");
    const totalRedactions = results.reduce((sum, r) => sum + r.redactions.length, 0);
    const contentWithPII = results.filter((r) => r.hadPII).length;
    lines.push("### Summary");
    lines.push(`- **Total Redactions**: ${totalRedactions}`);
    lines.push(`- **Content with PII**: ${contentWithPII}/${results.length}`);
    lines.push("");
    if (totalRedactions === 0) {
        lines.push("No PII detected in analyzed content.");
        return lines.join("\n");
    }
    // Aggregate by type
    const byType = {};
    const byPattern = {};
    for (const result of results) {
        for (const r of result.redactions) {
            byType[r.type] = (byType[r.type] || 0) + 1;
            if (r.patternName) {
                byPattern[r.patternName] = (byPattern[r.patternName] || 0) + 1;
            }
        }
    }
    lines.push("### By Type");
    for (const [type, count] of Object.entries(byType).sort((a, b) => b[1] - a[1])) {
        lines.push(`- **${type}**: ${count}`);
    }
    lines.push("");
    lines.push("### By Pattern");
    for (const [pattern, count] of Object.entries(byPattern).sort((a, b) => b[1] - a[1])) {
        lines.push(`- \`${pattern}\`: ${count}`);
    }
    return lines.join("\n");
}
//# sourceMappingURL=redaction.js.map