'use strict';

const { randomUUID } = require('node:crypto');
const { createClient } = require('../llm/client');

// ---------------------------------------------------------------------------
// Evaluation prompt builder
// ---------------------------------------------------------------------------

function buildEvaluationPrompt(userMessage, workerResponse) {
  return `You are evaluating an AI response. Score each dimension 1-5:

- Correctness: Are the facts and logic right?
- Completeness: Does it fully address the question?
- Relevance: Does it stay on topic without unnecessary filler?
- Clarity: Is it well-structured and easy to follow?

Original question: ${userMessage}
Response to evaluate: ${workerResponse}

Return ONLY valid JSON (no markdown fences, no preamble):
{"correctness": N, "completeness": N, "relevance": N, "clarity": N, "issues": "description or empty string", "verdict": "accept or refine"}`;
}

// ---------------------------------------------------------------------------
// Parse evaluator response (robust JSON extraction)
// ---------------------------------------------------------------------------

function parseEvaluatorResponse(text) {
  if (!text || typeof text !== 'string') return null;

  let parsed = null;

  // 1. Try direct parse
  try {
    parsed = JSON.parse(text);
  } catch {
    // 2. Try extracting from markdown fences
    const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
    if (fenceMatch) {
      try {
        parsed = JSON.parse(fenceMatch[1].trim());
      } catch {
        // fall through
      }
    }

    // 3. Regex extract first {...} block
    if (!parsed) {
      const braceMatch = text.match(/\{[^{}]*\}/);
      if (braceMatch) {
        try {
          parsed = JSON.parse(braceMatch[0]);
        } catch {
          // fall through
        }
      }
    }
  }

  if (!parsed) return null;

  // 4. Validate required fields
  const { correctness, completeness, relevance, clarity, verdict } = parsed;
  if (
    typeof correctness !== 'number' || correctness < 1 || correctness > 5 ||
    typeof completeness !== 'number' || completeness < 1 || completeness > 5 ||
    typeof relevance !== 'number' || relevance < 1 || relevance > 5 ||
    typeof clarity !== 'number' || clarity < 1 || clarity > 5 ||
    typeof verdict !== 'string'
  ) {
    return null;
  }

  return parsed;
}

// ---------------------------------------------------------------------------
// Weighted score
// ---------------------------------------------------------------------------

function computeWeightedScore(scores) {
  return (
    scores.correctness * 0.4 +
    scores.completeness * 0.3 +
    scores.relevance * 0.15 +
    scores.clarity * 0.15
  );
}

// ---------------------------------------------------------------------------
// Evaluator selection
// ---------------------------------------------------------------------------

function selectEvaluators(workerProviderType, candidates, count) {
  // Prefer different provider types
  const diffProvider = candidates.filter(c => c.providerType !== workerProviderType);

  let pool;
  if (diffProvider.length >= count) {
    pool = diffProvider;
  } else {
    // Fall back: include same-provider different-model candidates
    pool = [...diffProvider, ...candidates.filter(c => c.providerType === workerProviderType)];
  }

  // Sort by scorecard score if available, else keep order (effectively random from caller)
  pool.sort((a, b) => {
    const aScore = a._scorecardScore ?? -1;
    const bScore = b._scorecardScore ?? -1;
    return bScore - aScore;
  });

  return pool.slice(0, count);
}

// ---------------------------------------------------------------------------
// Cost estimation (copied from old quorum.js)
// ---------------------------------------------------------------------------

function estimateCost(usage, providerType) {
  if (!usage) return 0;
  const input = usage.input || 0;
  const output = usage.output || 0;
  const rates = {
    anthropic: { input: 0.000003, output: 0.000015 },
    openai: { input: 0.000002, output: 0.00001 },
    google: { input: 0.0000015, output: 0.000008 },
    ollama: { input: 0, output: 0 },
  };
  const rate = rates[providerType] || rates.openai;
  return input * rate.input + output * rate.output;
}

