// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// @ts-check
import { ESLintUtils } from '@typescript-eslint/utils';
import { createSyncFn } from 'synckit';

/**
 * @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
 */

const worker = createSyncFn(import.meta.resolve('./enforceTw.worker.mjs'));

export const enforceTw = ESLintUtils.RuleCreator.withoutDocs({
  name: 'enforce-tw',
  meta: {
    type: 'problem',
    messages: {
      needsTw: 'Tailwind classes must be wrapped with tw()',
    },
    schema: [],
    defaultOptions: [],
  },
  create(context) {
    /**
     * @param {string} input
     * @param {Node} node
     */
    function check(input, node) {
      if (typeof input !== 'string') {
        throw new Error(`Unexpected input ${input} for node type ${node.type}`);
      }

      const tailwindClasses = worker(input.split(/\s+/));

      for (const tailwindClass of tailwindClasses) {
        const index = input.indexOf(tailwindClass) + 1;
        const length = tailwindClass.length;
        context.report({
          node,
          loc: {
            start: {
              line: node.loc.start.line,
              column: node.loc.start.column + index,
            },
            end: {
              line: node.loc.end.line,
              column: node.loc.start.column + index + length,
            },
          },
          messageId: 'needsTw',
        });
      }
    }

    /**
     * @param {Node} node
     */
    function traverse(node) {
      if (node.type === 'Literal') {
        if (typeof node.value === 'string') {
          check(node.value, node);
        }
        // ignore other literals
      } else if (node.type === 'TemplateLiteral') {
        for (const element of node.quasis) {
          traverse(element);
        }
        for (const expression of node.expressions) {
          traverse(expression);
        }
      } else if (node.type === 'TemplateElement') {
        if (node.value.cooked != null) {
          check(node.value.cooked, node);
        }
      } else if (node.type === 'JSXExpressionContainer') {
        traverse(node.expression);
      } else if (node.type === 'ConditionalExpression') {
        // ignore node.test
        traverse(node.consequent);
        traverse(node.alternate);
      } else if (node.type === 'LogicalExpression') {
        if (node.operator === '||' || node.operator === '??') {
          traverse(node.left);
        }
        traverse(node.right);
      } else if (node.type === 'BinaryExpression') {
        if (node.operator === '+') {
          traverse(node.left);
          traverse(node.right);
        } else {
          throw new Error(`Unexpected binary operator: ${node.operator}`);
        }
      } else if (node.type === 'ObjectExpression') {
        for (const prop of node.properties) {
          traverse(prop);
        }
      } else if (node.type === 'Property') {
        if (node.key.type === 'Identifier') {
          if (!node.computed) {
            check(node.key.name, node.key);
          }
          // ignore computed
        } else if (node.key.type === 'Literal') {
          traverse(node.key);
        } else if (node.key.type === 'TemplateLiteral') {
          traverse(node.key);
        } else if (node.key.type === 'CallExpression') {
          // ignore
        } else {
          throw new Error(`Unexpected property key type: ${node.key.type}`);
        }
      } else if (node.type === 'ArrayExpression') {
        for (const element of node.elements) {
          if (element != null) {
            traverse(element);
          }
        }
      } else if (node.type === 'Identifier') {
        // ignore
      } else if (node.type === 'CallExpression') {
        // ignore
      } else if (node.type === 'MemberExpression') {
        // ignore
      } else {
        throw new Error(`Unexpected traverse node type: ${node.type}`);
      }
    }

    return {
      CallExpression(node) {
        if (node.callee.type !== 'Identifier') return;
        if (node.callee.name !== 'classNames') return;
        for (const arg of node.arguments) {
          traverse(arg);
        }
      },
      JSXAttribute(node) {
        if (node.name.type !== 'JSXIdentifier') return;
        if (node.name.name !== 'className') return;
        if (node.value != null) {
          traverse(node.value);
        }
      },
    };
  },
});
