/**
 * Flags catch blocks whose only action is to return a falsy/empty value
 * with no logging. Hides bugs and produces silent data loss.
 *
 * Example violation:
 *   try { ... } catch (e) { return null; }
 *
 * Acceptable:
 *   try { ... } catch (e) { logger.warn('msg', { error: e }); return null; }
 *   try { ... } catch (e) { throw new HttpErrors.X('...'); }
 */
import type { Rule } from 'eslint';
import type * as ESTree from 'estree';

const FALSY_LITERAL_VALUES = new Set<unknown>([null, '']);
const LOGGER_METHOD_REGEX = /^(log|warn|error|info|debug)$/i;
const LOGGER_NAME_REGEX = /\b(logger|console|log)\b/i;

const RETURNED_VALUE_LABELS: Record<string, string> = {
  ObjectExpression: '{}',
  ArrayExpression: '[]',
};

function isFalsyLiteral(node: ESTree.Expression | null | undefined): boolean {
  if (!node) {
    return true;
  }
  if (node.type === 'Literal' && FALSY_LITERAL_VALUES.has(node.value)) {
    return true;
  }
  if (node.type === 'Identifier' && node.name === 'undefined') {
    return true;
  }
  if (node.type === 'ObjectExpression' && node.properties.length === 0) {
    return true;
  }
  if (node.type === 'ArrayExpression' && node.elements.length === 0) {
    return true;
  }
  return false;
}

function walkMemberChain(node: ESTree.Expression): { root: string | null; props: string[] } {
  const props: string[] = [];
  let current: ESTree.Expression | ESTree.Super = node;
  while (current.type === 'MemberExpression') {
    if (current.property.type === 'Identifier') {
      props.push(current.property.name);
    }
    current = current.object;
  }
  const root = current.type === 'Identifier' ? current.name : null;
  return { root, props };
}

function isLoggerCallExpression(callExpr: ESTree.CallExpression): boolean {
  const callee = callExpr.callee;
  if (callee.type === 'Identifier') {
    return LOGGER_METHOD_REGEX.test(callee.name);
  }
  if (callee.type !== 'MemberExpression') {
    return false;
  }
  const { root, props } = walkMemberChain(callee as ESTree.Expression);
  return [root, ...props].some(p => p !== null && LOGGER_NAME_REGEX.test(p));
}

function isLoggerCall(node: ESTree.Statement): boolean {
  if (node.type !== 'ExpressionStatement') {
    return false;
  }
  const expr = node.expression;
  if (expr.type === 'AwaitExpression' && expr.argument && expr.argument.type === 'CallExpression') {
    return isLoggerCallExpression(expr.argument);
  }
  if (expr.type !== 'CallExpression') {
    return false;
  }
  return isLoggerCallExpression(expr);
}

function describeReturnedValue(node: ESTree.Expression | null | undefined): string {
  if (!node) {
    return 'undefined';
  }
  const label = RETURNED_VALUE_LABELS[node.type];
  if (label) {
    return label;
  }
  if (node.type === 'Literal') {
    return String(node.value);
  }
  if (node.type === 'Identifier') {
    return node.name;
  }
  return 'value';
}

const rule: Rule.RuleModule = {
  meta: {
    type: 'problem',
    docs: {
      description:
        'Disallow silent catch blocks that return falsy/empty values without logging. ' +
        'Detected: null, undefined, bare return, empty string, empty array, empty object. ' +
        'Boolean false and numeric 0 are NOT flagged — those are usually intentional semantic results.',
      recommended: true,
    },
    schema: [],
    messages: {
      silentReturn:
        'Catch block silently returns {{value}} without logging. Add a logger call (e.g. `logger.warn(...)`) or rethrow.',
    },
  },

  create(context) {
    return {
      CatchClause(node: ESTree.CatchClause) {
        const body = node.body.body;
        if (body.length === 0) {
          return;
        }

        const lastStatement = body[body.length - 1];
        if (!lastStatement || lastStatement.type !== 'ReturnStatement') {
          return;
        }
        if (!isFalsyLiteral(lastStatement.argument)) {
          return;
        }

        const earlierStatements = body.slice(0, -1);
        if (earlierStatements.some(isLoggerCall)) {
          return;
        }

        context.report({
          node: lastStatement,
          messageId: 'silentReturn',
          data: { value: describeReturnedValue(lastStatement.argument) },
        });
      },
    } as Rule.RuleListener;
  },
};

export default rule;