// ---------------------------------------------------------------------------
// Legacy consensus (for backward compat flow)
// ---------------------------------------------------------------------------

function determineConsensus(responses) {
  const successful = responses.filter(r => r.error === null && r.content);

  if (successful.length === 0) {
    return {
      consensus: { content: '', agreement: 0, method: 'single' },
      winner: { registryId: null, model: null },
    };
  }

  if (responses.length === 1) {
    const r = successful[0] || responses[0];
    return {
      consensus: {
        content: r.error === null ? r.content : '',
        agreement: r.error === null ? 1 : 0,
        method: 'single',
      },
      winner: r.error === null
        ? { registryId: r.registryId, model: r.model }
        : { registryId: null, model: null },
    };
  }

  const sorted = [...successful].sort((a, b) => {
    const lenDiff = b.content.length - a.content.length;
    if (lenDiff !== 0) return lenDiff;
    return a.latencyMs - b.latencyMs;
  });

  const best = sorted[0];
  const agreement = successful.length / responses.length;
  const allSimilar = successful.length > 1 && successful.every(r =>
    r.content.substring(0, 50) === successful[0].content.substring(0, 50)
  );

  return {
    consensus: {
      content: best.content,
      agreement,
      method: allSimilar ? 'majority' : 'best-quality',
    },
    winner: { registryId: best.registryId, model: best.model },
  };
}

function estimateConfidence(response) {
  if (response.confidence) return response.confidence;
  const hasContent = (response.content || '').length > 0;
  const completed = response.stopReason === 'end_turn';
  if (hasContent && completed) return 0.7;
  if (hasContent) return 0.5;
  return 0.3;
}

// ---------------------------------------------------------------------------
// Legacy (backward compat) flow
// ---------------------------------------------------------------------------

function selectCandidates(candidates, quorumSize, brain) {
  if (candidates.length <= quorumSize) return candidates;

  const scored = candidates.map(c => {
    const card = brain ? brain.getModelScorecard(c.registryId, 30) : null;
    const hasData = card && card.total_evals > 0 && card.avg_quality != null;
    return { candidate: c, score: hasData ? card.avg_quality : -1 };
  });

  const withData = scored.filter(s => s.score >= 0);
  if (withData.length > 0) {
    scored.sort((a, b) => b.score - a.score);
  } else {
    for (let i = scored.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [scored[i], scored[j]] = [scored[j], scored[i]];
    }
  }

  return scored.slice(0, quorumSize).map(s => s.candidate);
}

async function runLegacyQuorum(options, { clientFactory, brain } = {}) {
  const { prompt, system, tools, quorumSize, candidates, taskType } = options;
  const factory = clientFactory || createClient;
  const quorumId = randomUUID();

  const selected = selectCandidates(candidates, quorumSize, brain);

  const results = await Promise.allSettled(
    selected.map(async (candidate) => {
      const client = factory(candidate.providerType, {
        apiKey: candidate.apiKey,
        baseUrl: candidate.baseUrl,
      });
      const start = Date.now();
      const response = await client.chat({
        model: candidate.modelId,
        messages: [{ role: 'user', content: prompt }],
        system,
        tools,
        maxTokens: 4096,
      });
      const latencyMs = Date.now() - start;
      return {
        ...response,
        registryId: candidate.registryId,
        providerType: candidate.providerType,
        latencyMs,
      };
    })
  );

  const responses = results.map((result, i) => {
    const candidate = selected[i];
    if (result.status === 'fulfilled') {
      const r = result.value;
      const usage = r.usage || { input: 0, output: 0 };
      const cost = estimateCost(usage, candidate.providerType);
      return {
        registryId: r.registryId,
        model: r.model || candidate.modelId,
        content: r.content || '',
        confidence: r.confidence || estimateConfidence(r),
        latencyMs: r.latencyMs,
        cost,
        error: null,
        usage,
      };
    } else {
      return {
        registryId: candidate.registryId,
        model: candidate.modelId,
        content: '',
        confidence: 0,
        latencyMs: 0,
        cost: 0,
        error: result.reason?.message || String(result.reason),
        usage: { input: 0, output: 0 },
      };
    }
  });

  const { consensus, winner } = determineConsensus(responses);

  if (brain && taskType) {
    for (const r of responses) {
      try {
        brain.insertModelEvaluation({
          modelRegistryId: r.registryId,
          taskType,
          qualityScore: r.error === null ? r.confidence : 0,
          latencyMs: r.latencyMs,
          inputTokens: r.usage.input,
          outputTokens: r.usage.output,
          costEstimate: r.cost,
          wasSelected: winner.registryId === r.registryId,
          quorumId,
        });
      } catch (e) {
        // Don't let evaluation recording break the quorum
      }
    }
  }

  return { responses, consensus, winner, quorumId };
}

