import getDefaultTagStructureForMode from './getDefaultTagStructureForMode.js';
import {
  closureTags,
  jsdocTags,
  typeScriptTags,
} from './tagNames.js';
import WarnSettings from './WarnSettings.js';
import {
  stringify,
  tryParse,
} from '@es-joy/jsdoccomment';

/**
 * @typedef {number} Integer
 */
/**
 * @typedef {import('./utils/hasReturnValue.js').ESTreeOrTypeScriptNode} ESTreeOrTypeScriptNode
 */

/**
 * @typedef {"jsdoc"|"typescript"|"closure"|"permissive"} ParserMode
 */

/**
 * @type {import('./getDefaultTagStructureForMode.js').TagStructure}
 */
let tagStructure;

/**
 * @param {ParserMode} mode
 * @returns {void}
 */
const setTagStructure = (mode) => {
  tagStructure = getDefaultTagStructureForMode(mode);
};

/**
 * @typedef {undefined|string|{
 *   name: Integer,
 *   restElement: boolean
 * }|{
 *   isRestProperty: boolean|undefined,
 *   name: string,
 *   restElement: boolean
 * }|{
 *   name: string,
 *   restElement: boolean
 * }} ParamCommon
 */
/**
 * @typedef {ParamCommon|[string|undefined, (FlattendRootInfo & {
 *   annotationParamName?: string,
 * })]|NestedParamInfo} ParamNameInfo
 */

/**
 * @typedef {{
 *   hasPropertyRest: boolean,
 *   hasRestElement: boolean,
 *   names: string[],
 *   rests: boolean[],
 * }} FlattendRootInfo
 */
/**
 * @typedef {[string, (string[]|ParamInfo[])]} NestedParamInfo
 */
/**
 * @typedef {ParamCommon|
 * [string|undefined, (FlattendRootInfo & {
 *   annotationParamName?: string
 * })]|
 * NestedParamInfo} ParamInfo
 */

/**
 * Given a nested array of property names, reduce them to a single array,
 * appending the name of the root element along the way if present.
 * @callback FlattenRoots
 * @param {ParamInfo[]} params
 * @param {string} [root]
 * @returns {FlattendRootInfo}
 */

/** @type {FlattenRoots} */
const flattenRoots = (params, root = '') => {
  let hasRestElement = false;
  let hasPropertyRest = false;

  /**
   * @type {boolean[]}
   */
  const rests = [];

  const names = params.reduce(
    /**
     * @param {string[]} acc
     * @param {ParamInfo} cur
     * @returns {string[]}
     */
    (acc, cur) => {
      if (Array.isArray(cur)) {
        let nms;
        if (Array.isArray(cur[1])) {
          nms = cur[1];
        } else {
          if (cur[1].hasRestElement) {
            hasRestElement = true;
          }

          if (cur[1].hasPropertyRest) {
            hasPropertyRest = true;
          }

          nms = cur[1].names;
        }

        const flattened = flattenRoots(nms, root ? `${root}.${cur[0]}` : cur[0]);
        if (flattened.hasRestElement) {
          hasRestElement = true;
        }

        if (flattened.hasPropertyRest) {
          hasPropertyRest = true;
        }

        const inner = /** @type {string[]} */ ([
          root ? `${root}.${cur[0]}` : cur[0],
          ...flattened.names,
        ].filter(Boolean));
        rests.push(false, ...flattened.rests);

        return acc.concat(inner);
      }

      if (typeof cur === 'object') {
        if ('isRestProperty' in cur && cur.isRestProperty) {
          hasPropertyRest = true;
          rests.push(true);
        } else {
          rests.push(false);
        }

        if ('restElement' in cur && cur.restElement) {
          hasRestElement = true;
        }

        acc.push(root ? `${root}.${String(cur.name)}` : String(cur.name));
      } else if (typeof cur !== 'undefined') {
        rests.push(false);
        acc.push(root ? `${root}.${cur}` : cur);
      }

      return acc;
    }, [],
  );

  return {
    hasPropertyRest,
    hasRestElement,
    names,
    rests,
  };
};

/**
 * @param {import('@typescript-eslint/types').TSESTree.TSIndexSignature|
 *  import('@typescript-eslint/types').TSESTree.TSConstructSignatureDeclaration|
 *  import('@typescript-eslint/types').TSESTree.TSCallSignatureDeclaration|
 *  import('@typescript-eslint/types').TSESTree.TSPropertySignature} propSignature
 * @returns {undefined|string|[string, string[]]}
 */
const getPropertiesFromPropertySignature = (propSignature) => {
  if (
    propSignature.type === 'TSIndexSignature' ||
    propSignature.type === 'TSConstructSignatureDeclaration' ||
    propSignature.type === 'TSCallSignatureDeclaration'
  ) {
    return undefined;
  }

  if (propSignature.typeAnnotation && propSignature.typeAnnotation.typeAnnotation.type === 'TSTypeLiteral') {
    return [
      /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
        propSignature.key
      ).name,
      propSignature.typeAnnotation.typeAnnotation.members.map((member) => {
        return /** @type {string} */ (
          getPropertiesFromPropertySignature(
            /** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */ (
              member
            ),
          )
        );
      }),
    ];
  }

  return /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
    propSignature.key
  ).name;
};

/**
 * @param {ESTreeOrTypeScriptNode|null} functionNode
 * @param {boolean} [checkDefaultObjects]
 * @param {boolean} [ignoreInterfacedParameters]
 * @throws {Error}
 * @returns {ParamNameInfo[]}
 */
