'use strict';
/**
 * Per-skill model fallback chain.
 *
 * Borrowed from OpenClaw's runAgentHarnessAttemptWithFallback
 * (selection.ts:172-208) which retries an attempt against the next
 * candidate when the current one fails. OpenClaw's "candidates" are
 * harnesses; Wall-E's adaptation here is "candidate models" within a
 * single harness — different model on the same Claude API path.
 *
 * Use case: Haiku non-transient failure (refused, output too short) →
 * automatically retry with Sonnet. Without this, the skill goes into
 * exponential backoff for what may have been a model-specific issue.
 *
 * Per-skill candidates can come from:
 *   1. opts.model_candidates (caller override)
 *   2. skill.trigger_config.model_candidates (DB row)
 *   3. SKILL.md frontmatter (loaderSkill.meta.model_candidates)
 *   4. [opts.model || WALLE_MODEL || haiku-default] — single-element fallback
 *
 * Retry semantics:
 *   - Each model attempt internally goes through llm/retry.js for
 *     transient failures (429, 5xx, etc.)
 *   - On non-transient failure, advance to next candidate
 *   - All attempts captured in returned `attempts` array
 *   - Throws the last error if all candidates fail
 *
 * Item F of skill-dispatch deep-dive.
 */

let telemetry;
try { telemetry = require('../telemetry'); } catch { telemetry = { track() {} }; }
const { getDefaultModel, getDefaultProviderType, resolveCompatibleModel } = require('../llm/client');
const { classifyProviderError } = require('../llm/provider-error');

function _resolveCandidates(skill, opts) {
  if (Array.isArray(opts?.model_candidates) && opts.model_candidates.length) {
    return opts.model_candidates;
  }

  // Skill DB row's trigger_config
  if (skill?.trigger_config) {
    try {
      const cfg = JSON.parse(skill.trigger_config);
      if (Array.isArray(cfg.model_candidates) && cfg.model_candidates.length) {
        return cfg.model_candidates;
      }
    } catch {}
  }

  // SKILL.md frontmatter — try the loader if available
  try {
    const { findSkill } = require('./skill-loader');
    const loaderSkill = skill?.name ? findSkill(skill.name) : null;
    if (loaderSkill) {
      // Item B (multi-agent deep-dive): model_policy is the structured form.
      // Prefer it over the legacy meta.model_candidates array.
      if (loaderSkill.model_policy) {
        const policy = loaderSkill.model_policy;
        const list = [];
        if (typeof policy.primary === 'string') list.push(policy.primary);
        if (typeof policy.fallback === 'string') list.push(policy.fallback);
        else if (Array.isArray(policy.fallback)) list.push(...policy.fallback);
        if (list.length > 0) return list;
      }
      if (Array.isArray(loaderSkill.meta?.model_candidates)) {
        return loaderSkill.meta.model_candidates;
      }
    }
  } catch {}

  // Single-model fallback (no chain)
  const providerType = opts?.provider || opts?.providerType || getDefaultProviderType();
  const single = resolveCompatibleModel(opts?.model || process.env.WALLE_MODEL || getDefaultModel(), providerType);
  return [single];
}

/**
 * Run a skill, falling back through a list of model candidates on
 * non-transient failure.
 *
 * @param {Object} skill - skill row
 * @param {Object} [opts] - same as runSkill, plus optional model_candidates
 * @param {Function} [opts._runner] - injectable runner for tests
 * @returns {Promise<Object>} run result, with `attempts` populated
 */
async function runSkillWithFallback(skill, opts = {}) {
  const candidates = _resolveCandidates(skill, opts);
  const attempts = [];
  let lastError = null;

  for (let i = 0; i < candidates.length; i++) {
    const model = candidates[i];
    const attemptOpts = Object.assign({}, opts, { model });
    try {
      const runner = opts._runner || (await _defaultRunner());
      const result = await runner(skill, attemptOpts);
      attempts.push({ model, status: 'success' });
      if (candidates.length > 1 || i > 0) {
        try {
          telemetry.track('skill_fallback_attempt', {
            skill: skill?.name,
            model,
            status: 'success',
            attempt_index: i,
            total_attempts: candidates.length,
            dispatch_source: opts.dispatch_source || 'unknown',
          });
        } catch {}
      }
      return Object.assign({}, result, { attempts });
    } catch (err) {
      lastError = err;
      const providerError = classifyProviderError(err, {
        provider: opts.provider || opts.providerType || getDefaultProviderType(),
        model,
      });
      attempts.push({ model, status: 'failure', error: err.message, error_type: providerError.type });
      try {
        telemetry.track('skill_fallback_attempt', {
          skill: skill?.name,
          model,
          status: 'failure',
          error: String(err.message || '').slice(0, 300),
          error_type: providerError.type,
          attempt_index: i,
          total_attempts: candidates.length,
          dispatch_source: opts.dispatch_source || 'unknown',
        });
      } catch {}
      // Continue to next candidate, unless this was the last
    }
  }

  // All candidates failed
  const err = new Error(
    `All ${candidates.length} model candidate(s) failed for skill "${skill?.name || 'unknown'}". `
    + `Last error: ${lastError?.message || 'unknown'}`
  );
  err.attempts = attempts;
  err.cause = lastError;
  throw err;
}

let _cachedRunner = null;
async function _defaultRunner() {
  if (_cachedRunner) return _cachedRunner;
  const { runSkill } = require('./skill-executor');
  _cachedRunner = runSkill;
  return _cachedRunner;
}

module.exports = {
  runSkillWithFallback,
  _resolveCandidates, // exported for tests
};