// ---------------------------------------------------------------------------
// Main: runQuorum
// ---------------------------------------------------------------------------

async function runQuorum(options, deps = {}) {
  const { clientFactory, brain } = deps;
  const userMessage = options.userMessage || options.prompt;
  const {
    workerResponse,
    workerModel,
    workerProvider,
    system,
    tools,
    quorumSize = 2,
    candidates,
    taskType,
    refinedAlready,
  } = options;

  // If no workerResponse, use backward-compat legacy flow
  if (!workerResponse) {
    return runLegacyQuorum(options, deps);
  }

  // --- NEW structured evaluation flow ---
  const factory = clientFactory || createClient;
  const quorumId = randomUUID();

  // 1. Select evaluators from different providers
  const evaluators = selectEvaluators(workerProvider, candidates, quorumSize);

  // 2. Run evaluations in parallel
  const evalPrompt = buildEvaluationPrompt(userMessage, workerResponse);

  const evalResults = await Promise.allSettled(
    evaluators.map(async (candidate) => {
      const client = factory(candidate.providerType, {
        apiKey: candidate.apiKey,
        baseUrl: candidate.baseUrl,
      });
      const response = await client.chat({
        model: candidate.modelId,
        messages: [{ role: 'user', content: evalPrompt }],
        maxTokens: 1024,
        temperature: 0,
      });
      return {
        content: response.content || '',
        model: response.model || candidate.modelId,
        provider: response.provider || candidate.providerType,
        registryId: candidate.registryId,
        usage: response.usage || { input: 0, output: 0 },
      };
    })
  );

  // 3. Parse responses
  const responses = [];
  const parsedEvals = [];
  let parseFailures = 0;

  for (let i = 0; i < evalResults.length; i++) {
    const result = evalResults[i];
    if (result.status === 'fulfilled') {
      responses.push(result.value);
      const parsed = parseEvaluatorResponse(result.value.content);
      if (parsed) {
        parsedEvals.push(parsed);
      } else {
        parseFailures++;
      }
    } else {
      parseFailures++;
      responses.push({
        content: '',
        model: evaluators[i].modelId,
        provider: evaluators[i].providerType,
        registryId: evaluators[i].registryId,
        usage: { input: 0, output: 0 },
        error: result.reason?.message || String(result.reason),
      });
    }
  }

  // 4. All evaluators failed
  if (parsedEvals.length === 0) {
    return {
      responses,
      consensus: { agreement: 0, method: 'none' },
      winner: { registryId: null, model: null },
      quorumId,
      scores: null,
      verdict: 'fallback',
      refinedResponse: null,
      parseFailures,
      lowConfidence: false,
    };
  }

  // 5. Compute averaged scores
  const avgScores = {
    correctness: 0,
    completeness: 0,
    relevance: 0,
    clarity: 0,
  };
  for (const ev of parsedEvals) {
    avgScores.correctness += ev.correctness;
    avgScores.completeness += ev.completeness;
    avgScores.relevance += ev.relevance;
    avgScores.clarity += ev.clarity;
  }
  const n = parsedEvals.length;
  avgScores.correctness /= n;
  avgScores.completeness /= n;
  avgScores.relevance /= n;
  avgScores.clarity /= n;

  const weightedAvg = computeWeightedScore(avgScores);

  // 6. Check for low confidence (score difference > 2 on any dimension)
  let lowConfidence = false;
  if (parsedEvals.length > 1) {
    for (const dim of ['correctness', 'completeness', 'relevance', 'clarity']) {
      const vals = parsedEvals.map(e => e[dim]);
      const maxVal = Math.max(...vals);
      const minVal = Math.min(...vals);
      if (maxVal - minVal > 2) {
        lowConfidence = true;
        break;
      }
    }
  }

  // 7. Determine verdict
  const allAccept = parsedEvals.every(e => e.verdict === 'accept');
  const anyRefine = parsedEvals.some(e => e.verdict === 'refine');

  let verdict;
  if (weightedAvg >= 4.0 && allAccept) {
    verdict = 'accept';
  } else if (anyRefine && !refinedAlready) {
    verdict = 'refine';
  } else if (anyRefine && refinedAlready) {
    verdict = 'accept-with-note';
  } else {
    verdict = 'accept';
  }

  // 8. Refine if needed
  let refinedResponse = null;
  if (verdict === 'refine') {
    try {
      // Use the worker's own provider/model to refine
      const workerCandidate = candidates.find(c => c.modelId === workerModel) || evaluators[0];
      const client = factory(workerCandidate.providerType, {
        apiKey: workerCandidate.apiKey,
        baseUrl: workerCandidate.baseUrl,
      });

      const issuesSummary = parsedEvals
        .map(e => e.issues)
        .filter(Boolean)
        .join('; ');

      const refineResponse = await client.chat({
        model: workerCandidate.modelId,
        messages: [
          { role: 'user', content: userMessage },
          { role: 'assistant', content: workerResponse },
          {
            role: 'user',
            content: `Your previous response was evaluated and needs improvement. Issues found: ${issuesSummary || 'Low scores on quality dimensions.'}. Please provide an improved response.`,
          },
        ],
        maxTokens: 4096,
        temperature: 0.3,
      });
      refinedResponse = refineResponse.content || null;
    } catch {
      // Refinement failed, keep original
    }
  }

  // 9. Record evaluations in brain
  if (brain && taskType) {
    for (const r of responses) {
      try {
        brain.insertModelEvaluation({
          modelRegistryId: r.registryId,
          taskType: `${taskType}:evaluation`,
          qualityScore: weightedAvg / 5,
          latencyMs: 0,
          inputTokens: r.usage?.input || 0,
          outputTokens: r.usage?.output || 0,
          costEstimate: estimateCost(r.usage, r.provider),
          wasSelected: false,
          quorumId,
        });
      } catch {
        // Don't let recording break the flow
      }
    }
  }

  // Pick a "winner" — the first evaluator that parsed successfully
  const firstGoodIdx = evalResults.findIndex((r, i) => {
    if (r.status !== 'fulfilled') return false;
    return parseEvaluatorResponse(r.value.content) !== null;
  });
  const winnerResp = firstGoodIdx >= 0 ? responses[firstGoodIdx] : responses[0];

  return {
    responses,
    consensus: {
      agreement: parsedEvals.length / evalResults.length,
      method: 'structured-evaluation',
    },
    winner: {
      registryId: winnerResp?.registryId || null,
      model: winnerResp?.model || null,
    },
    quorumId,
    scores: avgScores,
    verdict,
    refinedResponse,
    parseFailures,
    lowConfidence,
  };
}

module.exports = {
  runQuorum,
  buildEvaluationPrompt,
  parseEvaluatorResponse,
  computeWeightedScore,
  selectEvaluators,
  estimateCost,
};