const getFunctionParameterNames = (
  functionNode, checkDefaultObjects, ignoreInterfacedParameters,
) => {
  /* eslint-disable complexity -- Temporary */
  /**
   * @param {import('estree').Identifier|import('estree').AssignmentPattern|
   *   import('estree').ObjectPattern|import('estree').Property|
   *   import('estree').RestElement|import('estree').ArrayPattern|
   *   import('@typescript-eslint/types').TSESTree.TSParameterProperty|
   *   import('@typescript-eslint/types').TSESTree.Property|
   *   import('@typescript-eslint/types').TSESTree.RestElement|
   *   import('@typescript-eslint/types').TSESTree.Identifier|
   *   import('@typescript-eslint/types').TSESTree.ObjectPattern|
   *   import('@typescript-eslint/types').TSESTree.BindingName|
   *   import('@typescript-eslint/types').TSESTree.Parameter
   * } param
   * @param {boolean} [isProperty]
   * @returns {ParamNameInfo|[string, ParamNameInfo[]]}
   */
  const getParamName = (param, isProperty) => {
    /* eslint-enable complexity -- Temporary */
    const hasLeftTypeAnnotation = 'left' in param && 'typeAnnotation' in param.left;

    if ('typeAnnotation' in param || hasLeftTypeAnnotation) {
      if (ignoreInterfacedParameters && 'typeAnnotation' in param &&
        param.typeAnnotation) {
        // No-op
        return [
          undefined, {
            hasPropertyRest: false,
            hasRestElement: false,
            names: [],
            rests: [],
          },
        ];
      }

      const typeAnnotation = hasLeftTypeAnnotation ?
        /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
          param.left
        ).typeAnnotation :
        /** @type {import('@typescript-eslint/types').TSESTree.Identifier|import('@typescript-eslint/types').TSESTree.ObjectPattern} */
        (param).typeAnnotation;

      if (typeAnnotation?.typeAnnotation?.type === 'TSTypeLiteral') {
        const propertyNames = typeAnnotation.typeAnnotation.members.map((member) => {
          return getPropertiesFromPropertySignature(
            /** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */
            (member),
          );
        });

        const flattened = {
          ...flattenRoots(propertyNames),
          annotationParamName: 'name' in param ? param.name : undefined,
        };
        const hasLeftName = 'left' in param && 'name' in param.left;

        if ('name' in param || hasLeftName) {
          return [
            hasLeftName ?
              /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
                param.left
              ).name :
              /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
                param
              ).name,
            flattened,
          ];
        }

        return [
          undefined, flattened,
        ];
      }
    }

    if ('name' in param) {
      return param.name;
    }

    if ('left' in param && 'name' in param.left) {
      return param.left.name;
    }

    if (
      param.type === 'ObjectPattern' ||
      ('left' in param &&
      (
        param
      ).left.type === 'ObjectPattern')
    ) {
      const properties = /** @type {import('@typescript-eslint/types').TSESTree.ObjectPattern} */ (
        param
      ).properties ||
        /** @type {import('estree').ObjectPattern} */
        (
          /** @type {import('@typescript-eslint/types').TSESTree.AssignmentPattern} */ (
            param
          ).left
        )?.properties;
      const roots = properties.map((prop) => {
        return getParamName(prop, true);
      });

      return [
        undefined, flattenRoots(roots),
      ];
    }

    if (param.type === 'Property') {
      switch (param.value.type) {
        case 'ArrayPattern': {
          return [
          /** @type {import('estree').Identifier} */
            (param.key).name,
            /** @type {import('estree').ArrayPattern} */ (
              param.value
            ).elements.map((prop, idx) => {
              return {
                name: idx,
                restElement: prop?.type === 'RestElement',
              };
            }),
          ];
        }

        case 'ObjectPattern': {
          return [
          /** @type {import('estree').Identifier} */ (param.key).name,
            /** @type {import('estree').ObjectPattern} */ (
              param.value
            ).properties.map((prop) => {
              return /** @type {string|[string, string[]]} */ (getParamName(prop, isProperty));
            }),
          ];
        }

        case 'AssignmentPattern': {
          switch (param.value.left.type) {
            case 'ArrayPattern':
              return [
                /** @type {import('estree').Identifier} */
                (param.key).name,
                /** @type {import('estree').ArrayPattern} */ (
                  param.value.left
                ).elements.map((prop, idx) => {
                  return {
                    name: idx,
                    restElement: prop?.type === 'RestElement',
                  };
                }),
              ];
            case 'Identifier':
              // Default parameter
              if (checkDefaultObjects && param.value.right.type === 'ObjectExpression') {
                return [
                  /** @type {import('estree').Identifier} */ (
                    param.key
                  ).name,
                  /** @type {import('estree').AssignmentPattern} */ (
                    param.value
                  ).right.properties.map((prop) => {
                    return /** @type {string} */ (getParamName(
                      /** @type {import('estree').Property} */
                      (prop),
                      isProperty,
                    ));
                  }),
                ];
              }

              break;
            case 'ObjectPattern':
              return [
                /** @type {import('estree').Identifier} */
                (param.key).name,
                /** @type {import('estree').ObjectPattern} */ (
                  param.value.left
                ).properties.map((prop) => {
                  return getParamName(prop, isProperty);
                }),
              ];
          }
        }
      }

      switch (param.key.type) {
        case 'Identifier':
          return param.key.name;

          // The key of an object could also be a string or number
        case 'Literal':
        /* c8 ignore next 2 -- `raw` may not be present in all parsers */
          return /** @type {string} */ (param.key.raw ||
          param.key.value);

          // case 'MemberExpression':
        default:
        // Todo: We should really create a structure (and a corresponding
        //   option analogous to `checkRestProperty`) which allows for
        //   (and optionally requires) dynamic properties to have a single
        //   line of documentation
          return undefined;
      }
    }

    if (
      param.type === 'ArrayPattern' ||
      /** @type {import('estree').AssignmentPattern} */ (
        param
      ).left?.type === 'ArrayPattern'
    ) {
      const elements = /** @type {import('estree').ArrayPattern} */ (
        param
      ).elements || /** @type {import('estree').ArrayPattern} */ (
        /** @type {import('estree').AssignmentPattern} */ (
          param
        ).left
      )?.elements;
      const roots = elements.map((prop, idx) => {
        return {
          name: `"${idx}"`,
          restElement: prop?.type === 'RestElement',
        };
      });

      return [
        undefined, flattenRoots(roots),
      ];
    }

    if ([
      'ExperimentalRestProperty', 'RestElement',
    ].includes(param.type)) {
      return {
        isRestProperty: isProperty,
        name: /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
          /** @type {import('@typescript-eslint/types').TSESTree.RestElement} */ (
            param
          // @ts-expect-error Ok
          ).argument).name ?? param?.argument?.elements?.map(({
          // @ts-expect-error Ok
          name,
        }) => {
          return name;
        }),
        restElement: true,
      };
    }

    if (param.type === 'TSParameterProperty') {
      return getParamName(
        /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
          /** @type {import('@typescript-eslint/types').TSESTree.TSParameterProperty} */ (
            param
          ).parameter
        ),
        true,
      );
    }

    throw new Error(`Unsupported function signature format: \`${param.type}\`.`);
  };

  if (!functionNode) {
    return [];
  }

  return (
    /** @type {import('@typescript-eslint/types').TSESTree.TSFunctionType} */
    (/** @type {import('@typescript-eslint/types').TSESTree.TSPropertySignature} */ (functionNode)?.typeAnnotation?.typeAnnotation)?.params ||
    /** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
      functionNode
    ).params || /** @type {import('@typescript-eslint/types').TSESTree.MethodDefinition} */ (
      functionNode
    ).value?.params || []).map((param) => {
    return getParamName(param);
  });
};

/**
 * @param {ESTreeOrTypeScriptNode} functionNode
 * @returns {Integer}
 */
