'use strict';

const { assessComplexity } = require('./complexity');
const { runQuorum } = require('./quorum-evaluator');
const { selectBestModels } = require('./scorecard');
const { complexityToModelTier, isCritical } = require('./tier-selector');
const { selfCritique, shouldSelfCritique } = require('./self-critique');
const { createClient, getDefaultClient } = require('../llm/client');
const { default: health, classifyError } = require('../llm/health');
const { default: providerAvailability } = require('../llm/provider-availability');

// ---------------------------------------------------------------------------
// Daily evaluation budget tracker
// ---------------------------------------------------------------------------

let _dailyEvalCost = 0;
let _dailyResetDate = new Date().toISOString().slice(0, 10);

function getDailyBudget() {
  const today = new Date().toISOString().slice(0, 10);
  if (today !== _dailyResetDate) {
    _dailyEvalCost = 0;
    _dailyResetDate = today;
  }
  const limit = parseFloat(process.env.WALLE_EVAL_BUDGET_DAILY || '0');
  return { spent: _dailyEvalCost, limit: limit || Infinity, exceeded: limit > 0 && _dailyEvalCost >= limit };
}

function recordEvalCost(cost) {
  getDailyBudget(); // ensure date reset
  _dailyEvalCost += cost;
}

// ---------------------------------------------------------------------------
// Candidate builder
// ---------------------------------------------------------------------------

/**
 * Build a candidate list from brain's model registry + providers.
 * @param {Object} brain
 * @returns {Array} - Array of candidate objects
 */
function buildCandidates(brain) {
  const models = brain.listAllModels();
  return models.map(m => {
    const provider = brain.getModelProvider(m.provider_id);
    return {
      registryId: m.id,
      providerId: m.provider_id,
      providerType: provider?.type || 'anthropic',
      modelId: m.model_id,
      apiKey: provider?.api_key_encrypted,
      baseUrl: provider?.base_url,
      // Pass through registry fields for scorecard defaults
      speed_tier: m.speed_tier,
      cost_per_1m_input: m.cost_per_1m_input,
    };
  }).filter(c => c.apiKey || c.providerType === 'ollama');
}

// ---------------------------------------------------------------------------
// Single-model call
// ---------------------------------------------------------------------------

async function callSingleModel(candidate, options, clientFactory) {
  const factory = clientFactory || createClient;
  const client = factory(candidate.providerType, {
    apiKey: candidate.apiKey,
    baseUrl: candidate.baseUrl,
  });

  const start = Date.now();
  try {
    const response = await client.chat({
      model: candidate.modelId,
      messages: [{ role: 'user', content: options.prompt }],
      system: options.system,
      tools: options.tools,
      maxTokens: options.maxTokens || 4096,
      temperature: options.temperature,
      signal: options.signal,
    });
    const latencyMs = Date.now() - start;

    health.trackCall(candidate.providerId, true);

    return {
      content: response.content || '',
      toolCalls: response.toolCalls || [],
      stopReason: response.stopReason || 'end_turn',
      usage: response.usage || { input: 0, output: 0 },
      latencyMs: response.latencyMs || latencyMs,
      model: response.model || candidate.modelId,
      provider: response.provider || candidate.providerType,
    };
  } catch (err) {
    health.trackCall(candidate.providerId, false, err);
    throw err;
  }
}

// ---------------------------------------------------------------------------
// Default fallback
// ---------------------------------------------------------------------------

async function callDefaultFallback(options) {
  const client = getDefaultClient();
  const start = Date.now();
  const response = await client.chat({
    messages: [{ role: 'user', content: options.prompt }],
    system: options.system,
    tools: options.tools,
    maxTokens: options.maxTokens || 4096,
    temperature: options.temperature,
    signal: options.signal,
  });
  const latencyMs = Date.now() - start;

  return {
    content: response.content || '',
    toolCalls: response.toolCalls || [],
    stopReason: response.stopReason || 'end_turn',
    usage: response.usage || { input: 0, output: 0 },
    latencyMs: response.latencyMs || latencyMs,
    model: response.model || 'default',
    provider: response.provider || 'anthropic',
  };
}

// ---------------------------------------------------------------------------
// Health-aware failover helper
// ---------------------------------------------------------------------------

/**
 * Get a candidate with fallback based on provider health.
 * Tries the preferred candidate first if healthy, then falls back to the next
 * healthy provider from the health tracker.
 * @param {Object} preferred - Preferred candidate
 * @param {Array} allCandidates - All available candidates
 * @returns {Array} - Ordered list of candidates to try (preferred first if healthy)
 */
