/**
 * Shared AST helpers for `@unito/integration-sdk` ESLint rules. Centralised so
 * rules that walk the same shapes (catch clauses, logger member chains,
 * HttpErrors call sites) don't drift across rule files.
 */
import type * as ESTree from 'estree';
import type { Rule } from 'eslint';

export const HTTP_ERRORS_NAMESPACE = 'HttpErrors';

export const LOGGER_NAME_BROAD_RE = /\b(logger|console|log)\b/i;
export const LOGGER_NAME_STRICT_RE = /\blogger\b/i;

type NodeWithParent = ESTree.Node & { parent?: NodeWithParent };

export interface MemberChain {
  root: string | null;
  props: string[];
}

/**
 * Walk a (possibly chained) MemberExpression and return its identifier
 * components.
 *
 *   foo.bar.baz()  → { root: 'foo',  props: ['baz', 'bar'] }
 *   this.x.y       → { root: null,   props: ['y',   'x']  }
 */
export function walkMemberChain(node: ESTree.Node | null | undefined): MemberChain {
  const props: string[] = [];
  let current: ESTree.Node | null | undefined = node;
  while (current && current.type === 'MemberExpression') {
    if (current.property && current.property.type === 'Identifier') {
      props.push(current.property.name);
    }
    current = current.object;
  }
  const root = current && current.type === 'Identifier' ? current.name : null;
  return { root, props };
}

/**
 * Walks parents from `node` until either:
 *   - finds a CatchClause (returns true)
 *   - crosses a function boundary (returns false)
 *
 * Use to scope rules to lexically-enclosed catch blocks; helpers called from a
 * catch via a separate function are intentionally NOT matched.
 */
export function isInsideCatchClause(node: Rule.Node): boolean {
  let parent: NodeWithParent | undefined = (node as unknown as NodeWithParent).parent;
  while (parent) {
    if (parent.type === 'CatchClause') {
      return true;
    }
    if (
      parent.type === 'FunctionDeclaration' ||
      parent.type === 'FunctionExpression' ||
      parent.type === 'ArrowFunctionExpression'
    ) {
      return false;
    }
    parent = parent.parent;
  }
  return false;
}

/**
 * Like isInsideCatchClause but returns the parameter name of the enclosing
 * CatchClause. Returns null if the node is not inside a catch, or if the catch
 * uses destructuring / has no parameter binding.
 */
export function getCatchClauseParam(node: Rule.Node): string | null {
  let parent: NodeWithParent | undefined = (node as unknown as NodeWithParent).parent;
  while (parent) {
    if (parent.type === 'CatchClause') {
      const catchClause = parent as unknown as ESTree.CatchClause;
      if (catchClause.param && catchClause.param.type === 'Identifier') {
        return catchClause.param.name;
      }
      return null;
    }
    if (
      parent.type === 'FunctionDeclaration' ||
      parent.type === 'FunctionExpression' ||
      parent.type === 'ArrowFunctionExpression'
    ) {
      return null;
    }
    parent = parent.parent;
  }
  return null;
}

/**
 * Returns true if `callee` is a `HttpErrors.<X>` MemberExpression. If `subClass`
 * is provided, additionally requires the property name to match.
 */
export function isHttpErrorsCallee(callee: ESTree.Node | null | undefined, subClass?: string): boolean {
  if (!callee || callee.type !== 'MemberExpression') {
    return false;
  }
  if (!callee.object || callee.object.type !== 'Identifier') {
    return false;
  }
  if (callee.object.name !== HTTP_ERRORS_NAMESPACE) {
    return false;
  }
  if (subClass === undefined) {
    return true;
  }
  if (!callee.property || callee.property.type !== 'Identifier') {
    return false;
  }
  return callee.property.name === subClass;
}