const hasParams = (functionNode) => {
  // Should also check `functionNode.value.params` if supporting `MethodDefinition`
  return /** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
    functionNode
  ).params.length;
};

/**
 * Gets all names of the target type, including those that refer to a path, e.g.
 * `foo` or `foo.bar`.
 * @param {import('comment-parser').Block} jsdoc
 * @param {string} targetTagName
 * @returns {{
 *   idx: Integer,
 *   name: string,
 *   type: string
 * }[]}
 */
const getJsdocTagsDeep = (jsdoc, targetTagName) => {
  const ret = [];
  for (const [
    idx,
    {
      name,
      tag,
      type,
    },
  ] of jsdoc.tags.entries()) {
    if (tag !== targetTagName) {
      continue;
    }

    ret.push({
      idx,
      name,
      type,
    });
  }

  return ret;
};

const modeWarnSettings = WarnSettings();

/**
 * @param {ParserMode|undefined} mode
 * @param {import('eslint').Rule.RuleContext} context
 * @returns {import('./tagNames.js').AliasedTags}
 */
const getTagNamesForMode = (mode, context) => {
  switch (mode) {
    case 'closure':
    case 'permissive':
      return closureTags;
    case 'jsdoc':
      return jsdocTags;
    case 'typescript':
      return typeScriptTags;
    default:
      if (!modeWarnSettings.hasBeenWarned(context, 'mode')) {
        context.report({
          loc: {
            end: {
              column: 1,
              line: 1,
            },
            start: {
              column: 1,
              line: 1,
            },
          },
          message: `Unrecognized value \`${mode}\` for \`settings.jsdoc.mode\`.`,
        });
        modeWarnSettings.markSettingAsWarned(context, 'mode');
      }

      // We'll avoid breaking too many other rules
      return jsdocTags;
  }
};

/**
 * @param {import('comment-parser').Spec} tg
 * @param {boolean} [returnArray]
 * @returns {string[]|string}
 */
const getTagDescription = (tg, returnArray) => {
  /**
   * @type {string[]}
   */
  const descriptions = [];
  tg.source.some(({
    tokens: {
      description,
      end,
      lineEnd,
      name,
      postDelimiter,
      postTag,
      tag,
      type,
    },
  }) => {
    const desc = (
      tag && postTag ||
        !tag && !name && !type && postDelimiter || ''

    // Remove space
    ).slice(1) +
        (description || '') + (lineEnd || '');

    if (end) {
      if (desc) {
        descriptions.push(desc);
      }

      return true;
    }

    descriptions.push(desc);

    return false;
  });

  return returnArray ? descriptions : descriptions.join('\n');
};

/**
 * @typedef {{
 *   report: (descriptor: import('eslint').Rule.ReportDescriptor) => void
 * }} Reporter
 */

/**
 * @param {string} name
 * @param {ParserMode|undefined} mode
 * @param {TagNamePreference} tagPreference
 * @param {import('eslint').Rule.RuleContext} context
 * @returns {string|false|{
 *   message: string;
 *   replacement?: string|undefined;
 * }}
 */
const getPreferredTagNameSimple = (
  name,
  mode,
  tagPreference = {},
  // @ts-expect-error Just a no-op
  // eslint-disable-next-line unicorn/no-object-as-default-parameter -- Ok
  context = {
    report () {
      // No-op
    },
  },
) => {
  const prefValues = Object.values(tagPreference);
  if (prefValues.includes(name) || prefValues.some((prefVal) => {
    return prefVal && typeof prefVal === 'object' && prefVal.replacement === name;
  })) {
    return name;
  }

  // Allow keys to have a 'tag ' prefix to avoid upstream bug in ESLint
  // that disallows keys that conflict with Object.prototype,
  // e.g. 'tag constructor' for 'constructor':
  // https://github.com/eslint/eslint/issues/13289
  // https://github.com/gajus/eslint-plugin-jsdoc/issues/537
  const tagPreferenceFixed = Object.fromEntries(
    Object
      .entries(tagPreference)
      .map(([
        key,
        value,
      ]) => {
        return [
          key.replace(/^tag /v, ''), value,
        ];
      }),
  );

  if (Object.hasOwn(tagPreferenceFixed, name)) {
    return tagPreferenceFixed[name];
  }

  const tagNames = getTagNamesForMode(mode, context);

  const preferredTagName = Object.entries(tagNames).find(([
    , aliases,
  ]) => {
    return aliases.includes(name);
  })?.[0];
  if (preferredTagName) {
    return preferredTagName;
  }

  return name;
};

/**
 * @param {import('eslint').Rule.RuleContext} context
 * @param {ParserMode|undefined} mode
 * @param {string} name
 * @param {string[]} definedTags
 * @returns {boolean}
 */
const isValidTag = (
  context,
  mode,
  name,
  definedTags,
) => {
  const tagNames = getTagNamesForMode(mode, context);

  const validTagNames = Object.keys(tagNames).concat(Object.values(tagNames).flat());
  const additionalTags = definedTags;
  const allTags = validTagNames.concat(additionalTags);

  return allTags.includes(name);
};

/**
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @param {string} targetTagName
 * @returns {boolean}
 */
const hasTag = (jsdoc, targetTagName) => {
  const targetTagLower = targetTagName.toLowerCase();

  return jsdoc.tags.some((doc) => {
    return doc.tag.toLowerCase() === targetTagLower;
  });
};

/**
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @param {(tag: import('@es-joy/jsdoccomment').JsdocTagWithInline) => boolean} filter
 * @returns {import('@es-joy/jsdoccomment').JsdocTagWithInline[]}
 */
const filterTags = (jsdoc, filter) => {
  return jsdoc.tags.filter((tag) => {
    return filter(tag);
  });
};

/**
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @param {string} tagName
 * @returns {import('comment-parser').Spec[]}
 */
const getTags = (jsdoc, tagName) => {
  return filterTags(jsdoc, (item) => {
    return item.tag === tagName;
  });
};

/**
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @param {{
 *   tagName: string,
 *   context?: import('eslint').Rule.RuleContext,
 *   mode?: ParserMode,
 *   report?: import('./iterateJsdoc.js').Report
 *   tagNamePreference?: TagNamePreference
 *   skipReportingBlockedTag?: boolean,
 *   allowObjectReturn?: boolean,
 *   defaultMessage?: string,
 * }} cfg
 * @returns {string|undefined|false|{
 *   message: string;
 *   replacement?: string|undefined;
 * }|{
 *   blocked: true,
 *   tagName: string
 * }}
 */
const getPreferredTagName = (jsdoc, {
  allowObjectReturn = false,
  context,
  tagName,
  defaultMessage = `Unexpected tag \`@${tagName}\``,
  mode,
  report = () => {},
  skipReportingBlockedTag = false,
  tagNamePreference,
}) => {
  const ret = getPreferredTagNameSimple(tagName, mode, tagNamePreference, context);
  const isObject = ret && typeof ret === 'object';
  if (hasTag(jsdoc, tagName) && (ret === false || isObject && !ret.replacement)) {
    if (skipReportingBlockedTag) {
      return {
        blocked: true,
        tagName,
      };
    }

    const message = isObject && ret.message || defaultMessage;
    report(message, null, getTags(jsdoc, tagName)[0]);

    return false;
  }

  return isObject && !allowObjectReturn ? ret.replacement : ret;
};

/**
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @param {string} tagName
 * @param {(
 *   matchingJsdocTag: import('@es-joy/jsdoccomment').JsdocTagWithInline,
 *   targetTagName: string
 * ) => void} arrayHandler
 * @param {object} cfg
 * @param {import('eslint').Rule.RuleContext} [cfg.context]
 * @param {ParserMode} [cfg.mode]
 * @param {import('./iterateJsdoc.js').Report} [cfg.report]
 * @param {TagNamePreference} [cfg.tagNamePreference]
 * @param {boolean} [cfg.skipReportingBlockedTag]
 * @returns {void}
 */
const forEachPreferredTag = (
  jsdoc, tagName, arrayHandler,
  {
    context,
    mode,
    report,
    skipReportingBlockedTag = false,
    tagNamePreference,
  } = {},
) => {
  const targetTagName = /** @type {string|false} */ (
    getPreferredTagName(jsdoc, {
      context,
      mode,
      report,
      skipReportingBlockedTag,
      tagName,
      tagNamePreference,
    })
  );
  if (!targetTagName ||
    skipReportingBlockedTag && targetTagName && typeof targetTagName === 'object'
  ) {
    return;
  }

  const matchingJsdocTags = jsdoc.tags.filter(({
    tag,
  }) => {
    return tag === targetTagName;
  });

  for (const matchingJsdocTag of matchingJsdocTags) {
    arrayHandler(
      /**
       * @type {import('@es-joy/jsdoccomment').JsdocTagWithInline}
       */ (
        matchingJsdocTag
      ), targetTagName,
    );
  }
};

/**
 * Get all inline tags and inline tags in tags
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @returns {(import('comment-parser').Spec|
 *   import('@es-joy/jsdoccomment').JsdocInlineTagNoType & {
 *     line?: number | undefined; column?: number | undefined;
 *   })[]}
 */
const getInlineTags = (jsdoc) => {
  return [
    ...jsdoc.inlineTags.map((inlineTag) => {
      // Tags don't have source or line numbers, so add before returning
      let line = -1;
      for (const {
        tokens: {
          description,
        },
      } of jsdoc.source) {
        line++;
        if (description && description.includes(`{@${inlineTag.tag}`)) {
          break;
        }
      }

      inlineTag.line = line;

      return inlineTag;
    }),
    ...jsdoc.tags.flatMap((tag) => {
      for (const inlineTag of tag.inlineTags) {
        /** @type {import('./iterateJsdoc.js').Integer} */
        let line = 0;
        for (const {
          number,
          tokens: {
            description,
          },
        } of tag.source) {
          if (description && description.includes(`{@${inlineTag.tag}`)) {
            line = number;
            break;
          }
        }

        inlineTag.line = line;
      }

      return (
        /**
         * @type {import('comment-parser').Spec & {
         *   inlineTags: import('@es-joy/jsdoccomment').JsdocInlineTagNoType[]
         * }}
         */ (
          tag
        ).inlineTags
      );
    }),
  ];
};

/**
 * Get all tags, inline tags and inline tags in tags
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @returns {(import('comment-parser').Spec|
 *   import('@es-joy/jsdoccomment').JsdocInlineTagNoType & {
 *     line?: number | undefined; column?: number | undefined;
 *   })[]}
 */
const getAllTags = (jsdoc) => {
  return [
    ...jsdoc.tags,
    ...getInlineTags(jsdoc),
  ];
};

/**
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @param {string[]} targetTagNames
 * @returns {boolean}
 */
const hasATag = (jsdoc, targetTagNames) => {
  return targetTagNames.some((targetTagName) => {
    return hasTag(jsdoc, targetTagName);
  });
};

/**
 * Checks if the JSDoc comment has an undefined type.
 * @param {import('comment-parser').Spec|null|undefined} tag
 *   the tag which should be checked.
 * @param {ParserMode} mode
 * @returns {boolean}
 *   true in case a defined type is undeclared; otherwise false.
 */
const mayBeUndefinedTypeTag = (tag, mode) => {
  // The function should not continue in the event the type is not defined...
  if (typeof tag === 'undefined' || tag === null) {
    return true;
  }

  // .. same applies if it declares an `{undefined}` or `{void}` type
  const tagType = tag.type.trim();

  // Exit early if matching
  if (
    tagType === 'undefined' || tagType === 'void' ||
    tagType === '*' || tagType === 'any'
  ) {
    return true;
  }

  let parsedTypes;
  try {
    parsedTypes = tryParse(
      tagType,
      mode === 'permissive' ? undefined : [
        mode,
      ],
    );
  } catch {
    // Ignore
  }

  if (
    // We do not traverse deeply as it could be, e.g., `Promise<void>`
    parsedTypes &&
    parsedTypes.type === 'JsdocTypeUnion' &&
    parsedTypes.elements.some((elem) => {
      return elem.type === 'JsdocTypeUndefined' ||
        elem.type === 'JsdocTypeName' && elem.value === 'void';
    })) {
    return true;
  }

  // In any other case, a type is present
  return false;
};

/**
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} map
 * @param {string} tag
 * @returns {Map<string, string|string[]|boolean|undefined>}
 */
const ensureMap = (map, tag) => {
  if (!map.has(tag)) {
    map.set(tag, new Map());
  }

  return /** @type {Map<string, string | boolean>} */ (map.get(tag));
};

/**
 * @param {import('./iterateJsdoc.js').StructuredTags} structuredTags
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {void}
 */
const overrideTagStructure = (structuredTags, tagMap = tagStructure) => {
  for (const [
    tag,
    {
      name,
      required = [],
      type,
    },
  ] of Object.entries(structuredTags)) {
    const tagStruct = ensureMap(tagMap, tag);

    tagStruct.set('namepathRole', name);
    tagStruct.set('typeAllowed', type);

    const requiredName = required.includes('name');
    if (requiredName && name === false) {
      throw new Error('Cannot add "name" to `require` with the tag\'s `name` set to `false`');
    }

    tagStruct.set('nameRequired', requiredName);

    const requiredType = required.includes('type');
    if (requiredType && type === false) {
      throw new Error('Cannot add "type" to `require` with the tag\'s `type` set to `false`');
    }

    tagStruct.set('typeRequired', requiredType);

    const typeOrNameRequired = required.includes('typeOrNameRequired');
    if (typeOrNameRequired && name === false) {
      throw new Error('Cannot add "typeOrNameRequired" to `require` with the tag\'s `name` set to `false`');
    }

    if (typeOrNameRequired && type === false) {
      throw new Error('Cannot add "typeOrNameRequired" to `require` with the tag\'s `type` set to `false`');
    }

    tagStruct.set('typeOrNameRequired', typeOrNameRequired);
  }
};

/**
 * @param {ParserMode} mode
 * @param {import('./iterateJsdoc.js').StructuredTags} structuredTags
 * @returns {import('./getDefaultTagStructureForMode.js').TagStructure}
 */
const getTagStructureForMode = (mode, structuredTags) => {
  const tagStruct = getDefaultTagStructureForMode(mode);

  try {
    overrideTagStructure(structuredTags, tagStruct);
  /* c8 ignore next 3 */
  } catch {
    //
  }

  return tagStruct;
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean}
 */
const isNameOrNamepathDefiningTag = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);

  return /** @type {(string|boolean|undefined)[]} */ ([
    'name-defining',
    'namepath-defining',
  ]).includes(/** @type {string|boolean|undefined} */ (
    tagStruct.get('namepathRole')));
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean}
 */