function getCandidatesWithFallback(preferred, allCandidates) {
  const ordered = [];

  // Try preferred first if its provider is healthy
  if (health.isHealthy(preferred.providerId)) {
    ordered.push(preferred);
  }

  // Add remaining healthy candidates, sorted by health status
  const healthyProviderIds = health.getHealthyProviders();
  for (const providerId of healthyProviderIds) {
    if (providerId === preferred.providerId) continue;
    const candidate = allCandidates.find(c => c.providerId === providerId);
    if (candidate) ordered.push(candidate);
  }

  // If preferred was unhealthy, add it last as final resort
  if (!health.isHealthy(preferred.providerId)) {
    ordered.push(preferred);
  }

  // Deduplicate (in case of overlap)
  const seen = new Set();
  return ordered.filter(c => {
    const key = c.registryId || c.providerId + ':' + c.modelId;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

// ---------------------------------------------------------------------------
// Main router
// ---------------------------------------------------------------------------

/**
 * Route a request to the best model or quorum.
 * @param {Object} options
 * @param {string} options.prompt - The prompt text
 * @param {string} [options.system] - System prompt
 * @param {Object[]} [options.tools] - Tools in Anthropic format
 * @param {string} [options.taskType='chat'] - Task type for scoring
 * @param {number} [options.maxTokens=4096]
 * @param {number} [options.temperature]
 * @param {AbortSignal} [options.signal]
 * @param {Object} [deps] - Injectable dependencies { brain, clientFactory, defaultClientFactory }
 * @returns {Promise<Object>} - Normalized response with model metadata
 */
async function routeRequest(options, deps = {}) {
  const { brain, clientFactory, defaultClientFactory } = deps;
  const taskType = options.taskType || 'chat';

  // 1. Assess complexity
  const complexity = assessComplexity({
    content: options.prompt,
    type: taskType,
  });
  let { tier, quorumSize } = complexity;

  // 2. Check model_defaults for taskType
  let strategy = quorumSize > 1 ? 'quorum' : 'single';
  let defaultModel = null;
  let strategyOverridden = false;
  if (brain) {
    defaultModel = brain.getModelDefault(taskType);
    if (defaultModel) {
      // model_defaults can override strategy
      if (defaultModel.strategy) {
        strategy = defaultModel.strategy;
        strategyOverridden = true;
      }
      const defaultQuorumSize = defaultModel.quorumSize ?? defaultModel.quorum_size;
      if (defaultQuorumSize) {
        quorumSize = defaultQuorumSize;
      } else if (defaultModel.strategy === 'single') {
        quorumSize = 1;
      }
    }
  }

  // Build candidates if brain is available
  const candidates = brain ? buildCandidates(brain) : [];

  // Include shadow-proven models as candidates
  try {
    if (brain && brain.getModelTaskScores) {
      const taskScores = brain.getModelTaskScores(taskType);
      for (const score of taskScores) {
        if (score.avg_score >= 0.75 && score.total_evals >= 50) {
          // Check rollout_pct for gradual rollout
          if (score.rollout_pct != null && score.rollout_pct < 1.0 && Math.random() > (score.rollout_pct || 0)) continue;
          // Don't duplicate if already a candidate
          if (candidates.some(c => c.modelId === score.model)) continue;
          candidates.push({
            registryId: `shadow::${score.model}`,
            providerId: score.provider || 'ollama',
            providerType: score.provider || 'ollama',
            modelId: score.model,
            speed_tier: 2, // local models are typically fast
            cost_per_1m_input: 0,
            _shadowScore: score,
          });
        }
      }
    }
  } catch {}

  // Include benchmark-proven models as candidates
  try {
    if (brain && brain.getBenchmarkLeaderboard) {
      const benchLb = brain.getBenchmarkLeaderboard({ days: 30 });
      for (const entry of benchLb) {
        const trustedEvals = Number(entry.trusted_evals) || 0;
        const qualityScore = entry.trusted_avg_score != null ? Number(entry.trusted_avg_score) : null;
        if (qualityScore != null && qualityScore >= 0.7 && trustedEvals >= 10) {
          const modelId = entry.model;
          if (candidates.some(c => c.modelId === modelId)) continue;
          candidates.push({
            registryId: `bench::${entry.provider}/${modelId}`,
            providerId: entry.provider,
            providerType: entry.provider,
            modelId,
            speed_tier: 2,
            cost_per_1m_input: 0,
            _benchScore: { ...entry, quality_score: qualityScore },
          });
        }
      }
    }
  } catch {}

  // --- Health pre-filter ---
  // Lazy refresh: probe providers that haven't been checked recently
  try {
    const probeFn = async (providerId) => {
      const state = providerAvailability.getProviderState(providerId);
      if (!state) return false;
      try {
        const client = createClient(state.providerType, {
          apiKey: state.apiKey,
          baseUrl: state.baseUrl,
        });
        await client.listModels();
        return true;
      } catch { return false; }
    };
    await providerAvailability.ensureFreshAll(probeFn);
  } catch {
    // Health refresh failed — continue with stale data
  }

  // Filter candidates by provider health
  const healthyCandidates = candidates.filter(c => {
    const state = providerAvailability.getProviderState(c.providerId);
    return !state || state.status !== 'unhealthy';
  });
  const activeCandidates = healthyCandidates.length > 0 ? healthyCandidates : candidates;
  if (healthyCandidates.length === 0 && candidates.length > 0) {
    console.log('[router] All providers unhealthy — trying all as last resort');
  }

  // Provider mode detection
  const providerTypes = new Set(activeCandidates.map(c => c.providerType || c.type));
  const providerMode = providerTypes.size >= 2 ? 'multi' : 'single';

  // Tier selection from complexity
  const modelTier = complexity.modelTier || complexityToModelTier(tier);
  const critical = isCritical(tier);

  // 5. Fallback: no models configured -> use default client
  if (activeCandidates.length === 0 && candidates.length === 0) {
    try {
      const fallbackFactory = defaultClientFactory || callDefaultFallback;
      const response = await fallbackFactory(options);
      return {
        ...response,
        routing: {
          tier,
          modelTier,
          complexityTier: tier,
          quorumSize,
          strategy: 'default-fallback',
          providerMode: 'none',
          selfCritique: 'skipped',
          refinedAlready: false,
          quorum: null,
          candidatesConsidered: 0,
          selectedModel: response.model || 'default',
        },
      };
    } catch (err) {
      throw new Error(`Router fallback failed: ${err.message}`);
    }
  }

  // 3. Single model (with health-aware failover)
  if (strategy === 'single' || quorumSize === 1) {
    // Get best model from scorecard (use activeCandidates for health-filtered pool)
    const best = selectBestModels(taskType, activeCandidates, brain, 1);
    const selected = best[0] || activeCandidates[0];

    // Build health-ordered candidate list for failover
    const orderedCandidates = getCandidatesWithFallback(selected, activeCandidates);
    let lastError = null;

    for (const candidate of orderedCandidates) {
      try {
        const response = await callSingleModel(candidate, options, clientFactory);

        // Record success with provider availability
        providerAvailability.recordSuccess(candidate.providerId);

        // --- Budget check ---
        const budget = getDailyBudget();
        const skipEval = budget.exceeded;
        if (skipEval) {
          console.log('[router] Daily evaluation budget exceeded — skipping self-critique and quorum');
        }

        // --- Self-critique (quality tier only) ---
        let refinedAlready = false;
        let selfCritiqueResult = { outcome: 'skipped' };

        const suppressEvaluators = strategyOverridden && strategy === 'single';

        if (!skipEval && !suppressEvaluators && shouldSelfCritique(modelTier)) {
          try {
            selfCritiqueResult = await selfCritique({
              userMessage: options.prompt || options.userMessage,
              workerResponse: response.content,
              workerModel: response.model,
              workerProvider: response.provider,
              modelTier,
              availableProviders: activeCandidates,
              refinedAlready,
              clientFactory: clientFactory || createClient,
            });
            if (selfCritiqueResult.outcome === 'refined' && selfCritiqueResult.refinedResponse) {
              response.content = selfCritiqueResult.refinedResponse;
              refinedAlready = true;
            }
          } catch (err) {
            console.error('[router] Self-critique failed:', err.message);
          }
        }

        // --- Quorum evaluation (quality + multi-provider, or critical) ---
        let quorumResult = null;
        const shouldRunQuorum = !suppressEvaluators && ((modelTier === 'quality' && providerMode === 'multi') || critical);

        if (!skipEval && shouldRunQuorum && activeCandidates.length >= 2) {
          try {
            quorumResult = await runQuorum({
              userMessage: options.prompt || options.userMessage,
              workerResponse: response.content,
              workerModel: response.model,
              workerProvider: response.provider,
              candidates: activeCandidates,
              taskType: options.taskType || 'general',
              refinedAlready,
            }, {
              clientFactory: clientFactory || createClient,
              brain,
            });

            if (quorumResult.verdict === 'refined' && quorumResult.refinedResponse) {
              response.content = quorumResult.refinedResponse;
              refinedAlready = true;
            }
          } catch (err) {
            console.error('[router] Quorum evaluation failed:', err.message);
          }
        }

        // --- Build routing metadata ---
        response.routing = {
          tier,
          modelTier,
          complexityTier: tier,
          quorumSize,
          strategy: quorumResult ? 'quorum' : (selfCritiqueResult.outcome !== 'skipped' ? 'single-self-critique' : (candidate === selected ? 'single' : 'single-failover')),
          providerMode,
          selfCritique: selfCritiqueResult.outcome,
          refinedAlready,
          quorum: quorumResult ? {
            evaluators: quorumResult.responses?.map(r => r.model) || [],
            scores: quorumResult.scores || null,
            avgScore: quorumResult.scores ? (quorumResult.scores.correctness * 0.4 + quorumResult.scores.completeness * 0.3 + quorumResult.scores.relevance * 0.15 + quorumResult.scores.clarity * 0.15) : null,
            verdict: quorumResult.verdict,
            parseFailures: quorumResult.parseFailures || 0,
          } : null,
          candidatesConsidered: activeCandidates.length,
          selectedModel: candidate.modelId,
          failedOver: candidate !== selected,
        };

        return response;
      } catch (err) {
        // Record failure with provider availability
        providerAvailability.recordFailure(candidate.providerId, err.message);

        lastError = err;
        // Only retry on retriable errors
        const classified = classifyError(err);
        if (!classified.retriable) break;
        // Continue to next candidate
      }
    }

    // All candidates failed — try default client as last resort
    try {
      const fallbackFactory = defaultClientFactory || callDefaultFallback;
      const response = await fallbackFactory(options);
      return {
        ...response,
        routing: {
          tier,
          modelTier,
          complexityTier: tier,
          quorumSize,
          strategy: 'default-fallback',
          providerMode,
          selfCritique: 'skipped',
          refinedAlready: false,
          quorum: null,
          candidatesConsidered: activeCandidates.length,
          selectedModel: response.model || 'default',
          error: lastError?.message,
        },
      };
    } catch (fallbackErr) {
      throw new Error(`Router: all providers failed (${lastError?.message}), fallback also failed (${fallbackErr.message})`);
    }
  }

  // 4. Quorum (strategy === 'quorum' with quorumSize > 1)
  try {
    const quorumResult = await runQuorum(
      {
        prompt: options.prompt,
        system: options.system,
        tools: options.tools,
        quorumSize,
        candidates: activeCandidates,
        taskType,
      },
      { clientFactory, brain }
    );

    const winnerResponse = quorumResult.responses.find(
      r => r.registryId === quorumResult.winner.registryId && r.error === null
    );
    const fallbackResponse = quorumResult.responses.find(r => r.error === null);
    const bestResponse = winnerResponse || fallbackResponse;

    if (!bestResponse) {
      throw new Error('All quorum models failed');
    }

    return {
      content: bestResponse.content || '',
      toolCalls: bestResponse.toolCalls || [],
      stopReason: bestResponse.stopReason || 'end_turn',
      usage: bestResponse.usage || { input: 0, output: 0 },
      latencyMs: bestResponse.latencyMs || 0,
      model: bestResponse.model,
      provider: bestResponse.providerType || bestResponse.provider || '',
      routing: {
        tier,
        modelTier,
        complexityTier: tier,
        quorumSize,
        strategy: 'quorum',
        providerMode,
        selfCritique: 'skipped',
        refinedAlready: false,
        quorum: quorumResult.scores ? {
          evaluators: quorumResult.responses?.map(r => r.model) || [],
          scores: quorumResult.scores,
          avgScore: quorumResult.scores.correctness * 0.4 + quorumResult.scores.completeness * 0.3 + quorumResult.scores.relevance * 0.15 + quorumResult.scores.clarity * 0.15,
          verdict: quorumResult.verdict,
          parseFailures: quorumResult.parseFailures || 0,
        } : null,
        candidatesConsidered: activeCandidates.length,
        selectedModel: bestResponse.model,
        quorumId: quorumResult.quorumId,
        consensus: quorumResult.consensus,
      },
    };
  } catch (err) {
    // Quorum failed entirely -> fallback
    try {
      const fallbackFactory = defaultClientFactory || callDefaultFallback;
      const response = await fallbackFactory(options);
      return {
        ...response,
        routing: {
          tier,
          modelTier,
          complexityTier: tier,
          quorumSize,
          strategy: 'default-fallback',
          providerMode,
          selfCritique: 'skipped',
          refinedAlready: false,
          quorum: null,
          candidatesConsidered: activeCandidates.length,
          selectedModel: response.model || 'default',
          error: err.message,
        },
      };
    } catch (fallbackErr) {
      throw new Error(`Router: quorum failed (${err.message}), fallback also failed (${fallbackErr.message})`);
    }
  }
}

module.exports = { routeRequest, buildCandidates, getCandidatesWithFallback };
