/**
 * Oxlint rule: require-cpk-prefix
 *
 * Enforces that Tailwind utility classes in className attributes and
 * class-helper calls (cn, twMerge, cva, clsx) use the `cpk:` prefix.
 * Also detects the prefix in the wrong position (after variants instead of before).
 *
 * In Tailwind v4 with prefix(cpk), the prefix MUST come before all variants:
 *   cpk:dark:hover:bg-white  ✓  (generates CSS)
 *   dark:hover:cpk:bg-white  ✗  (generates NO CSS)
 *
 * Detection logic reused from scripts/add-cpk-prefix.mjs.
 */

const PREFIX = "cpk:";

// ── Tailwind utility detection ──────────────────────────────────────────────

const SINGLE_WORD_UTILITIES = new Set([
  "absolute",
  "antialiased",
  "block",
  "border",
  "capitalize",
  "collapse",
  "container",
  "contents",
  "fixed",
  "flex",
  "grid",
  "grow",
  "hidden",
  "inline",
  "inline-block",
  "inline-flex",
  "inline-grid",
  "invisible",
  "isolate",
  "italic",
  "lowercase",
  "ordinal",
  "outline",
  "overflow",
  "overline",
  "relative",
  "resize",
  "ring",
  "rounded",
  "shadow",
  "shrink",
  "static",
  "sticky",
  "table",
  "truncate",
  "underline",
  "uppercase",
  "visible",
  "prose",
]);

const JS_VALUE_WORDS = new Set([
  "absolute",
  "static",
  "relative",
  "fixed",
  "sticky",
  "contents",
  "none",
  "auto",
  "inherit",
  "initial",
  "unset",
  "revert",
  "block",
  "inline",
  "flex",
  "grid",
  "hidden",
  "smooth",
  "instant",
  "nearest",
  "button",
  "submit",
  "reset",
  "input",
  "transcribe",
  "processing",
  "recording",
  "idle",
  "text",
  "password",
  "email",
  "number",
  "tel",
  "url",
  "search",
  "top",
  "bottom",
  "left",
  "right",
  "start",
  "end",
  "compact",
  "expanded",
  "before",
  "after",
  "open",
  "closed",
  "default",
  "destructive",
  "outline",
  "secondary",
  "ghost",
  "link",
]);

const VARIANT_RE =
  /^(?:dark|hover|focus|focus-visible|focus-within|active|disabled|visited|checked|required|invalid|first|last|odd|even|only|empty|enabled|read-only|placeholder-shown|autofill|default|indeterminate|open|closed|group-hover|group-focus|peer-hover|peer-focus|peer-checked|placeholder|before|after|selection|marker|first-line|first-letter|file|sm|md|lg|xl|2xl|portrait|landscape|motion-safe|motion-reduce|contrast-more|contrast-less|forced-colors|print|ltr|rtl|aria-invalid|aria-checked|aria-disabled|aria-expanded|aria-hidden|aria-pressed|aria-readonly|aria-required|aria-selected|has-\[.+?\]|not-\[.+?\]|group-data-\[.+?\]|supports-\[.+?\]|min-\[.+?\]|max-\[.+?\]|data-\[.+?\]|aria-\[.+?\]|\[.+?\]):/;

function stripVariants(token) {
  let base = token;
  let iterations = 0;
  while (VARIANT_RE.test(base) && iterations < 20) {
    base = base.replace(VARIANT_RE, "");
    iterations++;
  }
  return base;
}