const isNamepathReferencingTag = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);
  return tagStruct.get('namepathRole') === 'namepath-referencing';
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean}
 */
const isNamepathOrUrlReferencingTag = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);
  return tagStruct.get('namepathRole') === 'namepath-or-url-referencing';
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean|undefined}
 */
const tagMustHaveTypePosition = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);

  return /** @type {boolean|undefined} */ (tagStruct.get('typeRequired'));
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean|string}
 */
const tagMightHaveTypePosition = (tag, tagMap = tagStructure) => {
  if (tagMustHaveTypePosition(tag, tagMap)) {
    return true;
  }

  const tagStruct = ensureMap(tagMap, tag);

  const ret = /** @type {boolean|undefined} */ (tagStruct.get('typeAllowed'));

  return ret === undefined ? true : ret;
};

const namepathTypes = new Set([
  'name-defining', 'namepath-defining',
  'namepath-referencing',
]);

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean}
 */
const tagMightHaveNamePosition = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);

  const ret = tagStruct.get('namepathRole');

  return ret === undefined ? true : Boolean(ret);
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean}
 */
const tagMightHaveNameOrNamepath = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);

  const nampathRole = tagStruct.get('namepathRole');

  return nampathRole !== false &&
    namepathTypes.has(/** @type {string} */ (nampathRole));
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean}
 */
