/**
 * is-unsafe v2
 *
 * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings
 * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts.
 *
 * v2 change: contexts are imported as named pattern arrays rather than resolved
 * via a string-keyed registry. This makes each context independently
 * tree-shakeable — bundlers can drop any context you never import.
 *
 * @module is-unsafe
 */

// ─── Context pattern arrays (named exports) ────────────────────────────────
// Import only the ones you need. Each is independently tree-shakeable.

export { default as HTML } from './contexts/html.js';
export { default as XML  } from './contexts/xml.js';
export { default as SVG  } from './contexts/svg.js';
export { default as SQL  } from './contexts/sql.js';
export { default as SHELL   } from './contexts/shell.js';
export { default as REDOS   } from './contexts/redos.js';
export { default as NOSQL   } from './contexts/nosql.js';
export { default as LOG     } from './contexts/log.js';

// SQL-STRICT needs a quoted identifier because of the hyphen
import SQL_STRICT from './contexts/sql-strict.js';
export { SQL_STRICT };

// ─── VALID_CONTEXTS convenience re-export ─────────────────────────────────
// Importing this pulls in ALL contexts. Use it only when you need all of them
// (e.g. for validation UI, tooling, or exhaustive audits).
// If you only need a subset, import the named contexts directly instead.

import HTML    from './contexts/html.js';
import XML     from './contexts/xml.js';
import SVG     from './contexts/svg.js';
import SQL     from './contexts/sql.js';
import SHELL   from './contexts/shell.js';
import REDOS   from './contexts/redos.js';
import NOSQL   from './contexts/nosql.js';
import LOG     from './contexts/log.js';

// ─── Attach labels to named contexts ──────────────────────────────────────
// Each built-in PatternList carries its canonical name so matchList can read
// list.label directly — no registry lookup needed at match time.
// Custom PatternLists default to 'CUSTOM' unless the caller sets list.label.

HTML.label       = 'HTML';
XML.label        = 'XML';
SVG.label        = 'SVG';
SQL.label        = 'SQL';
SQL_STRICT.label = 'SQL-STRICT';
SHELL.label      = 'SHELL';
REDOS.label      = 'REDOS';
NOSQL.label      = 'NOSQL';
LOG.label        = 'LOG';

export const VALID_CONTEXTS = Object.freeze({
  HTML,
  XML,
  SVG,
  SQL,
  'SQL-STRICT': SQL_STRICT,
  SHELL,
  REDOS,
  NOSQL,
  LOG,
});

// ─── Types ────────────────────────────────────────────────────────────────

/**
 * @typedef {{ id: string, description: string, pattern: RegExp }} Rule
 */

/**
 * @typedef {Rule[]} PatternList
 */

/**
 * @typedef {Object} MatchResult
 * @property {string} context     - Label identifying which context matched ('HTML', 'CUSTOM', etc.)
 * @property {string} id          - Rule identifier
 * @property {string} description - Human-readable description of what was matched
 * @property {RegExp} pattern     - The pattern that matched
 */

// ─── Internal helpers ──────────────────────────────────────────────────────

/**
 * @param {unknown} value
 */
function assertString(value) {
  if (typeof value !== 'string') {
    throw new TypeError(
      `is-unsafe: first argument must be a string, got ${typeof value}`
    );
  }
}

/**
 * @param {unknown} context
 */
function assertContext(context) {
  if (context instanceof RegExp) return;

  if (Array.isArray(context)) {
    if (context.length === 0) {
      throw new TypeError('is-unsafe: context must not be an empty array');
    }
    // Detect array-of-arrays vs flat pattern list
    if (Array.isArray(context[0])) {
      // Array of PatternLists
      for (const list of context) {
        if (!Array.isArray(list) || list.length === 0) {
          throw new TypeError(
            'is-unsafe: each context in the array must be a non-empty pattern array (PatternList)'
          );
        }
      }
    }
    // else: flat PatternList — trust it, no deep validation needed
    return;
  }

  throw new TypeError(
    `is-unsafe: second argument must be a PatternList (e.g. HTML), ` +
    `an array of PatternLists (e.g. [HTML, XML]), or a RegExp. Got: ${typeof context}`
  );
}

