/**
 * Inside a `catch` block in a handler or route, do NOT propagate the provider's
 * `error.message` (or `err.message`, `e.message`, etc.) into the message
 * argument of an `HttpErrors.*` throw. The provider's error string crosses the
 * HTTP boundary into Unito's response, where it can carry tokens, customer
 * PII, or internal field names back to upstream callers and logs.
 *
 * Bad:
 *   } catch (error) {
 *     throw new HttpErrors.BadRequestError(error.message);
 *     throw new HttpErrors.X(`upload failed: ${err.message}`);
 *     throw new HttpErrors.Y(error instanceof Error ? error.message : String(error));
 *   }
 *
 * Good — strip provider message, surface a stable summary instead:
 *   } catch (error) {
 *     context.logger.warn('upload failed', { error: error.message });
 *     throw new HttpErrors.UnprocessableEntityError('Upload failed');
 *   }
 *
 * Scope:
 *   Fires only in `**\/src/handlers/**` and `**\/src/routes/**`. Rate-limiter
 *   files under those paths are excluded (intentional internal-lib message
 *   rethrow on rate-limit responses).
 */
import type { Rule } from 'eslint';
import type * as ESTree from 'estree';
import { isInsideCatchClause, isHttpErrorsCallee } from '../utils.js';

const SCOPE_FILE_RE = /\/src\/(handlers|routes)\//;
const RATE_LIMITER_RE = /[Rr]ate[Ll]imit/;
const ERROR_IDENTIFIER_RE = /^(err|error|e|originalError|cause)$/i;

function getPropertyName(property: ESTree.Node | undefined | null): string | null {
  if (!property) {
    return null;
  }
  if (property.type === 'Identifier') {
    return property.name;
  }
  if (property.type === 'Literal' && typeof property.value === 'string') {
    return property.value;
  }
  return null;
}

function isErrorMessageAccess(memberExpression: ESTree.Node): boolean {
  if (memberExpression.type !== 'MemberExpression') {
    return false;
  }
  if (getPropertyName(memberExpression.property) !== 'message') {
    return false;
  }
  if (memberExpression.object.type !== 'Identifier') {
    return false;
  }
  return ERROR_IDENTIFIER_RE.test(memberExpression.object.name);
}

function containsErrorMessageAccess(node: ESTree.Node | null | undefined): boolean {
  if (!node) {
    return false;
  }
  if (node.type === 'MemberExpression') {
    if (isErrorMessageAccess(node)) {
      return true;
    }
    return containsErrorMessageAccess(node.object) || containsErrorMessageAccess(node.property);
  }
  if (node.type === 'TemplateLiteral') {
    return node.expressions.some(e => containsErrorMessageAccess(e));
  }
  if (node.type === 'ConditionalExpression') {
    return (
      containsErrorMessageAccess(node.test) ||
      containsErrorMessageAccess(node.consequent) ||
      containsErrorMessageAccess(node.alternate)
    );
  }
  if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
    return containsErrorMessageAccess(node.left) || containsErrorMessageAccess(node.right);
  }
  if (node.type === 'CallExpression' || node.type === 'NewExpression') {
    return node.arguments.some(arg => containsErrorMessageAccess(arg as ESTree.Node));
  }
  // TS-specific node types (cast / non-null assertion). `estree` doesn't declare
  // them but @typescript-eslint/parser emits them; treat them as transparent
  // wrappers and recurse on .expression.
  const ts = node as unknown as { type: string; expression?: ESTree.Node };
  if (ts.type === 'TSAsExpression' || ts.type === 'TSNonNullExpression') {
    return containsErrorMessageAccess(ts.expression);
  }
  return false;
}

const rule: Rule.RuleModule = {
  meta: {
    type: 'problem',
    docs: {
      description:
        'Disallow rethrowing the provider error message into the HTTP response (`HttpErrors.X(err.message)`); strip the provider message and throw a stable summary across the boundary',
      recommended: true,
    },
    schema: [],
    messages: {
      errorMessageRethrow:
        'Do not propagate `error.message` (or `err.message`/`e.message`) into a `HttpErrors.*` throw. Provider error strings can carry tokens, PII, or internal field names — rethrowing them across the HTTP boundary turns the connector into an exfiltration path. Log the original message via the structured logger (key-based redaction) and throw a stable, provider-agnostic summary instead.',
    },
  },

  create(context) {
    const filename = context.filename ?? '';
    if (!SCOPE_FILE_RE.test(filename)) {
      return {};
    }
    if (RATE_LIMITER_RE.test(filename)) {
      return {};
    }

    return {
      ThrowStatement(node) {
        const argument = node.argument;
        if (!argument || argument.type !== 'NewExpression') {
          return;
        }
        if (!isHttpErrorsCallee(argument.callee)) {
          return;
        }
        if (!isInsideCatchClause(node as Rule.Node)) {
          return;
        }
        const propagates = argument.arguments.some(arg => containsErrorMessageAccess(arg as ESTree.Node));
        if (!propagates) {
          return;
        }
        context.report({ node, messageId: 'errorMessageRethrow' });
      },
    };
  },
};

export default rule;