const tagMightHaveNamepath = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);

  const nampathRole = tagStruct.get('namepathRole');

  return nampathRole !== false &&
    [
      'namepath-defining',
      'namepath-referencing',
    ].includes(/** @type {string} */ (nampathRole));
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean}
 */
const tagMightHaveName = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);

  const nampathRole = tagStruct.get('namepathRole');

  return nampathRole !== false &&
    nampathRole === 'name-defining';
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean|undefined}
 */
const tagMustHaveNamePosition = (tag, tagMap = tagStructure) => {
  const tagStruct = ensureMap(tagMap, tag);

  return /** @type {boolean|undefined} */ (tagStruct.get('nameRequired'));
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean}
 */
const tagMightHaveEitherTypeOrNamePosition = (tag, tagMap) => {
  return Boolean(tagMightHaveTypePosition(tag, tagMap)) || tagMightHaveNameOrNamepath(tag, tagMap);
};

/**
 * @param {string} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean|undefined}
 */
const tagMustHaveEitherTypeOrNamePosition = (tag, tagMap) => {
  const tagStruct = ensureMap(tagMap, tag);

  return /** @type {boolean} */ (tagStruct.get('typeOrNameRequired'));
};

/**
 * @param {import('comment-parser').Spec} tag
 * @param {import('./getDefaultTagStructureForMode.js').TagStructure} tagMap
 * @returns {boolean|undefined}
 */
const tagMissingRequiredTypeOrNamepath = (tag, tagMap = tagStructure) => {
  const mustHaveTypePosition = tagMustHaveTypePosition(tag.tag, tagMap);
  const mightHaveTypePosition = tagMightHaveTypePosition(tag.tag, tagMap);
  const hasTypePosition = mightHaveTypePosition && Boolean(tag.type);
  const hasNameOrNamepathPosition = (
    tagMustHaveNamePosition(tag.tag, tagMap) ||
    tagMightHaveNameOrNamepath(tag.tag, tagMap)
  ) && Boolean(tag.name);
  const mustHaveEither = tagMustHaveEitherTypeOrNamePosition(tag.tag, tagMap);
  const hasEither = tagMightHaveEitherTypeOrNamePosition(tag.tag, tagMap) &&
    (hasTypePosition || hasNameOrNamepathPosition);

  return mustHaveEither && !hasEither && !mustHaveTypePosition;
};

/* eslint-disable complexity -- Temporary */
/**
 * @param {ESTreeOrTypeScriptNode|null|undefined} node
 * @param {boolean} [checkYieldReturnValue]
 * @returns {boolean}
 */
const hasNonFunctionYield = (node, checkYieldReturnValue) => {
  /* eslint-enable complexity -- Temporary */
  if (!node) {
    return false;
  }

  switch (node.type) {
    case 'ArrayExpression':

    case 'ArrayPattern':
      return node.elements.some((element) => {
        return hasNonFunctionYield(element, checkYieldReturnValue);
      });
    case 'AssignmentExpression':
    case 'BinaryExpression':
    case 'LogicalExpression': {
      return hasNonFunctionYield(node.left, checkYieldReturnValue) ||
      hasNonFunctionYield(node.right, checkYieldReturnValue);
    }

    case 'AssignmentPattern':
      return hasNonFunctionYield(node.right, checkYieldReturnValue);
    case 'BlockStatement': {
      return node.body.some((bodyNode) => {
        return ![
          'ArrowFunctionExpression',
          'FunctionDeclaration',
          'FunctionExpression',
        ].includes(bodyNode.type) && hasNonFunctionYield(
          bodyNode, checkYieldReturnValue,
        );
      });
    }

    /* c8 ignore next 2 -- In Babel? */
    case 'CallExpression':
    // @ts-expect-error In Babel?
    case 'OptionalCallExpression':
      return node.arguments.some((element) => {
        return hasNonFunctionYield(element, checkYieldReturnValue);
      });
    case 'ChainExpression':
    case 'ExpressionStatement': {
      return hasNonFunctionYield(node.expression, checkYieldReturnValue);
    }

    /* c8 ignore next 2 -- In Babel? */
    // @ts-expect-error In Babel?
    case 'ClassProperty':

    /* c8 ignore next 2 -- In Babel? */
      // @ts-expect-error In Babel?
    case 'ObjectProperty':
    /* c8 ignore next 2 -- In Babel? */
    case 'Property':

    case 'PropertyDefinition':
      return node.computed && hasNonFunctionYield(node.key, checkYieldReturnValue) ||
      hasNonFunctionYield(node.value, checkYieldReturnValue);

    case 'ConditionalExpression':

    case 'IfStatement': {
      return hasNonFunctionYield(node.test, checkYieldReturnValue) ||
      hasNonFunctionYield(node.consequent, checkYieldReturnValue) ||
      hasNonFunctionYield(node.alternate, checkYieldReturnValue);
    }

    case 'DoWhileStatement':
    case 'ForInStatement':

    case 'ForOfStatement':

    case 'ForStatement':

    case 'LabeledStatement':
    case 'WhileStatement':
    case 'WithStatement': {
      return hasNonFunctionYield(node.body, checkYieldReturnValue);
    }

    /* c8 ignore next 2 -- In Babel? */
    // @ts-expect-error In Babel?
    case 'Import':
    case 'ImportExpression':
      return hasNonFunctionYield(node.source, checkYieldReturnValue);

    // ?.
      /* c8 ignore next 2 -- In Babel? */
    case 'MemberExpression':
    // @ts-expect-error In Babel?
    case 'OptionalMemberExpression':
      return hasNonFunctionYield(node.object, checkYieldReturnValue) ||
      hasNonFunctionYield(node.property, checkYieldReturnValue);

    case 'ObjectExpression':
    case 'ObjectPattern':
      return node.properties.some((property) => {
        return hasNonFunctionYield(property, checkYieldReturnValue);
      });
      /* c8 ignore next 2 -- In Babel? */
      // @ts-expect-error In Babel?
    case 'ObjectMethod':
    /* c8 ignore next 6 -- In Babel? */
    // @ts-expect-error In Babel?
      return node.computed && hasNonFunctionYield(node.key, checkYieldReturnValue) ||
      // @ts-expect-error In Babel?
      node.arguments.some((nde) => {
        return hasNonFunctionYield(nde, checkYieldReturnValue);
      });
    case 'ReturnStatement': {
      if (node.argument === null) {
        return false;
      }

      return hasNonFunctionYield(node.argument, checkYieldReturnValue);
    }

    // Comma
    case 'SequenceExpression':

    case 'TemplateLiteral':
      return node.expressions.some((subExpression) => {
        return hasNonFunctionYield(subExpression, checkYieldReturnValue);
      });
    case 'SpreadElement':

    case 'UnaryExpression':
      return hasNonFunctionYield(node.argument, checkYieldReturnValue);

    case 'SwitchStatement': {
      return node.cases.some(
        (someCase) => {
          return someCase.consequent.some((nde) => {
            return hasNonFunctionYield(nde, checkYieldReturnValue);
          });
        },
      );
    }

    case 'TaggedTemplateExpression':
      return hasNonFunctionYield(node.quasi, checkYieldReturnValue);

    case 'TryStatement': {
      return hasNonFunctionYield(node.block, checkYieldReturnValue) ||
      hasNonFunctionYield(
        node.handler && node.handler.body, checkYieldReturnValue,
      ) ||
      hasNonFunctionYield(
        /** @type {import('@typescript-eslint/types').TSESTree.BlockStatement} */
        (node.finalizer),
        checkYieldReturnValue,
      );
    }

    case 'VariableDeclaration': {
      return node.declarations.some((nde) => {
        return hasNonFunctionYield(nde, checkYieldReturnValue);
      });
    }

    case 'VariableDeclarator': {
      return hasNonFunctionYield(node.id, checkYieldReturnValue) ||
      hasNonFunctionYield(node.init, checkYieldReturnValue);
    }

    case 'YieldExpression': {
      if (checkYieldReturnValue) {
        if (
        /** @type {import('eslint').Rule.Node} */ (
            node
          ).parent?.type === 'VariableDeclarator'
        ) {
          return true;
        }

        return false;
      }

      // void return does not count.
      if (node.argument === null) {
        return false;
      }

      return true;
    }

    default: {
      return false;
    }
  }
};

