/**
 * @file Abstract base scorer class providing common functionality
 * All scorers extend this class for consistent behavior
 */
import { withTimeout, ErrorFactory } from "../../utils/errorHandling.js";
import { logger } from "../../utils/logger.js";
/**
 * Default score scale (0-10)
 */
export const DEFAULT_SCORE_SCALE = {
    min: 0,
    max: 10,
    precision: 2,
};
/**
 * Default scorer configuration
 */
export const DEFAULT_SCORER_CONFIG = {
    enabled: true,
    threshold: 0.7,
    weight: 1.0,
    timeout: 30000,
    retries: 2,
};
/**
 * Abstract base class for all scorers
 * Provides common functionality and enforces interface compliance
 */
export class BaseScorer {
    _config;
    _metadata;
    constructor(metadata, config) {
        this._metadata = metadata;
        this._config = {
            ...DEFAULT_SCORER_CONFIG,
            ...metadata.defaultConfig,
            ...config,
        };
    }
    /**
     * Get scorer metadata
     */
    get metadata() {
        return this._metadata;
    }
    /**
     * Get current configuration
     */
    get config() {
        return this._config;
    }
    /**
     * Validate input has required fields
     */
    validateInput(input) {
        const errors = [];
        for (const field of this._metadata.requiredInputs) {
            const value = input[field];
            if (value === undefined || value === null) {
                errors.push(`Missing required input: ${field}`);
            }
        }
        // Check for empty strings in required text fields
        if (this._metadata.requiredInputs.includes("query") &&
            typeof input.query === "string" &&
            !input.query.trim()) {
            errors.push("Query cannot be empty");
        }
        if (this._metadata.requiredInputs.includes("response") &&
            typeof input.response === "string" &&
            !input.response.trim()) {
            errors.push("Response cannot be empty");
        }
        // Check context array is not empty if required
        if (this._metadata.requiredInputs.includes("context") &&
            input.context !== undefined) {
            if (!Array.isArray(input.context) || input.context.length === 0) {
                errors.push("Context must be a non-empty array");
            }
        }
        return {
            valid: errors.length === 0,
            errors,
        };
    }
    /**
     * Update configuration
     */
    configure(config) {
        this._config = {
            ...this._config,
            ...config,
        };
        logger.debug(`Scorer ${this._metadata.id} reconfigured`, {
            config: this._config,
        });
    }
    /**
     * Normalize a score to 0-1 scale
     */
    normalizeScore(score, scale = DEFAULT_SCORE_SCALE) {
        // Validate inputs are finite
        if (!Number.isFinite(score) ||
            !Number.isFinite(scale.min) ||
            !Number.isFinite(scale.max)) {
            return 0;
        }
        // Guard against zero denominator
        const denominator = scale.max - scale.min;
        if (denominator === 0) {
            return 0;
        }
        // Clamp score to scale bounds
        const clampedScore = Math.max(scale.min, Math.min(scale.max, score));
        const normalized = (clampedScore - scale.min) / denominator;
        return Math.max(0, Math.min(1, normalized));
    }
    /**
     * Convert normalized score back to scale
     */
    denormalizeScore(normalizedScore, scale = DEFAULT_SCORE_SCALE) {
        const clamped = Math.max(0, Math.min(1, normalizedScore));
        return scale.min + clamped * (scale.max - scale.min);
    }
    /**
     * Check if score passes threshold
     */
    checkThreshold(normalizedScore) {
        const threshold = this._config.threshold ?? 0.7;
        return normalizedScore >= threshold;
    }
    /**
     * Create a standardized score result
     */
    createScoreResult(score, reasoning, options = {}) {
        const scale = options.scale ?? DEFAULT_SCORE_SCALE;
        const safeScore = Number.isFinite(score) ? score : scale.min;
        const clampedScore = Math.max(scale.min, Math.min(scale.max, safeScore));
        const normalizedScore = this.normalizeScore(clampedScore, scale);
        // Ensure no NaN leaks into published scores
        const finalScore = Number.isFinite(clampedScore) ? clampedScore : 0;
        const finalNormalized = Number.isFinite(normalizedScore)
            ? normalizedScore
            : 0;
        return {
            scorerId: this._metadata.id,
            scorerName: this._metadata.name,
            score: Number(finalScore.toFixed(scale.precision)),
            normalizedScore: Number(finalNormalized.toFixed(4)),
            scale,
            reasoning,
            passed: this.checkThreshold(finalNormalized),
            threshold: this._config.threshold ?? 0.7,
            confidence: options.confidence === undefined
                ? undefined
                : Number.isFinite(options.confidence)
                    ? Math.max(0, Math.min(1, options.confidence))
                    : 0,
            metadata: options.metadata,
            computeTime: 0, // Set by caller via executeWithTiming
            error: options.error,
        };
    }
    /**
     * Create an error score result
     */
    createErrorResult(error) {
        const errorMessage = error instanceof Error ? error.message : error;
        return {
            scorerId: this._metadata.id,
            scorerName: this._metadata.name,
            score: 0,
            normalizedScore: 0,
            scale: DEFAULT_SCORE_SCALE,
            reasoning: `Scoring failed: ${errorMessage}`,
            passed: false,
            threshold: this._config.threshold ?? 0.7,
            computeTime: 0,
            error: errorMessage,
        };
    }
    /**
     * Execute scoring with timing and error handling
     */
    async executeWithTiming(scoringFn) {
        const startTime = Date.now();
        try {
            const result = await scoringFn();
            return {
                ...result,
                computeTime: Date.now() - startTime,
            };
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            logger.error(`Scorer ${this._metadata.id} failed`, {
                error: errorMessage,
            });
            return {
                ...this.createErrorResult(errorMessage),
                computeTime: Date.now() - startTime,
            };
        }
    }
    /**
     * Execute scoring with timeout
     */
    async executeWithTimeout(fn, timeoutMs, operationName) {
        return withTimeout(fn(), timeoutMs, ErrorFactory.evaluationTimeout(operationName, timeoutMs));
    }
    /**
     * Execute with retry logic
     */
    async executeWithRetry(operation, retries) {
        const maxRetries = retries ?? this._config.retries ?? 2;
        let lastError;
        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                return await operation();
            }
            catch (error) {
                lastError = error instanceof Error ? error : new Error(String(error));
                if (attempt < maxRetries) {
                    logger.debug(`Scorer ${this._metadata.id} retry ${attempt + 1}/${maxRetries}`, { error: lastError.message });
                    // Exponential backoff
                    await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
                }
            }
        }
        throw lastError ?? new Error("Operation failed after retries");
    }
}
//# sourceMappingURL=baseScorer.js.map