'use strict';

/**
 * Scorecard engine for adaptive model selection.
 * Computes composite scores based on quality, cost, and speed,
 * weighted by task type.
 */

const TASK_WEIGHTS = {
  chat:       { quality: 0.3, cost: 0.2, speed: 0.5 },
  coding:     { quality: 0.6, cost: 0.1, speed: 0.3 },
  'coding:planning':    { quality: 0.7, cost: 0.1, speed: 0.2 },
  'coding:exploration': { quality: 0.3, cost: 0.2, speed: 0.5 },
  'coding:generation':  { quality: 0.7, cost: 0.1, speed: 0.2 },
  'coding:debugging':   { quality: 0.8, cost: 0.05, speed: 0.15 },
  'coding:refactoring': { quality: 0.7, cost: 0.1, speed: 0.2 },
  'coding:testing':     { quality: 0.6, cost: 0.15, speed: 0.25 },
  'coding:review':      { quality: 0.8, cost: 0.1, speed: 0.1 },
  extraction: { quality: 0.4, cost: 0.4, speed: 0.2 },
  reflection: { quality: 0.5, cost: 0.3, speed: 0.2 },
  initiative: { quality: 0.5, cost: 0.2, speed: 0.3 },
  skill:      { quality: 0.4, cost: 0.3, speed: 0.3 },
  default:    { quality: 0.4, cost: 0.3, speed: 0.3 },
};

// Normalization caps — used when there's only one candidate or to bound values
const MAX_LATENCY_MS = 30000;  // 30 seconds
const MAX_COST_PER_EVAL = 0.10; // $0.10 per evaluation

// Default scores by speed_tier (1=fast, 5=slow) when no eval data exists
const SPEED_TIER_DEFAULTS = {
  1: { quality: 0.5, latency: 2000 },
  2: { quality: 0.6, latency: 5000 },
  3: { quality: 0.7, latency: 10000 },
  4: { quality: 0.8, latency: 15000 },
  5: { quality: 0.9, latency: 20000 },
};

/**
 * Get task-type-specific weight configuration.
 * @param {string} taskType
 * @returns {{ quality: number, cost: number, speed: number }}
 */
function getWeights(taskType) {
  return TASK_WEIGHTS[taskType] || TASK_WEIGHTS.default;
}

/**
 * Get composite score for a model on a task type.
 * @param {Object} stats - From brain.getModelScorecard() { total_evals, avg_quality, avg_latency_ms, total_cost, wins }
 * @param {string} taskType - 'chat', 'coding', 'extraction', etc.
 * @param {Object} [normCaps] - Optional normalization caps { maxLatency, maxCost }
 * @returns {{ compositeScore: number, breakdown: { quality: number, cost: number, speed: number } }}
 */
function computeScore(stats, taskType, normCaps = {}, shadowStats = null) {
  const weights = getWeights(taskType);
  const maxLatency = normCaps.maxLatency || MAX_LATENCY_MS;
  const maxCost = normCaps.maxCost || MAX_COST_PER_EVAL;

  const avgQuality = stats.avg_quality ?? 0.5;
  const avgLatency = stats.avg_latency_ms ?? maxLatency * 0.5;
  const totalCalls = stats.total_evals ?? 0;
  const avgCost = totalCalls > 0 && stats.total_cost != null
    ? stats.total_cost / totalCalls
    : maxCost * 0.5;

  const normalizedQuality = Math.max(0, Math.min(avgQuality, 1));
  const normalizedCostEfficiency = 1 - Math.min(avgCost / maxCost, 1);
  const normalizedSpeed = 1 - Math.min(avgLatency / maxLatency, 1);

  const compositeScore =
    weights.quality * normalizedQuality +
    weights.cost * normalizedCostEfficiency +
    weights.speed * normalizedSpeed;

  // Blend in shadow eval data if available
  if (shadowStats && shadowStats.total_evals >= 10) {
    const shadowQuality = shadowStats.avg_score || 0;
    const shadowWeight = Math.min(shadowStats.total_evals / 100, 0.5);
    const blendedQuality = normalizedQuality * (1 - shadowWeight) + shadowQuality * shadowWeight;
    // Recompute composite with blended quality
    const blendedComposite =
      weights.quality * blendedQuality +
      weights.cost * normalizedCostEfficiency +
      weights.speed * normalizedSpeed;
    return {
      compositeScore: Math.round(blendedComposite * 1000) / 1000,
      breakdown: {
        quality: Math.round(blendedQuality * 1000) / 1000,
        cost: Math.round(normalizedCostEfficiency * 1000) / 1000,
        speed: Math.round(normalizedSpeed * 1000) / 1000,
      },
    };
  }

  return {
    compositeScore: Math.round(compositeScore * 1000) / 1000,
    breakdown: {
      quality: Math.round(normalizedQuality * 1000) / 1000,
      cost: Math.round(normalizedCostEfficiency * 1000) / 1000,
      speed: Math.round(normalizedSpeed * 1000) / 1000,
    },
  };
}

/**
 * Derive default stats from model registry metadata when no evaluations exist.
 * @param {Object} model - Model registry entry
 * @returns {Object} - Pseudo-stats object compatible with computeScore
 */
function defaultStatsFromRegistry(model) {
  const tier = model.speed_tier || 3;
  const defaults = SPEED_TIER_DEFAULTS[tier] || SPEED_TIER_DEFAULTS[3];
  const costPer1m = model.cost_per_1m_input || 3.0; // default mid-range

  return {
    total_evals: 0,
    avg_quality: defaults.quality,
    avg_latency_ms: defaults.latency,
    total_cost: null,
    wins: 0,
    _estimated_cost_per_call: costPer1m / 1000, // rough: ~1k tokens per call
  };
}

