function splitClasses(value) {
  return String(value)
    .split(/\s+/g)
    .map((s) => s.trim())
    .filter(Boolean);
}

const ARBITRARY_VALUE = /\[[^\]]+\]/;

const SPACING_PREFIXES = [
  { re: /^p-(.+)$/, kind: 'p' },
  { re: /^px-(.+)$/, kind: 'px' },
  { re: /^py-(.+)$/, kind: 'py' },
  { re: /^pt-(.+)$/, kind: 'pt' },
  { re: /^pr-(.+)$/, kind: 'pr' },
  { re: /^pb-(.+)$/, kind: 'pb' },
  { re: /^pl-(.+)$/, kind: 'pl' },
  { re: /^m-(.+)$/, kind: 'm' },
  { re: /^mx-(.+)$/, kind: 'mx' },
  { re: /^my-(.+)$/, kind: 'my' },
  { re: /^mt-(.+)$/, kind: 'mt' },
  { re: /^mr-(.+)$/, kind: 'mr' },
  { re: /^mb-(.+)$/, kind: 'mb' },
  { re: /^ml-(.+)$/, kind: 'ml' },
  { re: /^gap-(.+)$/, kind: 'gap' },
  { re: /^gap-x-(.+)$/, kind: 'gap-x' },
  { re: /^gap-y-(.+)$/, kind: 'gap-y' },
];

const COLORISH_PREFIXES = [
  /^text-/,
  /^bg-/,
  /^from-/,
  /^to-/,
  /^via-/,
  /^border(-[a-z0-9_-]+)?$/,
  /^ring-/,
  /^outline-/,
  /^fill-/,
  /^stroke-/,
  /^shadow-/,
  /^decoration-/,
];

const RADIUS = /^rounded(?:-[a-z0-9_.]+)?$/;

