// Pure, dependency-free helpers for the setup wizard. Tested by setup-lib.test.mjs.

export const ALL_ROLES = ['auto', 'fast', 'quality', 'title', 'reflection'];
export const CONNECTION_TYPES = ['direct', 'aao_gateway'];

// Normalize a user-entered LLM endpoint URL.
// Returns { endpoint, base } or { error }.
//   endpoint = scheme://host[:port]/path with no trailing slash (used for {endpoint}/models)
//   base     = endpoint with a trailing /v1 stripped (used for {base}/api/tags), mirroring src/worker.ts
export function parseEndpoint(input) {
  const raw = String(input ?? '').trim();
  if (!raw) return { error: 'endpoint is required' };
  const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`;
  let url;
  try { url = new URL(withScheme); } catch { return { error: `invalid URL: ${raw}` }; }
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    return { error: `unsupported scheme: ${url.protocol.replace(':', '')}` };
  }
  if (!url.hostname) return { error: 'missing host' };
  const endpoint = url.toString().replace(/\/+$/, '');
  const base = endpoint.replace(/\/v1$/, '');
  return { endpoint, base };
}

// Build the single llm.workers[0] entry the wizard writes.
export function buildWorkerEntry({ connectionType, endpoint, model, apiKey }) {
  const entry = {
    id: connectionType === 'aao_gateway' ? 'gateway' : 'local-llm',
    connection_type: connectionType,
    endpoint,
    model,
    roles: [...ALL_ROLES],
    max_concurrency: 1,
    enabled: true,
    vlm: false,
  };
  if (connectionType === 'aao_gateway') entry.api_key = apiKey;
  return entry;
}

// Quote a scalar when a YAML plain scalar would be ambiguous or invalid.
function yamlScalar(v) {
  if (typeof v === 'boolean' || typeof v === 'number') return String(v);
  const s = String(v);
  const needsQuote =
    s === '' ||
    /[:#\[\]{}&*!|>'"%@`,]/.test(s) ||      // structural / indicator chars (note: ':' covers host:port, model:tag)
    /^[\s]|[\s]$/.test(s) ||                 // leading/trailing space
    /^[-?]/.test(s) ||                       // leading '-' or '?'
    /^(true|false|null|yes|no|on|off|~)$/i.test(s) ||
    /^[0-9.+-]/.test(s) ||                   // could be parsed as a number
    /[\n\r\t]/.test(s);                      // control chars that would break multiline YAML
  if (!needsQuote) return s;
  return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"`;
}

// Render a minimal config.yaml (config_version + one llm.workers entry).
// We do NOT edit config.yaml.example (fragile + contradicts the minimal goal).
export function renderConfigYaml(worker) {
  const L = [];
  L.push('# Generated by `npm run setup`. Full reference: config.yaml.example');
  L.push('config_version: 2');
  L.push('');
  L.push('llm:');
  L.push('  workers:');
  L.push(`    - id: ${yamlScalar(worker.id)}`);
  L.push(`      connection_type: ${yamlScalar(worker.connection_type)}`);
  L.push(`      endpoint: ${yamlScalar(worker.endpoint)}`);
  L.push(`      model: ${yamlScalar(worker.model)}`);
  if (worker.api_key !== undefined) L.push(`      api_key: ${yamlScalar(worker.api_key)}`);
  L.push(`      roles: [${worker.roles.map(yamlScalar).join(', ')}]`);
  L.push(`      max_concurrency: ${worker.max_concurrency}`);
  L.push(`      enabled: ${worker.enabled}`);
  L.push(`      vlm: ${worker.vlm}`);
  L.push('');
  return L.join('\n');
}

// Upsert only PORT in a .env file; never write OLLAMA_* (those hit the legacy
// provider.* env override path in src/config.ts and would conflict with llm.workers).
export function renderDotenv(existingText, { port }) {
  const line = `PORT=${port}`;
  const text = String(existingText ?? '');
  if (!text.trim()) return `${line}\n`;
  const kept = text.replace(/\n$/, '').split('\n').filter((l) => !/^\s*PORT\s*=/.test(l));
  kept.push(line);
  return `${kept.join('\n')}\n`;
}

// Resolve + validate non-interactive answers from env. Single endpoint env for both types.
export function parseAnswersFromEnv(env) {
  const connectionType = env.SETUP_CONNECTION_TYPE || 'direct';
  if (!CONNECTION_TYPES.includes(connectionType)) {
    return { error: `SETUP_CONNECTION_TYPE must be one of ${CONNECTION_TYPES.join(', ')}` };
  }
  const ep = parseEndpoint(env.SETUP_LLM_ENDPOINT || '');
  if (ep.error) return { error: `SETUP_LLM_ENDPOINT: ${ep.error}` };
  const model = String(env.SETUP_MODEL || '').trim();
  if (!model) return { error: 'SETUP_MODEL is required in --yes mode' };
  let apiKey;
  if (connectionType === 'aao_gateway') {
    apiKey = String(env.SETUP_LLM_API_KEY || '').trim();
    if (!apiKey) return { error: 'SETUP_LLM_API_KEY is required for aao_gateway' };
  }
  const portRaw = env.PORT || '9876';
  const port = Number(portRaw);
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
    return { error: `PORT must be an integer 1-65535 (got ${portRaw})` };
  }
  return { answers: { connectionType, endpoint: ep.endpoint, base: ep.base, model, apiKey, port } };
}

// Probe model availability the same way the runtime worker does (src/worker.ts):
// 1) GET {base}/api/tags  -> Ollama  { models: [{ name }] }
// 2) GET {endpoint}/models -> OpenAI { data:   [{ id }] }
// Bearer is forwarded when apiKey is set (AAO gateway requires it).
export async function probeModels({ endpoint, base, apiKey, fetchImpl = fetch, timeoutMs = 5000 }) {
  const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
  const tryFetch = async (url, parse) => {
    const ac = new AbortController();
    const timer = setTimeout(() => ac.abort(), timeoutMs);
    try {
      const r = await fetchImpl(url, { headers, signal: ac.signal });
      if (!r || !r.ok) return { ok: false, error: `HTTP ${r ? r.status : 'no-response'} from ${url}` };
      let data;
      try { data = await r.json(); } catch { return { ok: false, error: `non-JSON response from ${url}` }; }
      return { ok: true, models: parse(data) };
    } catch (e) {
      const why = e && e.name === 'AbortError' ? 'timeout' : (e && e.message) || 'fetch failed';
      return { ok: false, error: `${url}: ${why}` };
    } finally {
      clearTimeout(timer);
    }
  };

  const ollama = await tryFetch(`${base}/api/tags`,
    (d) => (d && Array.isArray(d.models) ? d.models.map((m) => m && m.name).filter(Boolean) : []));
  if (ollama.ok) return { ok: true, models: ollama.models };

  const openai = await tryFetch(`${endpoint}/models`,
    (d) => (d && Array.isArray(d.data) ? d.data.map((m) => m && m.id).filter(Boolean) : []));
  if (openai.ok) return { ok: true, models: openai.models };

  return { ok: false, models: [], error: openai.error || ollama.error };
}