/**
 * Select the best model(s) for a task type from available candidates.
 * @param {string} taskType
 * @param {Array} candidates - Array of { registryId, ...modelInfo }
 * @param {Object} brain - Brain module for DB access (needs getModelScorecard)
 * @param {number} [count=1] - How many to select
 * @returns {Array} - Sorted candidates with scores, best first
 */
function selectBestModels(taskType, candidates, brain, count = 1) {
  if (!candidates || candidates.length === 0) return [];

  // Gather stats for all candidates
  const scoredCandidates = candidates.map(candidate => {
    const id = candidate.registryId || candidate.id;
    const stats = brain.getModelScorecard(id, 90);
    const hasData = stats && stats.total_evals > 0;

    let effectiveStats = stats;
    if (!hasData) {
      effectiveStats = defaultStatsFromRegistry(candidate);
    }

    // Compute normalization caps from the cohort
    return { candidate, stats: effectiveStats, hasData };
  });

  // Find max latency and max cost across candidates for normalization
  const latencies = scoredCandidates
    .map(sc => sc.stats.avg_latency_ms)
    .filter(v => v != null && v > 0);
  const costs = scoredCandidates
    .map(sc => {
      if (sc.stats.total_evals > 0 && sc.stats.total_cost != null) {
        return sc.stats.total_cost / sc.stats.total_evals;
      }
      return sc.stats._estimated_cost_per_call || null;
    })
    .filter(v => v != null && v > 0);

  const maxLatency = latencies.length > 0 ? Math.max(...latencies, 1) : MAX_LATENCY_MS;
  const maxCost = costs.length > 0 ? Math.max(...costs, 0.001) : MAX_COST_PER_EVAL;
  const normCaps = { maxLatency, maxCost };

  // Score each candidate
  const results = scoredCandidates.map(({ candidate, stats: rawStats, hasData }) => {
    let stats = rawStats;
    // For registry-only candidates, inject estimated cost into stats (clone to avoid mutation)
    if (!hasData && stats._estimated_cost_per_call) {
      stats = { ...stats };
      stats.total_cost = stats._estimated_cost_per_call;
      stats.total_evals = 1; // so avgCost = _estimated_cost_per_call
    }
    const { compositeScore, breakdown } = computeScore(stats, taskType, normCaps);
    return {
      ...candidate,
      compositeScore,
      breakdown,
      evalCount: hasData ? stats.total_evals : 0,
      hasEvalData: hasData,
    };
  });

  // Sort by composite score descending
  results.sort((a, b) => b.compositeScore - a.compositeScore);

  return results.slice(0, count);
}

/**
 * Compute a quality score from multiple evaluation signals.
 * @param {Object} evaluation
 * @param {Object} [evaluation.evaluatorScores] - { correctness, completeness, relevance, clarity } (1-5)
 * @param {string} [evaluation.selfCritique] - 'skipped' | 'no-issues' | 'refined'
 * @param {string|null} [evaluation.userFeedback] - 'positive' | 'negative' | null
 * @returns {number} Score between 0 and 1, rounded to 3 decimal places
 */
function computeQualityScore(evaluation) {
  let score = 0.7; // baseline

  if (evaluation.evaluatorScores) {
    const { correctness = 3, completeness = 3, relevance = 3, clarity = 3 } = evaluation.evaluatorScores;
    const avg = correctness * 0.4 + completeness * 0.3 + relevance * 0.15 + clarity * 0.15;
    score = avg / 5.0;
  }

  if (evaluation.selfCritique === 'no-issues') score = Math.min(score + 0.05, 1.0);
  if (evaluation.selfCritique === 'refined') score = Math.max(score - 0.1, 0);

  if (evaluation.userFeedback === 'positive') score = Math.min(score + 0.1, 1.0);
  if (evaluation.userFeedback === 'negative') score = Math.max(score - 0.2, 0);

  return Math.round(score * 1000) / 1000;
}

/**
 * Compute weighted average of evaluator dimension scores.
 * @param {Object} scores - { correctness, completeness, relevance, clarity } (1-5)
 * @returns {number} Weighted average (not normalized)
 */
function computeWeightedEvaluatorAvg(scores) {
  const { correctness = 3, completeness = 3, relevance = 3, clarity = 3 } = scores;
  return correctness * 0.4 + completeness * 0.3 + relevance * 0.15 + clarity * 0.15;
}

/**
 * Check whether a model should be promoted, demoted, or warned.
 * @param {number} avgScore - Average quality score (0-1)
 * @param {number} evalCount - Number of evaluations
 * @returns {'promote'|'demote'|'warning'|'stable'}
 */
function checkPromotionDemotion(avgScore, evalCount) {
  if (avgScore >= 0.8 && evalCount >= 50) return 'promote';
  if (avgScore < 0.5 && evalCount >= 30) return 'demote';
  if (avgScore >= 0.5 && avgScore < 0.65 && evalCount >= 30) return 'warning';
  return 'stable';
}

module.exports = {
  computeScore,
  selectBestModels,
  getWeights,
  TASK_WEIGHTS,
  defaultStatsFromRegistry,
  computeQualityScore,
  computeWeightedEvaluatorAvg,
  checkPromotionDemotion,
};