/**
 * Checks if a node has a return statement. Void return does not count.
 * @param {ESTreeOrTypeScriptNode} node
 * @param {boolean} [checkYieldReturnValue]
 * @returns {boolean}
 */
const hasYieldValue = (node, checkYieldReturnValue) => {
  return /** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
    node
  ).generator && (
    /** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */ (
      node
    ).expression || hasNonFunctionYield(
      /** @type {import('@typescript-eslint/types').TSESTree.FunctionDeclaration} */
      (node).body,
      checkYieldReturnValue,
    )
  );
};

/**
 * Checks if a node has a throws statement.
 * @param {ESTreeOrTypeScriptNode|null|undefined} node
 * @param {boolean} [innerFunction]
 * @returns {boolean}
 */
const hasThrowValue = (node, innerFunction) => {
  if (!node) {
    return false;
  }

  // There are cases where a function may execute its inner function which
  //   throws, but we're treating functions atomically rather than trying to
  //   follow them
  switch (node.type) {
    case 'ArrowFunctionExpression':
    case 'FunctionDeclaration':
    case 'FunctionExpression': {
      return !innerFunction && !node.async && hasThrowValue(node.body, true);
    }

    case 'BlockStatement': {
      return node.body.some((bodyNode) => {
        return bodyNode.type !== 'FunctionDeclaration' && hasThrowValue(bodyNode);
      });
    }

    case 'DoWhileStatement':
    case 'ForInStatement':
    case 'ForOfStatement':
    case 'ForStatement':
    case 'LabeledStatement':
    case 'WhileStatement':
    case 'WithStatement': {
      return hasThrowValue(node.body);
    }

    case 'IfStatement': {
      return hasThrowValue(node.consequent) || hasThrowValue(node.alternate);
    }

    case 'SwitchStatement': {
      return node.cases.some(
        (someCase) => {
          return someCase.consequent.some((nde) => {
            return hasThrowValue(nde);
          });
        },
      );
    }

    case 'ThrowStatement': {
      return true;
    }

    // We only consider it to throw an error if the catch or finally blocks throw an error.
    case 'TryStatement': {
      return hasThrowValue(node.handler && node.handler.body) ||
        hasThrowValue(node.finalizer);
    }

    default: {
      return false;
    }
  }
};

/**
 * @param {string} tag
 */
/*
const isInlineTag = (tag) => {
  return /^(@link|@linkcode|@linkplain|@tutorial) /v.test(tag);
};
*/

/**
 * Parses GCC Generic/Template types
 * @see {@link https://github.com/google/closure-compiler/wiki/Generic-Types}
 * @see {@link https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#template}
 * @param {import('comment-parser').Spec} tag
 * @returns {string[]}
 */