function isBaseUtility(base) {
  if (!base) return false;

  let b = base;
  if (b.startsWith("-") || b.startsWith("!")) b = b.slice(1);

  const slashIdx = b.indexOf("/");
  let bNoOpacity = b;
  if (slashIdx > 0 && !b.includes("[")) {
    bNoOpacity = b.slice(0, slashIdx);
  }

  if (SINGLE_WORD_UTILITIES.has(bNoOpacity)) return true;

  const prefixes = [
    "bg-",
    "text-",
    "font-",
    "p-",
    "px-",
    "py-",
    "pt-",
    "pb-",
    "pl-",
    "pr-",
    "m-",
    "mx-",
    "my-",
    "mt-",
    "mb-",
    "ml-",
    "mr-",
    "w-",
    "h-",
    "min-w-",
    "max-w-",
    "min-h-",
    "max-h-",
    "size-",
    "flex-",
    "grid-",
    "col-",
    "row-",
    "auto-cols-",
    "auto-rows-",
    "gap-",
    "gap-x-",
    "gap-y-",
    "space-x-",
    "space-y-",
    "items-",
    "justify-",
    "self-",
    "content-",
    "place-",
    "border-",
    "rounded-",
    "ring-",
    "outline-",
    "divide-",
    "shadow-",
    "z-",
    "inset-",
    "inset-x-",
    "inset-y-",
    "top-",
    "right-",
    "bottom-",
    "left-",
    "start-",
    "end-",
    "leading-",
    "tracking-",
    "whitespace-",
    "break-",
    "indent-",
    "align-",
    "decoration-",
    "underline-offset-",
    "opacity-",
    "overflow-",
    "object-",
    "float-",
    "clear-",
    "transition-",
    "duration-",
    "ease-",
    "delay-",
    "animate-",
    "scale-",
    "rotate-",
    "translate-",
    "skew-",
    "origin-",
    "cursor-",
    "pointer-events-",
    "select-",
    "touch-",
    "scroll-",
    "snap-",
    "accent-",
    "caret-",
    "will-change-",
    "contain-",
    "fill-",
    "stroke-",
    "aspect-",
    "columns-",
    "from-",
    "via-",
    "to-",
    "gradient-",
    "backdrop-",
    "blur-",
    "brightness-",
    "contrast-",
    "drop-shadow-",
    "grayscale-",
    "hue-rotate-",
    "invert-",
    "saturate-",
    "sepia-",
    "ring-offset-",
    "list-",
    "order-",
    "basis-",
    "grow-",
    "shrink-",
    "sr-",
    "appearance-",
    "transform-",
  ];

  for (const p of prefixes) {
    if (bNoOpacity.startsWith(p)) return true;
  }

  if (b.startsWith("[") && b.endsWith("]")) return true;
  if (/\[.+\]/.test(b)) return true;
  if (/\(/.test(b) && /^[a-z]/.test(b)) return true;

  if (
    /^(line-through|no-underline|normal-case|not-italic|subpixel-antialiased|table-auto|table-fixed|border-collapse|border-separate|sr-only|not-sr-only|break-words|break-all|break-normal|overflow-auto|overflow-hidden|overflow-visible|overflow-scroll|overflow-x-auto|overflow-x-hidden|overflow-y-auto|overflow-y-hidden|overflow-y-scroll|inline-block|inline-flex|inline-grid|flow-root|list-item|outline-hidden|outline-none|bg-clip-padding|bg-gradient-to-t|bg-gradient-to-b|bg-gradient-to-l|bg-gradient-to-r|not-prose|transform-gpu)$/.test(
      bNoOpacity,
    )
  )
    return true;

  return false;
}

function looksLikeTailwindUtility(token) {
  if (!token) return false;
  if (token.startsWith(PREFIX)) return false;
  if (token.startsWith("@")) return false;
  if (token.startsWith("data-") && !token.includes(":")) return false;

  let base = stripVariants(token);
  if (!base) return false;

  // Already prefixed somewhere after stripping known variants.
  // Uses includes() instead of startsWith() to handle unknown variants
  // that aren't in VARIANT_RE (e.g. *: child variant, &: nesting).
  if (base.includes(PREFIX)) return false;

  return isBaseUtility(base);
}

// ── Token prefixing ─────────────────────────────────────────────────────────
// In Tailwind v4 with prefix(cpk), the prefix MUST come before all variants:
//   cpk:dark:hover:bg-white  ✓
//   dark:hover:cpk:bg-white  ✗ (generates no CSS)

function prefixToken(token) {
  if (!looksLikeTailwindUtility(token)) return token;
  return PREFIX + token;
}

// Detect tokens where cpk: is placed after variant(s) instead of before them.
// e.g. dark:cpk:bg-white, hover:cpk:text-blue, dark:hover:cpk:bg-red
const WRONG_PREFIX_RE =
  /^((?:[a-z][-a-z0-9]*(?:\[.*?\])?:|\[.*?\]:)+)cpk:(.+)$/;

function hasWrongPrefixPosition(token) {
  return WRONG_PREFIX_RE.test(token);
}

function fixPrefixPosition(token) {
  return token.replace(WRONG_PREFIX_RE, "cpk:$1$2");
}

// ── Oxlint rule ─────────────────────────────────────────────────────────────

const rule = {
  meta: {
    type: "suggestion",
    docs: {
      description:
        "Enforce cpk: prefix on Tailwind utility classes in className attributes",
    },
    fixable: "code",
    schema: [],
    messages: {
      missingPrefix:
        "'{{token}}' is missing the 'cpk:' prefix. Use '{{fixed}}' instead. See oxlint-rules/README.md for why.",
      wrongPrefixPosition:
        "'{{token}}' has 'cpk:' in the wrong position. The prefix must come BEFORE variants. Use '{{fixed}}' instead. See oxlint-rules/README.md for why.",
    },
  },

  create(context) {
    const sourceCode = context.sourceCode || context.getSourceCode();
    const CLASS_HELPERS = new Set(["cn", "twMerge", "cva", "clsx"]);
    const checked = new WeakSet();

    // ── String-literal checker ────────────────────────────────────────────

    function checkStringLiteral(node) {
      if (checked.has(node)) return;
      checked.add(node);

      const value = node.value;
      if (typeof value !== "string" || !value.trim()) return;

      // Skip single-word strings that look like JS values, not class names
      const trimmed = value.trim();
      if (!trimmed.includes(" ") && !trimmed.includes("\t")) {
        if (JS_VALUE_WORDS.has(trimmed)) return;
        if (
          !trimmed.includes("-") &&
          !trimmed.includes(":") &&
          !trimmed.includes("[")
        )
          return;
      }

      // Work with the raw source to get accurate positions
      const src = sourceCode.getText(node);
      const inner = src.slice(1, -1); // strip quotes
      const innerStart = node.range[0] + 1;

      const regex = /\S+/g;
      let match;
      while ((match = regex.exec(inner)) !== null) {
        const token = match[0];
        const rangeStart = innerStart + match.index;
        const rangeEnd = rangeStart + token.length;

        if (hasWrongPrefixPosition(token)) {
          const fixed = fixPrefixPosition(token);
          context.report({
            node,
            messageId: "wrongPrefixPosition",
            data: { token, fixed },
            fix(fixer) {
              return fixer.replaceTextRange([rangeStart, rangeEnd], fixed);
            },
          });
        } else if (looksLikeTailwindUtility(token)) {
          const fixed = prefixToken(token);
          context.report({
            node,
            messageId: "missingPrefix",
            data: { token, fixed },
            fix(fixer) {
              return fixer.replaceTextRange([rangeStart, rangeEnd], fixed);
            },
          });
        }
      }
    }

    // ── Template-literal quasi checker ────────────────────────────────────

    function checkQuasi(quasi) {
      if (checked.has(quasi)) return;
      checked.add(quasi);

      const raw = quasi.value.raw;
      if (!raw || !raw.trim()) return;

      // Find where the raw content starts in the source.
      // TemplateElement ranges include delimiters (` or ${ or }).
      const srcSlice = sourceCode.text.slice(quasi.range[0], quasi.range[1]);
      const rawIdx = srcSlice.indexOf(raw);
      const contentStart = quasi.range[0] + (rawIdx >= 0 ? rawIdx : 1);

      const regex = /\S+/g;
      let match;
      while ((match = regex.exec(raw)) !== null) {
        const token = match[0];
        const rangeStart = contentStart + match.index;
        const rangeEnd = rangeStart + token.length;

        if (hasWrongPrefixPosition(token)) {
          const fixed = fixPrefixPosition(token);
          context.report({
            node: quasi,
            messageId: "wrongPrefixPosition",
            data: { token, fixed },
            fix(fixer) {
              return fixer.replaceTextRange([rangeStart, rangeEnd], fixed);
            },
          });
        } else if (looksLikeTailwindUtility(token)) {
          const fixed = prefixToken(token);
          context.report({
            node: quasi,
            messageId: "missingPrefix",
            data: { token, fixed },
            fix(fixer) {
              return fixer.replaceTextRange([rangeStart, rangeEnd], fixed);
            },
          });
        }
      }
    }

    // ── Recursive expression walker ───────────────────────────────────────

    function checkExpression(node) {
      if (!node) return;

      switch (node.type) {
        case "Literal":
        case "StringLiteral": // babel parser
          if (typeof node.value === "string") checkStringLiteral(node);
          break;

        case "JSXExpressionContainer":
          checkExpression(node.expression);
          break;

        case "TemplateLiteral":
          for (const quasi of node.quasis) checkQuasi(quasi);
          for (const expr of node.expressions) checkExpression(expr);
          break;

        case "ConditionalExpression":
          checkExpression(node.consequent);
          checkExpression(node.alternate);
          break;

        case "LogicalExpression":
          // e.g. isActive && "bg-blue-500"
          checkExpression(node.left);
          checkExpression(node.right);
          break;

        case "CallExpression":
          if (isClassHelper(node.callee)) {
            for (const arg of node.arguments) checkExpression(arg);
          }
          break;

        case "ArrayExpression":
          for (const el of node.elements) {
            if (el) checkExpression(el);
          }
          break;

        case "ObjectExpression":
          // For cva variant objects: { variant: { default: "...", ... } }
          for (const prop of node.properties) {
            if (prop.value) checkExpression(prop.value);
          }
          break;

        case "SpreadElement":
          break;

        default:
          break;
      }
    }

    function isClassHelper(callee) {
      if (!callee) return false;
      if (callee.type === "Identifier") {
        return CLASS_HELPERS.has(callee.name);
      }
      // Handle e.g. module.cn()
      if (callee.type === "MemberExpression" && callee.property) {
        const name =
          callee.property.type === "Identifier"
            ? callee.property.name
            : callee.property.value;
        return CLASS_HELPERS.has(name);
      }
      return false;
    }

    // ── Visitors ──────────────────────────────────────────────────────────

    return {
      JSXAttribute(node) {
        if (
          node.name &&
          node.name.type === "JSXIdentifier" &&
          node.name.name === "className" &&
          node.value
        ) {
          checkExpression(node.value);
        }
      },

      CallExpression(node) {
        if (isClassHelper(node.callee)) {
          for (const arg of node.arguments) {
            checkExpression(arg);
          }
        }
      },
    };
  },
};

export default rule;