/**
 * Normalise any valid context arg into an array of PatternLists.
 *
 * @param {Rule[]|Rule[][]|RegExp} context
 * @returns {{ lists: Rule[][]|null, regex: RegExp|null }}
 */
function normalise(context) {
  if (context instanceof RegExp) return { lists: null, regex: context };
  // Distinguish PatternList (array of rule objects) from array of PatternLists
  if (Array.isArray(context[0])) return { lists: context, regex: null };
  return { lists: [context], regex: null };
}

/**
 * Test value against a single PatternList. Returns the first MatchResult or null.
 *
 * @param {string} value
 * @param {Rule[]} list
 * @returns {MatchResult|null}
 */
function matchList(value, list) {
  const label = list.label ?? 'CUSTOM';
  for (const rule of list) {
    if (rule.pattern.test(value)) {
      return { context: label, id: rule.id, description: rule.description, pattern: rule.pattern };
    }
  }
  return null;
}

// ─── Public API ───────────────────────────────────────────────────────────

/**
 * Returns `true` if `value` is unsafe in the given context(s), `false` otherwise.
 *
 * @param {string} value - The string to test
 * @param {PatternList | PatternList[] | RegExp} context
 *   - A PatternList imported from is-unsafe (e.g. `HTML`, `XML`)
 *   - An array of PatternLists — returns true if unsafe in **any** of them
 *   - A custom RegExp — returns true if the pattern matches
 * @returns {boolean}
 *
 * @example
 * import { isUnsafe, HTML, SQL } from 'is-unsafe';
 *
 * isUnsafe('<script>alert(1)</script>', HTML)       // true
 * isUnsafe('hello world', HTML)                     // false
 * isUnsafe('value', [HTML, SQL])                    // false
 * isUnsafe('value', /my-pattern/i)                  // false
 */
function isUnsafe(value, context) {
  assertString(value);
  assertContext(context);

  const { lists, regex } = normalise(context);

  if (regex) return regex.test(value);

  for (const list of lists) {
    if (matchList(value, list) !== null) return true;
  }
  return false;
}

/**
 * Like `isUnsafe`, but returns the first `MatchResult` describing **why**
 * the value was flagged, or `null` if it is safe.
 *
 * @param {string} value
 * @param {PatternList | PatternList[] | RegExp} context
 * @returns {MatchResult|null}
 *
 * @example
 * import { whyUnsafe, HTML } from 'is-unsafe';
 *
 * whyUnsafe('<script>alert(1)</script>', HTML)
 * // { context: 'HTML', id: 'html-script-open', description: '...', pattern: /.../ }
 */
function whyUnsafe(value, context) {
  assertString(value);
  assertContext(context);

  const { lists, regex } = normalise(context);

  if (regex) {
    return regex.test(value)
      ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex }
      : null;
  }

  for (const list of lists) {
    const result = matchList(value, list);
    if (result !== null) return result;
  }
  return null;
}

/**
 * Returns **all** matching rules across the given context(s), or an empty
 * array if the value is safe. Useful for comprehensive auditing.
 *
 * @param {string} value
 * @param {PatternList | PatternList[] | RegExp} context
 * @returns {MatchResult[]}
 */
function allUnsafe(value, context) {
  assertString(value);
  assertContext(context);

  const { lists, regex } = normalise(context);
  const results = [];

  if (regex) {
    if (regex.test(value)) {
      results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex });
    }
    return results;
  }

  for (const list of lists) {
    const label = list.label ?? 'CUSTOM';
    for (const rule of list) {
      if (rule.pattern.test(value)) {
        results.push({ context: label, id: rule.id, description: rule.description, pattern: rule.pattern });
      }
    }
  }

  return results;
}

export { isUnsafe, whyUnsafe, allUnsafe };
export default isUnsafe;
