/**
 * Snippet-level accessibility checks for the conform engine.
 *
 * These run on a raw JSX/TSX snippet — no design-system catalog required — and
 * surface accessible-name gaps on native interactive and media elements. They
 * are advisory only: the engine emits `flag-accessibility` suggestions and never
 * auto-rewrites code to "fix" them, because the correct label is human intent we
 * cannot invent. Library components (uppercase JSX names) are out of scope here;
 * their a11y requirements are governed per-component by catalog policy.
 */
import { parse, type ParserOptions } from "@babel/parser";
import _traverse, { type NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import type { ConformSuggestion } from "@fragments-sdk/core";

const traverse = (_traverse as unknown as { default?: typeof _traverse }).default ?? _traverse;

const PARSER_OPTIONS: ParserOptions = {
  sourceType: "module",
  errorRecovery: true,
  plugins: ["jsx", "typescript"],
};

const NAME_SOURCE_PROPS = new Set(["aria-label", "aria-labelledby", "title"]);

export function findAccessibilityGaps(source: string): ConformSuggestion[] {
  const ast = parseSafely(source);
  if (!ast) return [];

  const suggestions: ConformSuggestion[] = [];
  traverse(ast, {
    JSXElement(path) {
      const element = nativeElementName(path.node.openingElement.name);
      if (!element) return;
      if (isAriaHidden(path)) return;

      if (element === "button" || element === "a") {
        if (hasAccessibleName(path)) return;
        suggestions.push(
          suggestion(
            path,
            `<${element}> has no accessible name. Add visible text, an aria-label, or aria-labelledby so assistive tech can announce it.`
          )
        );
        return;
      }

      if (element === "img" && !hasAttribute(path, "alt")) {
        suggestions.push(
          suggestion(
            path,
            `<img> has no alt attribute. Add descriptive alt text, or alt="" if the image is decorative.`
          )
        );
      }
    },
  });
  return suggestions;
}

function hasAccessibleName(path: NodePath<t.JSXElement>): boolean {
  for (const child of path.node.children) {
    if (t.isJSXText(child) && child.value.trim().length > 0) return true;
    if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) return true;
  }
  for (const attr of openingAttributes(path)) {
    if (!t.isJSXIdentifier(attr.name)) continue;
    if (!NAME_SOURCE_PROPS.has(attr.name.name)) continue;
    if (attr.value === null) continue;
    if (t.isStringLiteral(attr.value)) {
      if (attr.value.value.trim().length > 0) return true;
      continue;
    }
    // A dynamic value (expression container) cannot be proven empty here.
    return true;
  }
  return false;
}

function isAriaHidden(path: NodePath<t.JSXElement>): boolean {
  for (const attr of openingAttributes(path)) {
    if (!t.isJSXIdentifier(attr.name) || attr.name.name !== "aria-hidden") continue;
    if (attr.value === null) return false;
    if (t.isStringLiteral(attr.value)) return attr.value.value === "true";
    if (t.isJSXExpressionContainer(attr.value)) {
      return t.isBooleanLiteral(attr.value.expression) && attr.value.expression.value === true;
    }
  }
  return false;
}

function hasAttribute(path: NodePath<t.JSXElement>, name: string): boolean {
  return openingAttributes(path).some(
    (attr) => t.isJSXIdentifier(attr.name) && attr.name.name === name
  );
}

function openingAttributes(path: NodePath<t.JSXElement>): t.JSXAttribute[] {
  return path.node.openingElement.attributes.filter((attr): attr is t.JSXAttribute =>
    t.isJSXAttribute(attr)
  );
}

function nativeElementName(name: t.JSXOpeningElement["name"]): string | null {
  if (!t.isJSXIdentifier(name)) return null;
  // Native HTML elements are lowercase; uppercase identifiers are components.
  return /^[a-z]/.test(name.name) ? name.name : null;
}

function suggestion(path: NodePath<t.JSXElement>, message: string): ConformSuggestion {
  const loc = path.node.openingElement.loc?.start;
  return {
    kind: "flag-accessibility",
    message,
    confidence: "medium",
    location: loc ? { line: loc.line, column: loc.column } : undefined,
  };
}

function parseSafely(source: string): t.File | null {
  try {
    return parse(source, PARSER_OPTIONS);
  } catch {
    return null;
  }
}