const parseClosureTemplateTag = (tag) => {
  return tag.name
    .split(',')
    .map((type) => {
      return type.trim().replace(/^\[?(?<name>.*?)=.*$/v, '$<name>');
    });
};

/**
 * @typedef {true|string[]} DefaultContexts
 */

/**
 * Checks user option for `contexts` array, defaulting to
 * contexts designated by the rule. Returns an array of
 * ESTree AST types, indicating allowable contexts.
 * @param {import('eslint').Rule.RuleContext} context
 * @param {DefaultContexts|undefined} defaultContexts
 * @param {{
 *   contexts?: import('./iterateJsdoc.js').Context[]
 * }} settings
 * @returns {(string|import('./iterateJsdoc.js').ContextObject)[]}
 */
const enforcedContexts = (context, defaultContexts, settings) => {
  const contexts = context.options[0]?.contexts || settings.contexts || (defaultContexts === true ? [
    'ArrowFunctionExpression',
    'FunctionDeclaration',
    'FunctionExpression',
    'TSDeclareFunction',
  ] : defaultContexts);

  return contexts;
};

/**
 * @param {import('./iterateJsdoc.js').Context[]} contexts
 * @param {import('./iterateJsdoc.js').CheckJsdoc} checkJsdoc
 * @param {import('@es-joy/jsdoccomment').CommentHandler} [handler]
 * @returns {import('eslint').Rule.RuleListener}
 */
const getContextObject = (contexts, checkJsdoc, handler) => {
  /** @type {import('eslint').Rule.RuleListener} */
  const properties = {};

  for (const [
    idx,
    prop,
  ] of contexts.entries()) {
    /** @type {string} */
    let property;

    /** @type {(node: import('eslint').Rule.Node) => void} */
    let value;

    if (typeof prop === 'object') {
      const selInfo = {
        lastIndex: idx,
        selector: prop.context,
      };
      if (prop.comment) {
        property = /** @type {string} */ (prop.context);
        value = checkJsdoc.bind(
          null,
          {
            ...selInfo,
            comment: prop.comment,
          },
          /**
           * @type {(jsdoc: import('@es-joy/jsdoccomment').JsdocBlockWithInline) => boolean}
           */
          (/** @type {import('@es-joy/jsdoccomment').CommentHandler} */ (
            handler
          ).bind(null, prop.comment)),
        );
      } else {
        property = /** @type {string} */ (prop.context);
        value = checkJsdoc.bind(null, selInfo, null);
      }
    } else {
      const selInfo = {
        lastIndex: idx,
        selector: prop,
      };
      property = prop;
      value = checkJsdoc.bind(null, selInfo, null);
    }

    const old = /**
                 * @type {((node: import('eslint').Rule.Node) => void)}
                 */ (properties[property]);
    properties[property] = old ?
      /**
       * @type {((node: import('eslint').Rule.Node) => void)}
       */
      function (node) {
        old(node);
        value(node);
      } :
      value;
  }

  return properties;
};

const tagsWithNamesAndDescriptions = new Set([
  'arg', 'argument', 'param', 'prop', 'property',
  'return',

  // These two are parsed by our custom parser as though having a `name`
  'returns', 'template',
]);

/**
 * @typedef {{
 *   [key: string]: false|string|
 *     {message: string, replacement?: string}
 * }} TagNamePreference
 */

/**
 * @param {import('eslint').Rule.RuleContext} context
 * @param {ParserMode|undefined} mode
 * @param {import('comment-parser').Spec[]} tags
 * @returns {{
 *   tagsWithNames: import('comment-parser').Spec[],
 *   tagsWithoutNames: import('comment-parser').Spec[]
 * }}
 */
const getTagsByType = (context, mode, tags) => {
  /**
   * @type {import('comment-parser').Spec[]}
   */
  const tagsWithoutNames = [];
  const tagsWithNames = tags.filter((tag) => {
    const {
      tag: tagName,
    } = tag;
    const tagWithName = tagsWithNamesAndDescriptions.has(tagName);
    if (!tagWithName) {
      tagsWithoutNames.push(tag);
    }

    return tagWithName;
  });

  return {
    tagsWithNames,
    tagsWithoutNames,
  };
};

/**
 * @param {import('eslint').SourceCode|{
 *   text: string
 * }} sourceCode
 * @returns {string}
 */
const getIndent = (sourceCode) => {
  return (sourceCode.text.match(/^\n*([ \t]+)/v)?.[1] ?? '') + ' ';
};

/**
 * @param {import('eslint').Rule.Node|null} node
 * @returns {boolean}
 */
const isConstructor = (node) => {
  return node?.type === 'MethodDefinition' && node.kind === 'constructor' ||
  /** @type {import('@typescript-eslint/types').TSESTree.MethodDefinition} */ (
    node?.parent
  )?.kind === 'constructor';
};

/**
 * @param {import('eslint').Rule.Node|null} node
 * @returns {boolean}
 */
const isGetter = (node) => {
  return node !== null &&
  /**
   * @type {import('@typescript-eslint/types').TSESTree.MethodDefinition|
   *   import('@typescript-eslint/types').TSESTree.Property}
   */ (
    node.parent
  )?.kind === 'get';
};

/**
 * @param {import('eslint').Rule.Node|null} node
 * @returns {boolean}
 */
const isSetter = (node) => {
  return node !== null &&
  /**
   * @type {import('@typescript-eslint/types').TSESTree.MethodDefinition|
   *   import('@typescript-eslint/types').TSESTree.Property}
   */(
    node.parent
  )?.kind === 'set';
};

/**
 * @param {import('eslint').Rule.Node} node
 * @returns {boolean}
 */
const hasAccessorPair = (node) => {
  const {
    key,
    kind: sourceKind,
    type,
  } =
    /**
     * @type {import('@typescript-eslint/types').TSESTree.MethodDefinition|
     *   import('@typescript-eslint/types').TSESTree.Property}
     */ (node);

  const sourceName =
    /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
      key
    ).name;

  const oppositeKind = sourceKind === 'get' ? 'set' : 'get';

  const sibling = type === 'MethodDefinition' ?
    /** @type {import('@typescript-eslint/types').TSESTree.ClassBody} */ (
      node.parent
    ).body :
    /** @type {import('@typescript-eslint/types').TSESTree.ObjectExpression} */ (
      node.parent
    ).properties;

  return (
    sibling.some((child) => {
      const {
        key: ky,
        kind,
      } = /**
           * @type {import('@typescript-eslint/types').TSESTree.MethodDefinition|
           *   import('@typescript-eslint/types').TSESTree.Property}
           */ (child);

      const name =
        /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
          ky
        ).name;

      return kind === oppositeKind && name === sourceName;
    })
  );
};

/**
 * @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
 * @param {import('eslint').Rule.Node|null} node
 * @param {import('eslint').Rule.RuleContext} context
 * @param {import('json-schema').JSONSchema4} schema
 * @returns {boolean}
 */
const exemptSpeciaMethods = (jsdoc, node, context, schema) => {
  /**
   * @param {"checkGetters"|"checkSetters"|"checkConstructors"} prop
   * @returns {boolean|"no-setter"|"no-getter"}
   */
  const hasSchemaOption = (prop) => {
    const schemaProperties = schema[0].properties;

    return context.options[0]?.[prop] ??
      (schemaProperties[prop] && schemaProperties[prop].default);
  };

  const checkGetters = hasSchemaOption('checkGetters');
  const checkSetters = hasSchemaOption('checkSetters');

  return !hasSchemaOption('checkConstructors') &&
    (
      isConstructor(node) ||
      hasATag(jsdoc, [
        'class',
        'constructor',
      ])) ||
  isGetter(node) && (
    !checkGetters ||
    checkGetters === 'no-setter' && hasAccessorPair(
      /** @type {import('./iterateJsdoc.js').Node} */
      (/** @type {import('./iterateJsdoc.js').Node} */ (node).parent),
    )
  ) ||
  isSetter(node) && (
    !checkSetters ||
    checkSetters === 'no-getter' && hasAccessorPair(
      /** @type {import('./iterateJsdoc.js').Node} */
      (/** @type {import('./iterateJsdoc.js').Node} */ (node).parent),
    )
  );
};

/**
 * Since path segments may be unquoted (if matching a reserved word,
 * identifier or numeric literal) or single or double quoted, in either
 * the `@param` or in source, we need to strip the quotes to give a fair
 * comparison.
 * @param {string} str
 * @returns {string}
 */
