/**
 * Forbids `throw new Error(...)` and related untyped builtins inside connector
 * source. Connectors must throw typed errors from `@unito/integration-sdk`
 * (e.g. `HttpErrors.UnprocessableEntityError`, `HttpErrors.NotFoundError`,
 * domain-specific subclasses).
 *
 * Bad:
 *   throw new Error('Item not found');
 *   throw Error(`Unsupported item type "${type}"`);
 *   throw new TypeError('Invalid input');
 *
 * Good:
 *   throw new HttpErrors.NotFoundError('Item not found');
 *   throw new HttpErrors.UnprocessableEntityError(`Unsupported item type "${type}"`);
 *
 * Why:
 *   1. Typed errors carry HTTP status codes — the SDK maps them to the correct
 *      response. Raw `Error` becomes a generic 500.
 *   2. Typed errors carry structured fields (`category`, `code`, `subCategory`,
 *      `additionalErrorData`) that the structured logger extracts via its
 *      whitelist. Raw `Error` only logs the message.
 *   3. Downstream code can branch on `instanceof` — raw `Error` forces string
 *      matching on the message, which is brittle.
 *
 * Allowlisted files (legitimate raw `Error` use):
 *   - `**\/cache.ts`             — internal hash-helper guards
 *   - `**\/script(s)/**`         — one-off CLI tooling, not request-path
 *   - `**\/errors.ts`            — defines the typed error classes themselves
 *   - `**\/helpers/stringHelper.ts` — pure utility, not request-path
 *
 * Allowlisted message substrings:
 *   - `/only supported in tests/i` — dev-mode-only guard
 *   - `/assertNever/`              — exhaustiveness check helper
 */
import type { Rule } from 'eslint';
import type * as ESTree from 'estree';

const RAW_ERROR_NAMES = new Set<string>(['Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError']);

const ALLOWLISTED_FILE_PATTERNS = [/\/cache\.ts$/, /\/scripts?\//, /\/errors\.ts$/, /\/helpers\/stringHelper\.ts$/];

const ALLOWLISTED_MESSAGE_PATTERNS = [/only supported in tests/i, /assertNever/];

function isFileAllowlisted(filename: string): boolean {
  return ALLOWLISTED_FILE_PATTERNS.some(re => re.test(filename));
}

function isMessageAllowlisted(arg: ESTree.Node | undefined, sourceCode: Rule.RuleContext['sourceCode']): boolean {
  if (!arg) {
    return false;
  }
  const text = sourceCode.getText(arg);
  return ALLOWLISTED_MESSAGE_PATTERNS.some(re => re.test(text));
}

function getThrownConstructorName(arg: ESTree.Node | null | undefined): string | null {
  if (!arg) {
    return null;
  }
  const isConstructorLike = arg.type === 'NewExpression' || arg.type === 'CallExpression';
  if (!isConstructorLike) {
    return null;
  }
  const callExpr = arg as ESTree.NewExpression | ESTree.CallExpression;
  if (!callExpr.callee || callExpr.callee.type !== 'Identifier') {
    return null;
  }
  return callExpr.callee.name;
}

const rule: Rule.RuleModule = {
  meta: {
    type: 'problem',
    docs: {
      description:
        'Disallow `throw new Error(...)` and related raw builtins; require typed errors from `@unito/integration-sdk`',
      recommended: true,
    },
    schema: [],
    messages: {
      rawThrow:
        'Do not `throw new {{name}}(...)`. Throw a typed error from `@unito/integration-sdk` instead (e.g. `HttpErrors.UnprocessableEntityError`, `HttpErrors.NotFoundError`). Typed errors carry the right HTTP status code, expose structured metadata to the logger, and let callers branch on `instanceof`.',
    },
  },

  create(context) {
    const filename = context.filename ?? '';
    if (isFileAllowlisted(filename)) {
      return {};
    }

    const sourceCode = context.sourceCode;
    return {
      ThrowStatement(node) {
        const constructorName = getThrownConstructorName(node.argument);
        if (!constructorName || !RAW_ERROR_NAMES.has(constructorName)) {
          return;
        }
        const callExpr = node.argument as ESTree.NewExpression | ESTree.CallExpression;
        const firstArg = callExpr.arguments[0] as ESTree.Node | undefined;
        if (isMessageAllowlisted(firstArg, sourceCode)) {
          return;
        }
        context.report({
          node,
          messageId: 'rawThrow',
          data: { name: constructorName },
        });
      },
    };
  },
};

export default rule;
