'use strict';
/**
 * advisor-errors — stop the Tune advisors turning a failed query into "no findings".
 *
 * WHY
 * ---
 * Every advisor swallowed per-query failures:
 *     sgaAdvice = mapSga(await queryViaAgent(connId, creds, SGA_SQL).catch(() => []));
 * so an ORA-00942 from a missing grant, an unlicensed pack, or a dropped connection
 * became [] — and [] renders as "nothing to optimise". On a tuning product that reads
 * as a CLEAN BILL OF HEALTH for a check that never ran. Same false all-clear as the
 * blank health-check panels and the Deep EBS "Demo data" page.
 *
 * The comment at those sites said "individual query failures are non-fatal (view may
 * not apply)". Non-fatal is right — the advisor should still return its other sections.
 * Silent is the bug.
 *
 * VOCABULARY
 * ----------
 * Classes match oracle-proxy.py `_classify_check_error` and the report's
 * renderCheckErrors() META map, so one shared wording covers agent-side checks and
 * cloud-side advisors: tunevault_bug | permission | permission_or_missing |
 * not_licensed | not_applicable | system | unknown.
 */

const CLASSES = {
  tunevault_bug: 'A TuneVault query is malformed for this Oracle version. Nothing is wrong with your database. Please report it — we treat these as defects.',
  permission: 'The monitoring account lacks a privilege this check needs. See the role script at /setup/role-script.',
  permission_or_missing: 'The object is either not granted to the monitoring account or does not exist on this database (edition, version, or configuration). Check the grants in /setup/role-script first.',
  not_licensed: 'This advisor view requires a licensed Oracle management pack. Nothing is wrong with your database.',
  not_applicable: 'This view does not exist on this edition, version or configuration.',
  system: 'The database or listener had a problem while this query ran.',
  unknown: 'Not yet classified by TuneVault.',
};

function classify(err) {
  const msg  = String((err && err.message) || err || '');
  const m    = msg.match(/(ORA-\d{5})/);
  const code = m ? m[1] : '';
  const detail = msg.trim().split('\n')[0].slice(0, 300);

  // Malformed SQL is OURS, never the customer's problem.
  if (['ORA-00904', 'ORA-00933', 'ORA-00936', 'ORA-00907', 'ORA-00918', 'ORA-00979'].includes(code))
    return { code, cls: 'tunevault_bug', detail };
  if (code === 'ORA-01031') return { code, cls: 'permission', detail };
  if (code === 'ORA-00942') return { code, cls: 'permission_or_missing', detail };
  // ORA-00439 = feature not enabled for this edition; 12504/12505 handled as system below.
  if (code === 'ORA-00439') return { code, cls: 'not_applicable', detail };
  if (/not licensed|management pack|diagnostic\+tuning|DIAGNOSTIC/i.test(msg) && !code)
    return { code, cls: 'not_licensed', detail };
  if (['ORA-01034','ORA-03113','ORA-03114','ORA-12541','ORA-12514','ORA-12537','ORA-12170','ORA-01089',
       'ORA-01013','ORA-00054','ORA-04021','ORA-01555','ORA-08181'].includes(code))
    return { code, cls: 'system', detail };
  return { code, cls: 'unknown', detail };
}

/**
 * @returns {{errors: Array, catcher: Function, record: Function, hasBugs: Function}}
 */
/**
 * @param advisorName  used in the log line and in each error record
 * @param opts.budgetMs TOTAL wall-clock budget for all queries in one request.
 *
 * The budget exists because per-query catching ALONE made things worse. Wrapping each
 * query so it fails softly means a slow advisor no longer stops at the first timeout —
 * it swallows it and starts the next one, burning the agent timeout again per query
 * until the 120s global request backstop returns a 503 with NO body. Measured on an EBS
 * lab: 500 at 60-80s before, 503 at 120s after. Strictly worse, because the 503 loses
 * the error detail entirely.
 *
 * Default 75s leaves comfortable headroom under the 120s backstop for response
 * assembly, so the advisor always answers with whatever it managed to collect.
 */
function makeCollector(advisorName, opts) {
  const errors = [];
  const budgetMs = (opts && opts.budgetMs) || 75000;
  const deadline = Date.now() + budgetMs;

  function record(label, err) {
    const { code, cls, detail } = classify(err);
    errors.push({
      check: label,
      advisor: advisorName,
      ora_code: code,
      class: cls,
      message: detail,
      remedy: CLASSES[cls] || CLASSES.unknown,
    });
    // One line per failed query — these were previously invisible in the logs too.
    console.warn('[%s] %s failed: %s%s', advisorName, label, code ? code + ' ' : '', detail);
  }

  /**
   * Drop-in for `.catch(() => [])`:
   *     .catch(errs.catcher('SGA advice'))
   * Returns the fallback so downstream mapping code is unchanged.
   */
  function catcher(label, fallback) {
    const fb = arguments.length > 1 ? fallback : [];
    return function (err) { record(label, err); return fb; };
  }

  function hasBugs() { return errors.some(e => e.class === 'tunevault_bug'); }
  function remainingMs() { return Math.max(0, deadline - Date.now()); }

  /**
   * Run one query inside the SHARED budget.
   *   rows = await errs.guard('Usage', () => queryViaAgent(...), []);
   * If the budget is already spent the query is NOT issued at all — it is recorded as
   * skipped and the fallback returned, so the remaining sections still render and the
   * response still beats the global backstop. Otherwise the query races the remaining
   * budget, so no single slow query can consume the whole request.
   */
  function guard(label, fn, fallback) {
    const fb = arguments.length > 2 ? fallback : [];
    const left = remainingMs();
    if (left <= 0) {
      record(label, new Error('Skipped: advisor time budget exhausted before this query ran'));
      return Promise.resolve(fb);
    }
    let timer;
    const timeout = new Promise((_, reject) => {
      timer = setTimeout(
        () => reject(new Error('Exceeded remaining advisor time budget (' + Math.round(left / 1000) + 's)')),
        left);
    });
    return Promise.race([Promise.resolve().then(fn), timeout])
      .then(v => { clearTimeout(timer); return v; })
      .catch(err => { clearTimeout(timer); record(label, err); return fb; });
  }

  return { errors, record, catcher, guard, hasBugs, remainingMs };
}

module.exports = { makeCollector, classify, CLASSES };
