/**
 * Theme B (P1-14) — classifyError
 *
 * Maps a probe failure into one of the structured ErrorClass values.
 * Strategies call this from their catch blocks and post-response
 * branches; the rules engine matches notification conditions on the
 * result.
 *
 * Precedence (most specific first):
 *   1. keywordFailed   -> KEYWORD_MISS
 *   2. error.code      -> ECONNREFUSED/EHOSTUNREACH/etc. -> TCP/DNS/TLS
 *   3. error.message   -> "Timeout", "Hostname/IP does not match..."
 *   4. statusCode      -> HTTP_5XX / HTTP_4XX
 *   5. fallback        -> UNKNOWN
 *
 * The error-code branch wins over statusCode because if the socket
 * failed (ECONNREFUSED) we never got an HTTP status anyway. The
 * keyword-fail check wins over everything because it represents a
 * successful HTTP exchange that nonetheless failed our assertion —
 * neither a socket nor a status problem.
 */
import type { ErrorClass } from './index';

interface ClassifyInput {
    error?: Error | { code?: string; message?: string } | null;
    statusCode?: number;
    keywordFailed?: boolean;
}

const DNS_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN', 'EAI_NONAME']);
const TCP_REJECT_CODES = new Set([
    'ECONNREFUSED',
    'EHOSTUNREACH',
    'ENETUNREACH',
    'ECONNRESET',
]);
const TLS_CODES = new Set([
    'CERT_HAS_EXPIRED',
    'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
    'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
    'DEPTH_ZERO_SELF_SIGNED_CERT',
    'SELF_SIGNED_CERT_IN_CHAIN',
    'ERR_TLS_CERT_ALTNAME_INVALID',
]);

const TLS_MESSAGE_PATTERNS = [
    /altnames?/i,
    /certificate/i,
    /TLS/i,
    /SSL/i,
    /handshake/i,
];

export function classifyError(input: ClassifyInput): ErrorClass {
    if (input.keywordFailed) return 'KEYWORD_MISS';

    const err = input.error;
    if (err) {
        const code = (err as { code?: string }).code;
        const message = (err as { message?: string }).message ?? '';

        // Specific error codes win over message-based heuristics. This
        // matters when an error has BOTH a meaningful code (e.g.
        // ECONNREFUSED) AND a message that happens to contain "Timeout".
        if (code && DNS_CODES.has(code)) return 'DNS_FAIL';
        if (code && TCP_REJECT_CODES.has(code)) return 'TCP_REJECT';
        if (code && TLS_CODES.has(code)) return 'TLS_INVALID';
        if (code === 'ABORT_ERR') return 'TIMEOUT';

        // No specific code — fall back to message inspection.
        if (/timeout/i.test(message)) return 'TIMEOUT';
        if (TLS_MESSAGE_PATTERNS.some((re) => re.test(message))) {
            return 'TLS_INVALID';
        }
    }

    if (typeof input.statusCode === 'number') {
        if (input.statusCode >= 500 && input.statusCode < 600) return 'HTTP_5XX';
        if (input.statusCode >= 400 && input.statusCode < 500) return 'HTTP_4XX';
    }

    return 'UNKNOWN';
}