const dropPathSegmentQuotes = (str) => {
  return str.replaceAll(/\.(['"])(.*)\1/gv, '.$2');
};

/**
 * @param {string} name
 * @returns {(otherPathName: string) => boolean}
 */
const comparePaths = (name) => {
  return (otherPathName) => {
    return otherPathName === name ||
      dropPathSegmentQuotes(otherPathName) === dropPathSegmentQuotes(name);
  };
};

/**
 * @callback PathDoesNotBeginWith
 * @param {string} name
 * @param {string} otherPathName
 * @returns {boolean}
 */

/** @type {PathDoesNotBeginWith} */
const pathDoesNotBeginWith = (name, otherPathName) => {
  return !name.startsWith(otherPathName) &&
    !dropPathSegmentQuotes(name).startsWith(dropPathSegmentQuotes(otherPathName));
};

/**
 * @param {string} regexString
 * @param {string} [requiredFlags]
 * @returns {RegExp}
 */
const getRegexFromString = (regexString, requiredFlags) => {
  const match = regexString.match(/^\/(.*)\/([gimyvus]*)$/vs);
  let flags = 'v';
  let regex = regexString;
  if (match) {
    [
      , regex,
      flags,
    ] = match;
    if (!flags) {
      flags = 'v';
    }
  }

  const uniqueFlags = [
    ...new Set(flags + (requiredFlags || '')),
  ];
  flags = uniqueFlags.join('');

  return new RegExp(regex, flags);
};

const strictNativeTypes = [
  'undefined',
  'null',
  'boolean',
  'number',
  'bigint',
  'string',
  'symbol',
  'object',
  'Array',
  'Function',
  'Date',
  'RegExp',
];

/**
 * @param {import('@es-joy/jsdoccomment').JsdocBlockWithInline} jsdoc
 * @param {import('@es-joy/jsdoccomment').JsdocTagWithInline} tag
 * @param {import('jsdoc-type-pratt-parser').RootResult} parsedType
 * @param {string} indent
 * @param {string} typeBracketSpacing
 */
const rewireByParsedType = (jsdoc, tag, parsedType, indent, typeBracketSpacing = '') => {
  const typeLines = stringify(parsedType).split('\n');
  const firstTypeLine = typeLines.shift();
  const lastTypeLine = typeLines.pop();

  const beginNameOrDescIdx = tag.source.findIndex(({
    tokens,
  }) => {
    return tokens.name || tokens.description;
  });

  const nameAndDesc = beginNameOrDescIdx === -1 ?
    null :
    tag.source.slice(beginNameOrDescIdx);

  const initialNumber = tag.source[0].number;

  const src = [
    // Get inevitably present tag from first `tag.source`
    {
      number: initialNumber,
      source: '',
      tokens: {
        ...tag.source[0].tokens,
        ...(typeLines.length || lastTypeLine ? {
          end: '',
          name: '',
          postName: '',
          postType: '',
        } : (nameAndDesc ? {
          name: nameAndDesc[0].tokens.name,
          postType: ' ',
        } : {})),
        type: '{' + typeBracketSpacing + firstTypeLine + (!typeLines.length && lastTypeLine === undefined ? typeBracketSpacing + '}' : ''),
      },
    },
    // Get any intervening type lines
    ...(typeLines.length ? typeLines.map((typeLine, idx) => {
      return {
        number: initialNumber + idx + 1,
        source: '',
        tokens: {
          // Grab any delimiter info from first item
          ...tag.source[0].tokens,
          delimiter: tag.source[0].tokens.delimiter === '/**' ? '*' : tag.source[0].tokens.delimiter,
          end: '',
          name: '',
          postName: '',
          postTag: '',
          postType: '',
          start: indent + ' ',
          tag: '',
          type: typeLine,
        },
      };
    }) : []),
  ];

  // Merge any final type line and name and description
  if (
    // Name and description may be already included if present with the tag
    nameAndDesc && beginNameOrDescIdx > 0
  ) {
    if (typeLines.length || lastTypeLine !== undefined) {
      src.push({
        number: src.length + 1,
        source: '',
        tokens: {
          ...nameAndDesc[0].tokens,
          type: lastTypeLine + typeBracketSpacing + '}',
        },
      });
    }

    if (
      // Get any remaining description lines
      nameAndDesc.length > 1
    ) {
      src.push(
        ...nameAndDesc.slice(1).map(({
          source,
          tokens,
        }, idx) => {
          return {
            number: src.length + idx + 2,
            source,
            tokens,
          };
        }),
      );
    }
  } else if (nameAndDesc) {
    if ((typeLines.length || lastTypeLine !== undefined) && lastTypeLine) {
      src.push({
        number: src.length + 1,
        source: '',
        tokens: {
          ...nameAndDesc[0].tokens,
          delimiter: nameAndDesc[0].tokens.delimiter === '/**' ? '*' : nameAndDesc[0].tokens.delimiter,
          postTag: '',
          start: indent + ' ',
          tag: '',
          type: lastTypeLine + typeBracketSpacing + '}',
        },
      });
    }

    if (
      // Get any remaining description lines
      nameAndDesc.length > 1
    ) {
      src.push(
        ...nameAndDesc.slice(1).map(({
          source,
          tokens,
        }, idx) => {
          return {
            number: src.length + idx + 2,
            source,
            tokens,
          };
        }),
      );
    }
  } else if (lastTypeLine) {
    src.push({
      number: src.length + 1,
      source: '',
      tokens: {
        ...tag.source[0].tokens,
        delimiter: tag.source[0].tokens.delimiter === '/**' ? '*' : tag.source[0].tokens.delimiter,
        postTag: '',
        start: indent + ' ',
        tag: '',
        type: lastTypeLine + typeBracketSpacing + '}',
      },
    });
  }

  tag.source = src;

  // Properly rewire `jsdoc.source`
  const firstTagIdx = jsdoc.source.findIndex(({
    tokens: {
      tag: tg,
    },
  }) => {
    return tg;
  });

  const initialEndSource = jsdoc.source.find(({
    tokens: {
      end,
    },
  }) => {
    return end;
  });

  jsdoc.source = [
    ...jsdoc.source.slice(0, firstTagIdx),
    ...jsdoc.tags.flatMap(({
      source,
    }) => {
      return source;
    }),
  ];

  if (initialEndSource && !jsdoc.source.at(-1)?.tokens?.end) {
    jsdoc.source.push(initialEndSource);
  }
};

export {
  comparePaths,
  dropPathSegmentQuotes,
  enforcedContexts,
  exemptSpeciaMethods,
  filterTags,
  flattenRoots,
  forEachPreferredTag,
  getAllTags,
  getContextObject,
  getFunctionParameterNames,
  getIndent,
  getInlineTags,
  getJsdocTagsDeep,
  getPreferredTagName,
  getPreferredTagNameSimple,
  getRegexFromString,
  getTagDescription,
  getTags,
  getTagsByType,
  getTagStructureForMode,
  hasATag,
  hasParams,

  hasTag,
  hasThrowValue,

  hasYieldValue,
  isConstructor,
  isGetter,
  isNameOrNamepathDefiningTag,
  isNamepathOrUrlReferencingTag,
  isNamepathReferencingTag,
  isSetter,
  isValidTag,
  mayBeUndefinedTypeTag,
  overrideTagStructure,
  parseClosureTemplateTag,
  pathDoesNotBeginWith,
  rewireByParsedType,
  setTagStructure,
  strictNativeTypes,
  tagMightHaveEitherTypeOrNamePosition,
  tagMightHaveName,
  tagMightHaveNameOrNamepath,
  tagMightHaveNamepath,
  tagMightHaveNamePosition,
  tagMightHaveTypePosition,
  tagMissingRequiredTypeOrNamepath,
  tagMustHaveNamePosition,
  tagMustHaveTypePosition,
};
export {
  hasReturnValue,
  hasValueOrExecutorHasNonEmptyResolveValue,
} from './utils/hasReturnValue.js';