const HEX_IN_VALUE = /#[0-9a-fA-F]{3,8}\b/;
const RAW_FN_COLOR = /\b(?:rgb|rgba|hsl|hsla)\s*\(/i;
const RAW_GRADIENT = /\b(?:linear|radial|conic)-gradient\s*\(/i;
const PX_LITERAL = /^-?\d+(?:\.\d+)?px$/;

const STYLE_VALUE_ALLOW = new Set([
  'transparent', 'currentColor', 'inherit', 'unset', 'initial', 'none', 'auto', '0', '0px',
]);

const SPACING_STYLE_PROPS = new Set([
  'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'paddingBlock', 'paddingInline',
  'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'marginBlock', 'marginInline',
  'gap', 'rowGap', 'columnGap',
]);

const RADIUS_STYLE_PROPS = new Set([
  'borderRadius', 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius',
]);

const COLOR_STYLE_PROPS = new Set([
  'color', 'background', 'backgroundColor', 'backgroundImage',
  'border', 'borderColor', 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor',
  'outline', 'outlineColor', 'fill', 'stroke', 'boxShadow', 'textDecorationColor', 'caretColor',
]);

/** @param {import('estree').Node | null | undefined} node */
function staticStyleValue(node) {
  if (!node) return null;
  if (node.type === 'Literal') return node.value;
  if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
    return node.quasis.map((q) => q.cooked).join('');
  }
  return null;
}

/** @param {import('estree').Property} prop */
function styleKeyName(prop) {
  if (prop.key?.type === 'Identifier') return prop.key.name;
  if (prop.key?.type === 'Literal') return String(prop.key.value);
  return null;
}

/** @param {import('eslint').Rule.RuleContext} context */
function checkInlineStyle(node, context) {
  const styleAttr = node.attributes?.find(
    (a) => a.type === 'JSXAttribute' && a.name?.name === 'style',
  );
  if (!styleAttr) return;
  if (styleAttr.value?.type !== 'JSXExpressionContainer') return;
  const obj = styleAttr.value.expression;
  if (!obj || obj.type !== 'ObjectExpression') return;

  for (const prop of obj.properties) {
    if (prop.type !== 'Property') continue;
    const key = styleKeyName(prop);
    if (!key) continue;
    const val = staticStyleValue(prop.value);
    if (val === null) continue;
    const strVal = String(val).trim();
    if (val !== 0 && (strVal === '' || STYLE_VALUE_ALLOW.has(strVal))) continue;
    if (strVal.includes('var(')) continue;

    if (RAW_GRADIENT.test(strVal)) {
      context.report({ node: prop, messageId: 'noRawGradientInStyle', data: { prop: key, value: strVal } });
      continue;
    }
    if (HEX_IN_VALUE.test(strVal) || RAW_FN_COLOR.test(strVal)) {
      context.report({ node: prop, messageId: 'noRawColorInStyle', data: { prop: key, value: strVal } });
      continue;
    }
    if (SPACING_STYLE_PROPS.has(key)) {
      const isPx = typeof val === 'number' ? val !== 0 : PX_LITERAL.test(strVal);
      if (isPx) {
        context.report({ node: prop, messageId: 'arbitrarySpacingInStyle', data: { prop: key, value: strVal } });
      }
      continue;
    }
    if (RADIUS_STYLE_PROPS.has(key)) {
      const isPx = typeof val === 'number' ? val !== 0 : PX_LITERAL.test(strVal);
      if (isPx) {
        context.report({ node: prop, messageId: 'arbitraryRadiusInStyle', data: { prop: key, value: strVal } });
      }
      continue;
    }
    if (COLOR_STYLE_PROPS.has(key) && /^(white|black)$/i.test(strVal)) {
      context.report({ node: prop, messageId: 'noRawColorInStyle', data: { prop: key, value: strVal } });
    }
  }
}

/**
 * @param {import('estree').ImportDeclaration[]} importDeclarations
 * @param {Set<string>} importSources
 */
function getImportedLocalNamesForTfds(importDeclarations, importSources) {
  /** @type {Map<string, string>} */
  const map = new Map();
  for (const decl of importDeclarations) {
    if (decl.type !== 'ImportDeclaration') continue;
    const src = decl.source?.value;
    if (typeof src !== 'string' || !importSources.has(src)) continue;
    for (const s of decl.specifiers || []) {
      if (s.type === 'ImportSpecifier' && s.imported?.type === 'Identifier') {
        map.set(s.local.name, src);
      } else if (s.type === 'ImportDefaultSpecifier') {
        map.set(s.local.name, src);
      }
    }
  }
  return map;
}

/** @param {import('estree').JSXOpeningElement} node */
function getJsxName(node) {
  if (node.name?.type === 'JSXIdentifier') return node.name.name;
  if (node.name?.type === 'JSXMemberExpression') {
    const obj = getJsxName({ name: node.name.object });
    const prop = node.name.property?.name;
    return prop ? `${obj}.${prop}` : obj;
  }
  return null;
}

/** @param {unknown} options */
function resolveRuleOptions(options) {
  const opts = options && typeof options === 'object' ? options : {};
  const importSources = new Set(
    Array.isArray(opts.importSources) && opts.importSources.length
      ? opts.importSources
      : ['@tfdesign/b-end'],
  );
  const allowedSpacingTokens = new Set(
    Array.isArray(opts.allowedSpacingTokens) ? opts.allowedSpacingTokens : [],
  );
  const lockedLocalNames = new Set(
    Array.isArray(opts.lockedLocalNames) ? opts.lockedLocalNames : [],
  );
  return { importSources, allowedSpacingTokens, lockedLocalNames };
}

export default {
  meta: {
    type: 'problem',
    docs: {
      description:
        'TFDS Tailwind contract: lock layout templates; constrain spacing utilities to TOKEN_SPACING; ban arbitrary Tailwind values.',
    },
    schema: [
      {
        type: 'object',
        properties: {
          importSources: { type: 'array', items: { type: 'string' } },
          allowedSpacingTokens: { type: 'array', items: { type: 'string' } },
          lockedLocalNames: { type: 'array', items: { type: 'string' } },
        },
        additionalProperties: false,
      },
    ],
    messages: {
      noArbitraryTailwindValue:
        '禁止使用任意值 Tailwind class（包含 `[...]`），请改用 TFDS token/组件 props：{{className}}',
      spacingNotOnAllowlist:
        '间距类必须使用 TFDS spacing token 表允许的刻度：{{kind}}-{{token}}（当前不允许：{{className}}）',
      colorishDisallowed:
        '禁止在业务页面直接使用颜色/阴影/描边等 Tailwind 原子类（请改用 TFDS 组件 props 或语义组件）：{{className}}',
      radiusDisallowed:
        '禁止直接使用 rounded-*（圆角必须由 TFDS 组件/token 体系承载）：{{className}}',
      lockedTemplateNoSpacing:
        '页面模板组件（{{component}}）不允许外层再用间距类二次排版：{{className}}',
      lockedTemplateNoClassName:
        '页面模板组件（{{component}}）不允许传入 className（请移除外层样式干预，保持模板预设间距）',
      noRawColorInStyle:
        'inline style 禁止硬编码颜色（{{prop}}: {{value}}）：请改用 TFDS 颜色 token，如 var(--color-foreground) / var(--color-surface) / var(--color-border-default)',
      noRawGradientInStyle:
        'inline style 禁止手写渐变（{{prop}}: {{value}}）：请改用 var(--gradient-ai-fill-*) / var(--gradient-dark) 等 token',
      arbitrarySpacingInStyle:
        'inline style 间距禁止用任意 px/数字（{{prop}}: {{value}}）：请改用 var(--spacing-*)（如 8px→var(--spacing-2)，24px→var(--spacing-6)）',
      arbitraryRadiusInStyle:
        'inline style 圆角禁止用任意 px/数字（{{prop}}: {{value}}）：请改用 var(--radius-*)（如 12px→var(--radius-lg)）',
    },
  },
  create(context) {
    const { importSources, allowedSpacingTokens, lockedLocalNames } = resolveRuleOptions(context.options?.[0]);

    return {
      JSXOpeningElement(node) {
        checkInlineStyle(node, context);

        const program = context.getSourceCode().ast;
        const importDecls = program.body.filter((n) => n.type === 'ImportDeclaration');
        const tfdsImports = getImportedLocalNamesForTfds(importDecls, importSources);
        const tag = getJsxName(node);
        if (!tag) return;

        const isTfdsImported = tfdsImports.has(tag);
        const locked = lockedLocalNames.has(tag) && isTfdsImported;

        const classAttr =
          node.attributes?.find((a) => a.type === 'JSXAttribute' && a.name?.name === 'className') || null;

        if (locked && classAttr) {
          context.report({ node: classAttr, messageId: 'lockedTemplateNoClassName', data: { component: tag } });
          return;
        }

        if (!classAttr || classAttr.value?.type !== 'Literal' || typeof classAttr.value.value !== 'string') {
          return;
        }

        const cls = classAttr.value.value;
        const parts = splitClasses(cls);

        for (const p of parts) {
          if (ARBITRARY_VALUE.test(p)) {
            context.report({ node: classAttr.value, messageId: 'noArbitraryTailwindValue', data: { className: p } });
            continue;
          }

          if (RADIUS.test(p)) {
            context.report({ node: classAttr.value, messageId: 'radiusDisallowed', data: { className: p } });
            continue;
          }

          if (COLORISH_PREFIXES.some((re) => re.test(p))) {
            context.report({ node: classAttr.value, messageId: 'colorishDisallowed', data: { className: p } });
            continue;
          }

          for (const { re, kind } of SPACING_PREFIXES) {
            const m = p.match(re);
            if (!m) continue;
            const token = m[1];

            if (locked) {
              context.report({
                node: classAttr.value,
                messageId: 'lockedTemplateNoSpacing',
                data: { component: tag, className: p },
              });
              break;
            }

            if (allowedSpacingTokens.size > 0 && !allowedSpacingTokens.has(token)) {
              context.report({
                node: classAttr.value,
                messageId: 'spacingNotOnAllowlist',
                data: { kind, token, className: p },
              });
            }
            break;
          }
        }
      },
    };
  },
};
