import iterateJsdoc from '../iterateJsdoc.js';

/**
 * Checks if a node or its children contain Promise rejection patterns
 * @param {import('eslint').Rule.Node} node
 * @param {boolean} [innerFunction]
 * @param {boolean} [isAsync]
 * @returns {boolean}
 */
// eslint-disable-next-line complexity -- Temporary
const hasRejectValue = (node, innerFunction, isAsync) => {
  if (!node) {
    return false;
  }

  switch (node.type) {
    case 'ArrowFunctionExpression':
    case 'FunctionDeclaration':
    case 'FunctionExpression': {
      // For inner functions in async contexts, check if they throw
      // (they could be called and cause rejection)
      if (innerFunction) {
        // Check inner functions for throws - if called from async context, throws become rejections
        const innerIsAsync = node.async;
        // Pass isAsync=true if the inner function is async OR if we're already in an async context
        return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.body), false, innerIsAsync || isAsync);
      }

      // This is the top-level function we're checking
      return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.body), true, node.async);
    }

    case 'BlockStatement': {
      return node.body.some((bodyNode) => {
        return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (bodyNode), innerFunction, isAsync);
      });
    }

    case 'CallExpression': {
      // Check for Promise.reject()
      if (node.callee.type === 'MemberExpression' &&
          node.callee.object.type === 'Identifier' &&
          node.callee.object.name === 'Promise' &&
          node.callee.property.type === 'Identifier' &&
          node.callee.property.name === 'reject') {
        return true;
      }

      // Check for reject() call (in Promise executor context)
      if (node.callee.type === 'Identifier' && node.callee.name === 'reject') {
        return true;
      }

      // Check if this is calling an inner function that might reject
      if (innerFunction && node.callee.type === 'Identifier') {
        // We found a function call inside - check if it could be calling a function that rejects
        // We'll handle this in function body traversal
        return false;
      }

      return false;
    }

    case 'DoWhileStatement':
    case 'ForInStatement':
    case 'ForOfStatement':
    case 'ForStatement':
    case 'LabeledStatement':
    case 'WhileStatement':

    case 'WithStatement': {
      return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.body), innerFunction, isAsync);
    }

    case 'ExpressionStatement': {
      return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.expression), innerFunction, isAsync);
    }

    case 'IfStatement': {
      return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.consequent), innerFunction, isAsync) || hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.alternate), innerFunction, isAsync);
    }

    case 'NewExpression': {
      // Check for new Promise((resolve, reject) => { reject(...) })
      if (node.callee.type === 'Identifier' && node.callee.name === 'Promise' && node.arguments.length > 0) {
        const executor = node.arguments[0];
        if (executor.type === 'ArrowFunctionExpression' || executor.type === 'FunctionExpression') {
          // Check if the executor has reject() calls
          return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (executor.body), false, false);
        }
      }

      return false;
    }

    case 'ReturnStatement': {
      if (node.argument) {
        return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.argument), innerFunction, isAsync);
      }

      return false;
    }

    case 'SwitchStatement': {
      return node.cases.some(
        (someCase) => {
          return someCase.consequent.some((nde) => {
            return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (nde), innerFunction, isAsync);
          });
        },
      );
    }

    // Throw statements in async functions become rejections
    case 'ThrowStatement': {
      return isAsync === true;
    }

    case 'TryStatement': {
      return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.handler && node.handler.body), innerFunction, isAsync) ||
        hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.finalizer), innerFunction, isAsync);
    }

    default: {
      return false;
    }
  }
};

/**
 * We can skip checking for a rejects value, in case the documentation is inherited
 * or the method is abstract.
 * @param {import('../iterateJsdoc.js').Utils} utils
 * @returns {boolean}
 */
const canSkip = (utils) => {
  return utils.hasATag([
    'abstract',
    'virtual',
    'type',
  ]) ||
    utils.avoidDocs();
};

export default iterateJsdoc(({
  node,
  report,
  utils,
}) => {
  if (canSkip(utils)) {
    return;
  }

  const tagName = /** @type {string} */ (utils.getPreferredTagName({
    tagName: 'rejects',
  }));
  if (!tagName) {
    return;
  }

  const tags = utils.getTags(tagName);
  const iteratingFunction = utils.isIteratingFunction();

  const [
    tag,
  ] = tags;
  const missingRejectsTag = typeof tag === 'undefined' || tag === null;

  const shouldReport = () => {
    if (!missingRejectsTag) {
      return false;
    }

    // Check if this is an async function or returns a Promise
    const isAsync = utils.isAsync();
    if (!isAsync && !iteratingFunction) {
      return false;
    }

    // For async functions, check for throw statements
    // For regular functions, check for Promise.reject or reject calls
    return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node));
  };

  if (shouldReport()) {
    report('Promise-rejecting function requires `@rejects` tag');
  }
}, {
  contextDefaults: true,
  meta: {
    docs: {
      description: 'Requires that Promise rejections are documented with `@rejects` tags.',
      url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-rejects.md#repos-sticky-header',
    },
    schema: [
      {
        additionalProperties: false,
        properties: {
          contexts: {
            description: `Set this to an array of strings representing the AST context
(or objects with optional \`context\` and \`comment\` properties) where you wish
the rule to be applied.

\`context\` defaults to \`any\` and \`comment\` defaults to no specific comment context.

Overrides the default contexts (\`ArrowFunctionExpression\`, \`FunctionDeclaration\`,
\`FunctionExpression\`).`,
            items: {
              anyOf: [
                {
                  type: 'string',
                },
                {
                  additionalProperties: false,
                  properties: {
                    comment: {
                      type: 'string',
                    },
                    context: {
                      type: 'string',
                    },
                  },
                  type: 'object',
                },
              ],
            },
            type: 'array',
          },
          exemptedBy: {
            description: `Array of tags (e.g., \`['type']\`) whose presence on the
document block avoids the need for a \`@rejects\`. Defaults to an array
with \`abstract\`, \`virtual\`, and \`type\`. If you set this array, it will overwrite the default,
so be sure to add back those tags if you wish their presence to cause
exemption of the rule.`,
            items: {
              type: 'string',
            },
            type: 'array',
          },
        },
        type: 'object',
      },
    ],
    type: 'suggestion',
  },
});
