import {
  getSettings,
} from '../iterateJsdoc.js';
import {
  enforcedContexts,
  getContextObject,
  getIndent,
} from '../jsdocUtils.js';
import {
  getDecorator,
  getFollowingComment,
  getNonJsdocComment,
  getReducedASTNode,
} from '@es-joy/jsdoccomment';

/** @type {import('eslint').Rule.RuleModule} */
export default {
  create (context) {
    /**
     * @typedef {import('eslint').AST.Token | import('estree').Comment | {
     *   type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang",
     *   range: [number, number],
     *   value: string
     * }} Token
     */

    /**
     * @callback AddComment
     * @param {boolean|undefined} inlineCommentBlock
     * @param {Token} comment
     * @param {string} indent
     * @param {number} lines
     * @param {import('eslint').Rule.RuleFixer} fixer
     */

    /* c8 ignore next -- Fallback to deprecated method */
    const {
      sourceCode = context.getSourceCode(),
    } = context;
    const settings = getSettings(context);
    if (!settings) {
      return {};
    }

    const {
      allowedPrefixes = [
        '@ts-', 'istanbul ', 'c8 ', 'v8 ', 'eslint', 'prettier-',
      ],
      contexts = settings.contexts || [],
      contextsAfter = /** @type {string[]} */ ([]),
      contextsBeforeAndAfter = [
        'VariableDeclarator', 'TSPropertySignature', 'PropertyDefinition',
      ],
      enableFixer = true,
      enforceJsdocLineStyle = 'multi',
      lineOrBlockStyle = 'both',
    } = context.options[0] ?? {};

    let reportingNonJsdoc = false;

    /**
     * @param {string} messageId
     * @param {import('estree').Comment|Token} comment
     * @param {import('eslint').Rule.Node} node
     * @param {import('eslint').Rule.ReportFixer} fixer
     */
    const report = (messageId, comment, node, fixer) => {
      const loc = {
        end: {
          column: 0,
          /* c8 ignore next 2 -- Guard */
          // @ts-expect-error Ok
          line: (comment.loc?.start?.line ?? 1),
        },
        start: {
          column: 0,
          /* c8 ignore next 2 -- Guard */
          // @ts-expect-error Ok
          line: (comment.loc?.start?.line ?? 1),
        },
      };

      context.report({
        fix: enableFixer ? fixer : null,
        loc,
        messageId,
        node,
      });
    };

    /**
     * @param {import('eslint').Rule.Node} node
     * @param {import('eslint').AST.Token | import('estree').Comment | {
     *   type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang",
     *   range: [number, number],
     *   value: string
     * }} comment
     * @param {AddComment} addComment
     * @param {import('../iterateJsdoc.js').Context[]} ctxts
     */
    const getFixer = (node, comment, addComment, ctxts) => {
      return /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
        // Default to one line break if the `minLines`/`maxLines` settings allow
        const lines = settings.minLines === 0 && settings.maxLines >= 1 ? 1 : settings.minLines;
        let baseNode =
          /**
           * @type {import('@typescript-eslint/types').TSESTree.Node|import('eslint').Rule.Node}
           */ (
            getReducedASTNode(node, sourceCode)
          );

        const decorator = getDecorator(
          /** @type {import('eslint').Rule.Node} */
          (baseNode),
        );
        if (decorator) {
          baseNode = /** @type {import('@typescript-eslint/types').TSESTree.Decorator} */ (
            decorator
          );
        }

        const indent = getIndent({
          text: sourceCode.getText(
            /** @type {import('eslint').Rule.Node} */ (baseNode),
            /** @type {import('eslint').AST.SourceLocation} */
            (
              /** @type {import('eslint').Rule.Node} */ (baseNode).loc
            ).start.column,
          ),
        });

        const {
          inlineCommentBlock,
        } =
          /**
           * @type {{
           *     context: string,
           *     inlineCommentBlock: boolean,
           *     minLineCount: import('../iterateJsdoc.js').Integer
           *   }[]}
           */ (ctxts).find((contxt) => {
            if (typeof contxt === 'string') {
              return false;
            }

            const {
              context: ctxt,
            } = contxt;
            return ctxt === node.type;
          }) || {};

        return addComment(inlineCommentBlock, comment, indent, lines, fixer);
      };
    };

    /**
     * @param {import('eslint').AST.Token | import('estree').Comment | {
     *   type: import('eslint').AST.TokenType|"Line"|"Block"|"Shebang",
     *   range: [number, number],
     *   value: string
     * }} comment
     * @param {import('eslint').Rule.Node} node
     * @param {AddComment} addComment
     * @param {import('../iterateJsdoc.js').Context[]} ctxts
     */
    const reportings = (comment, node, addComment, ctxts) => {
      const fixer = getFixer(node, comment, addComment, ctxts);

      if (comment.type === 'Block') {
        if (lineOrBlockStyle === 'line') {
          return;
        }

        report('blockCommentsJsdocStyle', comment, node, fixer);
        return;
      }

      if (comment.type === 'Line') {
        if (lineOrBlockStyle === 'block') {
          return;
        }

        report('lineCommentsJsdocStyle', comment, node, fixer);
      }
    };

    /**
     * @type {import('../iterateJsdoc.js').CheckJsdoc}
     */
    const checkNonJsdoc = (_info, _handler, node) => {
      const comment = getNonJsdocComment(sourceCode, node, settings);

      if (
        !comment ||
        /** @type {string[]} */
        (allowedPrefixes).some((prefix) => {
          return comment.value.trimStart().startsWith(prefix);
        })
      ) {
        return;
      }

      reportingNonJsdoc = true;

      /** @type {AddComment} */
      // eslint-disable-next-line unicorn/consistent-function-scoping -- Avoid conflicts
      const addComment = (inlineCommentBlock, commentToAdd, indent, lines, fixer) => {
        const insertion = (
          inlineCommentBlock || enforceJsdocLineStyle === 'single' ?
            `/** ${commentToAdd.value.trim()} ` :
            `/**\n${indent}*${commentToAdd.value.trimEnd()}\n${indent}`
        ) +
            `*/${'\n'.repeat((lines || 1) - 1)}`;

        return fixer.replaceText(
          /** @type {import('eslint').AST.Token} */
          (commentToAdd),
          insertion,
        );
      };

      reportings(comment, node, addComment, contexts);
    };

    /**
     * @param {import('eslint').Rule.Node} node
     * @param {import('../iterateJsdoc.js').Context[]} ctxts
     */
    const checkNonJsdocAfter = (node, ctxts) => {
      const comment = getFollowingComment(sourceCode, node);

      if (
        !comment ||
        comment.value.startsWith('*') ||
        /** @type {string[]} */
        (allowedPrefixes).some((prefix) => {
          return comment.value.trimStart().startsWith(prefix);
        })
      ) {
        return;
      }

      /** @type {AddComment} */
      const addComment = (inlineCommentBlock, commentToAdd, indent, lines, fixer) => {
        const insertion = (
          inlineCommentBlock || enforceJsdocLineStyle === 'single' ?
            `/** ${commentToAdd.value.trim()} ` :
            `/**\n${indent}*${commentToAdd.value.trimEnd()}\n${indent}`
        ) +
            `*/${'\n'.repeat((lines || 1) - 1)}${lines ? `\n${indent.slice(1)}` : ' '}`;

        return [
          fixer.remove(
          /** @type {import('eslint').AST.Token} */
            (commentToAdd),
          ), fixer.insertTextBefore(
            node.type === 'VariableDeclarator' ? node.parent : node,
            insertion,
          ),
        ];
      };

      reportings(comment, node, addComment, ctxts);
    };

    // Todo: add contexts to check after (and handle if want both before and after)
    return {
      ...getContextObject(
        enforcedContexts(context, true, settings),
        checkNonJsdoc,
      ),
      ...getContextObject(
        contextsAfter,
        (_info, _handler, node) => {
          checkNonJsdocAfter(node, contextsAfter);
        },
      ),
      ...getContextObject(
        contextsBeforeAndAfter,
        (_info, _handler, node) => {
          checkNonJsdoc({}, null, node);
          if (!reportingNonJsdoc) {
            checkNonJsdocAfter(node, contextsBeforeAndAfter);
          }
        },
      ),
    };
  },
  meta: {
    docs: {
      description: 'Converts non-JSDoc comments preceding or following nodes into JSDoc ones',
      url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/convert-to-jsdoc-comments.md#repos-sticky-header',
    },

    fixable: 'code',

    messages: {
      blockCommentsJsdocStyle: 'Block comments should be JSDoc-style.',
      lineCommentsJsdocStyle: 'Line comments should be JSDoc-style.',
    },
    schema: [
      {
        additionalProperties: false,
        properties: {
          allowedPrefixes: {
            description: `An array of prefixes to allow at the beginning of a comment.

Defaults to \`['@ts-', 'istanbul ', 'c8 ', 'v8 ', 'eslint', 'prettier-']\`.

Supplying your own value overrides the defaults.`,
            items: {
              type: 'string',
            },
            type: 'array',
          },
          contexts: {
            description: `The contexts array which will be checked for preceding content.

Can either be strings or an object with a \`context\` string and an optional, default \`false\` \`inlineCommentBlock\` boolean.

Defaults to \`ArrowFunctionExpression\`, \`FunctionDeclaration\`,
\`FunctionExpression\`, \`TSDeclareFunction\`.`,
            items: {
              anyOf: [
                {
                  type: 'string',
                },
                {
                  additionalProperties: false,
                  properties: {
                    context: {
                      type: 'string',
                    },
                    inlineCommentBlock: {
                      type: 'boolean',
                    },
                  },
                  type: 'object',
                },
              ],
            },
            type: 'array',
          },
          contextsAfter: {
            description: `The contexts array which will be checked for content on the same line after.

Can either be strings or an object with a \`context\` string and an optional, default \`false\` \`inlineCommentBlock\` boolean.

Defaults to an empty array.`,
            items: {
              anyOf: [
                {
                  type: 'string',
                },
                {
                  additionalProperties: false,
                  properties: {
                    context: {
                      type: 'string',
                    },
                    inlineCommentBlock: {
                      type: 'boolean',
                    },
                  },
                  type: 'object',
                },
              ],
            },
            type: 'array',
          },
          contextsBeforeAndAfter: {
            description: `The contexts array which will be checked for content before and on the same
line after.

Can either be strings or an object with a \`context\` string and an optional, default \`false\` \`inlineCommentBlock\` boolean.

Defaults to \`VariableDeclarator\`, \`TSPropertySignature\`, \`PropertyDefinition\`.`,
            items: {
              anyOf: [
                {
                  type: 'string',
                },
                {
                  additionalProperties: false,
                  properties: {
                    context: {
                      type: 'string',
                    },
                    inlineCommentBlock: {
                      type: 'boolean',
                    },
                  },
                  type: 'object',
                },
              ],
            },
            type: 'array',
          },
          enableFixer: {
            description: 'Set to `false` to disable fixing.',
            type: 'boolean',
          },
          enforceJsdocLineStyle: {
            description: `What policy to enforce on the conversion of non-JSDoc comments without
line breaks. (Non-JSDoc (mulitline) comments with line breaks will always
be converted to \`multi\` style JSDoc comments.)

- \`multi\` - Convert to multi-line style
\`\`\`js
/**
 * Some text
 */
\`\`\`
- \`single\` - Convert to single-line style
\`\`\`js
/** Some text */
\`\`\`

Defaults to \`multi\`.`,
            enum: [
              'multi', 'single',
            ],
            type: 'string',
          },
          lineOrBlockStyle: {
            description: `What style of comments to which to apply JSDoc conversion.

- \`block\` - Applies to block-style comments (\`/* ... */\`)
- \`line\` - Applies to line-style comments (\`// ...\`)
- \`both\` - Applies to both block and line-style comments

Defaults to \`both\`.`,
            enum: [
              'block', 'line', 'both',
            ],
            type: 'string',
          },
        },
        type: 'object',
      },
    ],
    type: 'suggestion',
  },
};
