// Per-field mapping helpers for the React → UCF adapter. Each helper maps one
// slice of UCF; exported individually so tests can drive them in isolation.

import ts from "typescript";
import type { FactLocation } from "@fragments-sdk/core";

import type {
  ControlledFlag,
  DefinitionKind,
  EventFact,
  ImportRecord,
  PropFact,
  PropSchemaConfidence,
  RootElementFact,
  SlotFact,
  StyleSystem,
} from "./types.js";

const MAX_ROOT_BRANCHES = 4;
const MAX_CLASSNAMES = 64;

const REACT_HOOK_STATE = new Set(["useState", "useReducer"]);
const REACT_HOOK_EFFECT = new Set(["useEffect", "useLayoutEffect", "useInsertionEffect"]);

const WRAPPER_NAMES = new Set(["forwardRef", "memo", "observer", "styled"]);

// ---------------------------------------------------------------------------
// definitionKind + wrappedBy
// ---------------------------------------------------------------------------

export interface DefinitionShape {
  kind: DefinitionKind;
  wrappedBy: string[];
  /** Inner function/class node — used by other mappers. */
  inner: ts.Node | null;
}

export function mapDefinitionKind(declaration: ts.Node): DefinitionShape {
  if (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) {
    return { kind: "class", wrappedBy: [], inner: declaration };
  }

  if (ts.isFunctionDeclaration(declaration)) {
    return { kind: "function", wrappedBy: [], inner: declaration };
  }

  if (ts.isArrowFunction(declaration) || ts.isFunctionExpression(declaration)) {
    return { kind: "function", wrappedBy: [], inner: declaration };
  }

  if (ts.isCallExpression(declaration)) {
    const wrappedBy: string[] = [];
    let cursor: ts.Node = declaration;
    while (ts.isCallExpression(cursor)) {
      const name = getCalleeName(cursor.expression);
      if (name && WRAPPER_NAMES.has(name)) {
        wrappedBy.push(name);
        const first = cursor.arguments[0];
        if (!first) break;
        cursor = first;
      } else {
        break;
      }
    }
    if (wrappedBy.length > 0) {
      return { kind: "wrapper", wrappedBy, inner: cursor };
    }
  }

  return { kind: "function", wrappedBy: [], inner: null };
}

function getCalleeName(expr: ts.Expression): string | null {
  if (ts.isIdentifier(expr)) return expr.text;
  if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
  return null;
}

// ---------------------------------------------------------------------------
// imports
// ---------------------------------------------------------------------------

export function mapImports(sourceFile: ts.SourceFile): ImportRecord[] {
  const records: ImportRecord[] = [];

  for (const stmt of sourceFile.statements) {
    if (!ts.isImportDeclaration(stmt)) continue;
    if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;

    const moduleSpecifier = stmt.moduleSpecifier.text;
    const importedNames: Array<{ imported: string; local: string }> = [];

    if (stmt.importClause) {
      // Default import: import X from '...'
      if (stmt.importClause.name) {
        importedNames.push({
          imported: "default",
          local: stmt.importClause.name.text,
        });
      }

      const bindings = stmt.importClause.namedBindings;
      if (bindings) {
        if (ts.isNamespaceImport(bindings)) {
          importedNames.push({
            imported: "*",
            local: bindings.name.text,
          });
        } else if (ts.isNamedImports(bindings)) {
          for (const element of bindings.elements) {
            importedNames.push({
              imported: element.propertyName?.text ?? element.name.text,
              local: element.name.text,
            });
          }
        }
      }
    }

    records.push({
      moduleSpecifier,
      resolvedPackage: resolvePackageName(moduleSpecifier),
      importedNames,
    });
  }

  return records;
}

function resolvePackageName(spec: string): string | undefined {
  if (spec.startsWith(".") || spec.startsWith("/")) return undefined;
  if (spec.startsWith("@")) {
    const parts = spec.split("/");
    return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
  }
  return spec.split("/")[0];
}

// ---------------------------------------------------------------------------
// exports
// ---------------------------------------------------------------------------

export interface FileExportSummary {
  exportNames: string[];
  exportRecords: FileExportRecord[];
  exportedFromBarrel: boolean;
}

export interface FileExportRecord {
  exportedAs: string;
  localName: string;
  isDefault: boolean;
  fromBarrel: boolean;
}

export function mapFileExports(sourceFile: ts.SourceFile): FileExportSummary {
  const seen = new Set<string>();
  const exportNames: string[] = [];
  const exportRecords: FileExportRecord[] = [];
  const baseName = sourceFile.fileName.split("/").pop() ?? "";
  const exportedFromBarrel = /^index\.(t|j)sx?$/.test(baseName);

  function add(exportedAs: string, localName = exportedAs): void {
    const key = `${exportedAs}:${localName}`;
    if (seen.has(key)) return;
    seen.add(key);
    if (!exportNames.includes(exportedAs)) exportNames.push(exportedAs);
    exportRecords.push({
      exportedAs,
      localName,
      isDefault: exportedAs === "default",
      fromBarrel: exportedFromBarrel,
    });
  }

  for (const stmt of sourceFile.statements) {
    if (ts.isVariableStatement(stmt) && hasExportModifier(stmt)) {
      for (const decl of stmt.declarationList.declarations) {
        if (ts.isIdentifier(decl.name)) add(decl.name.text);
      }
    } else if (ts.isFunctionDeclaration(stmt) && hasExportModifier(stmt) && stmt.name) {
      add(hasDefaultModifier(stmt) ? "default" : stmt.name.text, stmt.name.text);
    } else if (ts.isClassDeclaration(stmt) && hasExportModifier(stmt) && stmt.name) {
      add(hasDefaultModifier(stmt) ? "default" : stmt.name.text, stmt.name.text);
    } else if (ts.isExportAssignment(stmt)) {
      add("default", exportAssignmentLocalName(stmt));
    } else if (ts.isExportDeclaration(stmt) && stmt.exportClause) {
      if (ts.isNamedExports(stmt.exportClause)) {
        for (const element of stmt.exportClause.elements) {
          add(element.name.text, element.propertyName?.text ?? element.name.text);
        }
      }
    }
  }

  return { exportNames, exportRecords, exportedFromBarrel };
}

function hasExportModifier(node: ts.Node): boolean {
  const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
  return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
}

function hasDefaultModifier(node: ts.Node): boolean {
  const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
  return modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
}

function exportAssignmentLocalName(stmt: ts.ExportAssignment): string {
  return ts.isIdentifier(stmt.expression) ? stmt.expression.text : "default";
}

// ---------------------------------------------------------------------------
// rootElements + truncation
// ---------------------------------------------------------------------------

export interface RootElementsResult {
  rootElements: RootElementFact[];
  rootElementsTruncated: boolean;
}

export function mapRootElements(inner: ts.Node | null): RootElementsResult {
  if (!inner) return { rootElements: [], rootElementsTruncated: false };

  const branches: BranchAccumulator = [];
  collectReturnedJsx(inner, branches, undefined);

  let truncated = false;
  if (branches.length > MAX_ROOT_BRANCHES) {
    truncated = true;
    branches.length = MAX_ROOT_BRANCHES;
  }

  const rootElements = branches.flatMap(({ jsx, guard }) =>
    effectiveRootNodes(jsx).map((root) => toRootElementFact(root, guard))
  );
  return { rootElements, rootElementsTruncated: truncated };
}

type JsxRootLike = ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment;

type BranchAccumulator = Array<{
  jsx: JsxRootLike;
  guard?: string;
}>;

function collectReturnedJsx(
  node: ts.Node,
  acc: BranchAccumulator,
  guard: string | undefined
): void {
  if (!node) return;

  if (ts.isReturnStatement(node)) {
    if (node.expression) collectFromExpression(node.expression, acc, guard);
    return;
  }

  // For arrow functions with concise body
  if (ts.isArrowFunction(node) && !ts.isBlock(node.body)) {
    collectFromExpression(node.body, acc, guard);
    return;
  }

  if (ts.isIfStatement(node)) {
    const thenGuard = node.expression.getText();
    walk(node.thenStatement, acc, combineGuards(guard, thenGuard));
    if (node.elseStatement) {
      walk(node.elseStatement, acc, combineGuards(guard, `!(${thenGuard})`));
    }
    return;
  }

  ts.forEachChild(node, (child) => walk(child, acc, guard));
}

function walk(node: ts.Node, acc: BranchAccumulator, guard: string | undefined): void {
  if (acc.length >= MAX_ROOT_BRANCHES + 1) return;
  collectReturnedJsx(node, acc, guard);
}

function collectFromExpression(
  expr: ts.Expression,
  acc: BranchAccumulator,
  guard: string | undefined
): void {
  if (acc.length >= MAX_ROOT_BRANCHES + 1) return;

  const stripped = stripParens(expr);

  if (ts.isConditionalExpression(stripped)) {
    const condText = stripped.condition.getText();
    collectFromExpression(stripped.whenTrue, acc, combineGuards(guard, condText));
    collectFromExpression(stripped.whenFalse, acc, combineGuards(guard, `!(${condText})`));
    return;
  }

  if (
    ts.isBinaryExpression(stripped) &&
    stripped.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
  ) {
    const condText = stripped.left.getText();
    collectFromExpression(stripped.right, acc, combineGuards(guard, condText));
    return;
  }

  if (ts.isJsxElement(stripped) || ts.isJsxSelfClosingElement(stripped)) {
    acc.push({ jsx: stripped, guard });
    return;
  }

  if (ts.isJsxFragment(stripped)) {
    acc.push({ jsx: stripped, guard });
    return;
  }
}

function stripParens(expr: ts.Expression): ts.Expression {
  while (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr)) {
    expr = (expr as ts.ParenthesizedExpression | ts.AsExpression).expression;
  }
  return expr;
}

function combineGuards(prev: string | undefined, next: string): string {
  return prev ? `${prev} && ${next}` : next;
}

function effectiveRootNodes(jsx: JsxRootLike): Array<ts.JsxOpeningLikeElement | ts.JsxFragment> {
  return effectiveRootsFromJsx(jsx);
}

function effectiveRootsFromJsx(jsx: JsxRootLike): Array<ts.JsxOpeningLikeElement | ts.JsxFragment> {
  if (ts.isJsxSelfClosingElement(jsx)) return [jsx];
  if (ts.isJsxFragment(jsx)) return effectiveRootsFromFragment(jsx);

  const opening = jsx.openingElement;
  const children = effectiveRootsFromChildren(jsx.children);
  if (children.length > 0 && shouldPreferChildRoots(opening)) {
    const preferred = children.filter(isRootCandidate);
    return preferred.length > 0 ? preferred : children;
  }
  return [opening];
}

function effectiveRootsFromFragment(
  jsx: ts.JsxFragment
): Array<ts.JsxOpeningLikeElement | ts.JsxFragment> {
  const children = effectiveRootsFromChildren(jsx.children);
  return children.length > 0 ? children : [jsx];
}

function effectiveRootsFromChildren(
  children: ts.NodeArray<ts.JsxChild>
): Array<ts.JsxOpeningLikeElement | ts.JsxFragment> {
  const roots: Array<ts.JsxOpeningLikeElement | ts.JsxFragment> = [];
  for (const child of children) {
    if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
      roots.push(...effectiveRootsFromJsx(child));
    }
  }
  return roots;
}

function shouldPreferChildRoots(opening: ts.JsxOpeningElement): boolean {
  const tag = opening.tagName.getText();
  if (tag === "Fragment") return true;
  if (tag.endsWith(".Provider")) return true;
  if (tag === "Field.Root" || tag.endsWith("Field.Root")) return true;
  return /^(div|span|label|section|fieldset)$/i.test(tag);
}

function isRootCandidate(jsx: ts.JsxOpeningLikeElement | ts.JsxFragment) {
  if (ts.isJsxFragment(jsx)) return false;
  const tag = jsx.tagName.getText();
  if (/^[a-z]/.test(tag)) {
    return /^(button|input|textarea|select|dialog|details|progress|hr|table|nav|form)$/i.test(tag);
  }
  return !tag.includes(".") || tag.endsWith(".Root");
}

function toRootElementFact(
  jsx: ts.JsxOpeningLikeElement | ts.JsxFragment,
  guard: string | undefined
): RootElementFact {
  if (ts.isJsxFragment(jsx)) {
    return {
      tag: "Fragment",
      attrs: {},
      loc: locationForNode(jsx),
      conditionalGuard: guard,
    };
  }

  const tagName = jsx.tagName.getText();
  const isHtml = /^[a-z]/.test(tagName);
  const tag = isHtml ? tagName : `Custom:${tagName}`;
  const attrs: Record<string, string | true> = {};
  let inputType: string | undefined;

  for (const attr of jsx.attributes.properties) {
    if (!ts.isJsxAttribute(attr)) continue;
    const name = attr.name.getText();
    const value = readAttrValue(attr);
    if (value !== undefined) attrs[name] = value;
    if (name === "type" && tag === "input" && typeof value === "string") {
      inputType = value;
    }
  }

  const fact: RootElementFact = { tag, attrs, loc: locationForNode(jsx) };
  if (inputType) fact.inputType = inputType;
  if (guard) fact.conditionalGuard = guard;
  return fact;
}

function readAttrValue(attr: ts.JsxAttribute): string | true | undefined {
  if (!attr.initializer) return true;
  if (ts.isStringLiteral(attr.initializer)) return attr.initializer.text;
  if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
    const inner = attr.initializer.expression;
    if (ts.isStringLiteral(inner) || ts.isNoSubstitutionTemplateLiteral(inner)) {
      return inner.text;
    }
  }
  return undefined;
}

// ---------------------------------------------------------------------------
// aria
// ---------------------------------------------------------------------------

export function mapAria(rootElements: ReadonlyArray<RootElementFact>): {
  ariaRoles: string[];
  ariaAttributes: Record<string, string | true>;
} {
  const roles = new Set<string>();
  const attributes: Record<string, string | true> = {};

  for (const root of rootElements) {
    for (const [key, value] of Object.entries(root.attrs)) {
      if (key === "role" && typeof value === "string") roles.add(value);
      if (key.startsWith("aria-")) attributes[key] = value;
    }
  }

  return { ariaRoles: [...roles], ariaAttributes: attributes };
}

// ---------------------------------------------------------------------------
// props
// ---------------------------------------------------------------------------

export function mapProps(
  inner: ts.Node | null,
  checker: ts.TypeChecker,
  declaration?: ts.Node | null
): PropFact[] {
  if (!inner) return [];

  const propsType = resolvePropsType(inner, checker, declaration);
  if (!propsType) return [];

  const defaults = collectDestructuredDefaults(inner);
  const schemaConfidence = propSchemaConfidence(propsType, checker, inner);
  const facts: PropFact[] = [];
  const ownerSourceFile = inner.getSourceFile();

  for (const symbol of propSymbolsForType(propsType, checker)) {
    const decl = symbol.getDeclarations()?.[0];
    if (decl && !isUserDeclaration(decl, ownerSourceFile)) continue;

    const declNode = decl ?? inner;
    const propType = checker.getTypeOfSymbolAtLocation(symbol, declNode);

    const optional = (symbol.flags & ts.SymbolFlags.Optional) !== 0;
    const typeText = checker.typeToString(propType, declNode);
    const fact: PropFact = {
      name: symbol.getName(),
      typeText,
      loc: locationForNode(propLocationNode(declNode)),
      optional,
      hasDefault: defaults.has(symbol.getName()),
      isUnion: propType.isUnion(),
      schemaConfidence,
    };

    if (fact.hasDefault) {
      fact.defaultValueText = defaults.get(symbol.getName());
    }

    if (propType.isUnion()) {
      fact.unionMembers = unionMembers(propType, declNode, checker);
    }

    const jsdocText = getJsDocText(symbol);
    if (jsdocText) fact.jsdoc = jsdocText;

    facts.push(fact);
  }

  return facts;
}

function propSchemaConfidence(
  type: ts.Type,
  checker: ts.TypeChecker,
  location: ts.Node
): PropSchemaConfidence {
  if (isUnknownLikeType(type) || typeHasIndexSignature(type, checker)) return "partial";
  if (hasPolymorphicProp(type, checker)) return "partial";
  if (typeContainsWeakSurface(type, checker, location)) return "partial";
  return "exact";
}

function typeContainsWeakSurface(
  type: ts.Type,
  checker: ts.TypeChecker,
  location: ts.Node,
  seen = new Set<ts.Type>()
): boolean {
  if (seen.has(type)) return false;
  seen.add(type);

  if (isUnknownLikeType(type) || typeHasIndexSignature(type, checker)) return true;
  if (type.isIntersection()) {
    return type.types.some((part) => typeContainsWeakSurface(part, checker, location, seen));
  }
  if (type.isUnion()) {
    return type.types.some((part) => typeContainsWeakSurface(part, checker, location, seen));
  }

  const alias = type.aliasSymbol?.getName();
  if (alias === "Record") return true;

  const symbol = type.getSymbol() ?? type.aliasSymbol;
  const declarations = symbol?.getDeclarations() ?? [];
  if (
    declarations.some((decl) => {
      if (decl.getSourceFile().fileName.includes("node_modules")) return true;
      return declarationHasWeakSurface(decl);
    })
  ) {
    return true;
  }

  return checker
    .getPropertiesOfType(type)
    .some((prop) => typeContainsWeakProp(prop, checker, location, seen));
}

function typeContainsWeakProp(
  prop: ts.Symbol,
  checker: ts.TypeChecker,
  location: ts.Node,
  seen: Set<ts.Type>
): boolean {
  const declarations = prop.getDeclarations() ?? [];
  if (declarations.some((decl) => decl.getSourceFile().fileName.includes("node_modules"))) {
    return true;
  }
  const declaration = declarations[0] ?? location;
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
  return isUnknownLikeType(propType) || typeReferencesTypeParameter(propType, checker, seen);
}

function hasPolymorphicProp(type: ts.Type, checker: ts.TypeChecker): boolean {
  const names = new Set(checker.getPropertiesOfType(type).map((prop) => prop.getName()));
  return names.has("as") || names.has("asChild");
}

function declarationHasWeakSurface(declaration: ts.Declaration): boolean {
  if (ts.isInterfaceDeclaration(declaration) || ts.isTypeLiteralNode(declaration)) {
    return declaration.members.some((member) => ts.isIndexSignatureDeclaration(member));
  }
  if (ts.isTypeAliasDeclaration(declaration)) {
    return typeNodeHasWeakSurface(declaration.type);
  }
  return false;
}

function typeNodeHasWeakSurface(node: ts.TypeNode): boolean {
  if (ts.isTypeLiteralNode(node)) {
    return node.members.some((member) => ts.isIndexSignatureDeclaration(member));
  }
  if (ts.isTypeReferenceNode(node)) {
    return node.typeName.getText() === "Record";
  }
  if (ts.isIntersectionTypeNode(node) || ts.isUnionTypeNode(node)) {
    return node.types.some(typeNodeHasWeakSurface);
  }
  return false;
}

function typeReferencesTypeParameter(
  type: ts.Type,
  checker: ts.TypeChecker,
  seen: Set<ts.Type>
): boolean {
  if (seen.has(type)) return false;
  seen.add(type);
  if (isTypeParameter(type)) return true;
  if (type.isUnionOrIntersection()) {
    return type.types.some((part) => typeReferencesTypeParameter(part, checker, seen));
  }
  return checker.getPropertiesOfType(type).some((prop) => {
    const declaration = prop.getDeclarations()?.[0];
    if (!declaration) return false;
    const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
    return typeReferencesTypeParameter(propType, checker, seen);
  });
}

function typeHasIndexSignature(type: ts.Type, checker: ts.TypeChecker): boolean {
  return (
    checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined ||
    checker.getIndexInfoOfType(type, ts.IndexKind.Number) !== undefined
  );
}

function isUnknownLikeType(type: ts.Type): boolean {
  return Boolean(
    type.flags &
    (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never | ts.TypeFlags.TypeParameter)
  );
}

function isTypeParameter(type: ts.Type): boolean {
  return Boolean(type.flags & ts.TypeFlags.TypeParameter);
}

function propSymbolsForType(type: ts.Type, checker: ts.TypeChecker): ts.Symbol[] {
  const byName = new Map<string, ts.Symbol>();
  const add = (symbol: ts.Symbol) => {
    if (!byName.has(symbol.getName())) byName.set(symbol.getName(), symbol);
  };

  for (const symbol of checker.getPropertiesOfType(type)) add(symbol);

  if (type.isIntersection()) {
    for (const part of type.types) {
      for (const symbol of propSymbolsForType(part, checker)) add(symbol);
    }
  }

  const interfaceType = type as ts.InterfaceType;
  for (const base of interfaceType.getBaseTypes?.() ?? []) {
    for (const symbol of propSymbolsForType(base, checker)) add(symbol);
  }

  const apparent = checker.getApparentType(type);
  if (apparent !== type) {
    for (const symbol of checker.getPropertiesOfType(apparent)) add(symbol);
  }

  return [...byName.values()];
}

function propLocationNode(node: ts.Node): ts.Node {
  if (ts.isPropertySignature(node) || ts.isPropertyDeclaration(node)) {
    return node.name;
  }
  if (ts.isBindingElement(node)) {
    return node.name;
  }
  if (ts.isParameter(node)) {
    return node.name;
  }
  return node;
}

function locationForNode(node: ts.Node): FactLocation {
  const sourceFile = node.getSourceFile();
  const start = node.getStart(sourceFile);
  const end = node.getEnd();
  const startPos = sourceFile.getLineAndCharacterOfPosition(start);
  const endPos = sourceFile.getLineAndCharacterOfPosition(end);
  return {
    file: sourceFile.fileName,
    line: startPos.line + 1,
    column: startPos.character,
    endLine: endPos.line + 1,
    endColumn: endPos.character,
  };
}

function resolvePropsType(
  node: ts.Node,
  checker: ts.TypeChecker,
  declaration?: ts.Node | null
): ts.Type | null {
  const declaredComponentType = declaration
    ? resolvePropsTypeFromComponentDeclaration(declaration, checker)
    : null;
  if (declaredComponentType) return declaredComponentType;

  if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
    const param = node.parameters[0];
    if (!param) return null;
    return checker.getTypeAtLocation(param);
  }
  if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
    // class Foo extends Component<Props>
    const heritage = node.heritageClauses?.[0]?.types?.[0];
    if (heritage?.typeArguments?.[0]) {
      return checker.getTypeFromTypeNode(heritage.typeArguments[0]);
    }
  }
  return null;
}

function resolvePropsTypeFromComponentDeclaration(
  declaration: ts.Node,
  checker: ts.TypeChecker
): ts.Type | null {
  if (!ts.isVariableDeclaration(declaration) || !ts.isIdentifier(declaration.name)) return null;

  const propsTypeArg = propsTypeArgumentFromComponentAnnotation(declaration.type, checker);
  if (propsTypeArg) return propsTypeArg;

  const explicitType = declaration.type ? checker.getTypeFromTypeNode(declaration.type) : null;
  const componentType = explicitType ?? checker.getTypeAtLocation(declaration.name);
  return propsTypeFromComponentType(componentType, declaration, checker);
}

function propsTypeArgumentFromComponentAnnotation(
  typeNode: ts.TypeNode | undefined,
  checker: ts.TypeChecker
): ts.Type | null {
  if (!typeNode || !ts.isTypeReferenceNode(typeNode)) return null;
  const name = typeNode.typeName.getText();
  if (
    name !== "FC" &&
    name !== "React.FC" &&
    name !== "FunctionComponent" &&
    name !== "React.FunctionComponent"
  ) {
    return null;
  }
  const propsArg = typeNode.typeArguments?.[0];
  return propsArg ? checker.getTypeFromTypeNode(propsArg) : null;
}

function propsTypeFromComponentType(
  componentType: ts.Type,
  location: ts.Node,
  checker: ts.TypeChecker
): ts.Type | null {
  for (const signature of componentType.getCallSignatures()) {
    const param = signature.getParameters()[0];
    if (!param) continue;
    return checker.getTypeOfSymbolAtLocation(param, location);
  }

  const propsProperty = componentType.getProperty("props");
  if (propsProperty) {
    return checker.getTypeOfSymbolAtLocation(propsProperty, location);
  }

  const apparent = checker.getApparentType(componentType);
  if (apparent !== componentType) return propsTypeFromComponentType(apparent, location, checker);
  return null;
}

function collectDestructuredDefaults(node: ts.Node): Map<string, string> {
  const defaults = new Map<string, string>();
  if (
    !ts.isFunctionDeclaration(node) &&
    !ts.isArrowFunction(node) &&
    !ts.isFunctionExpression(node)
  ) {
    return defaults;
  }
  const param = node.parameters[0];
  if (!param || !ts.isObjectBindingPattern(param.name)) return defaults;

  for (const element of param.name.elements) {
    if (!ts.isIdentifier(element.name)) continue;
    if (!element.initializer) continue;
    const propName = element.propertyName ? element.propertyName.getText() : element.name.text;
    defaults.set(propName, element.initializer.getText());
  }
  return defaults;
}

/**
 * Filters out props inherited from React/DOM library declarations (HTMLAttributes,
 * ARIAMixin, etc.). Without this guard, `forwardRef<HTMLButtonElement, Props>`
 * leaks the full HTMLButton DOM surface into PropFact[].
 */
function isUserDeclaration(decl: ts.Declaration, owner: ts.SourceFile): boolean {
  const declSource = decl.getSourceFile();
  if (declSource === owner) return true;
  return !declSource.fileName.includes("node_modules");
}

function unionMembers(type: ts.Type, location: ts.Node, checker: ts.TypeChecker): string[] {
  if (!type.isUnion()) return [];
  const members = type.types.map((t) => checker.typeToString(t, location));
  return [...new Set(members)].sort();
}

function getJsDocText(symbol: ts.Symbol): string | undefined {
  const tags = symbol.getDocumentationComment(undefined);
  if (!tags || tags.length === 0) return undefined;
  return ts.displayPartsToString(tags).trim() || undefined;
}

// ---------------------------------------------------------------------------
// behavior hints (state, effects, isControlled)
// ---------------------------------------------------------------------------

export interface BehaviorHints {
  hasInternalState: boolean;
  hasEffects: boolean;
  isControlled: ControlledFlag;
}

export function mapBehaviorHints(
  inner: ts.Node | null,
  props: ReadonlyArray<PropFact>
): BehaviorHints {
  let hasInternalState = false;
  let hasEffects = false;

  if (inner) {
    visit(inner, (node) => {
      if (hasInternalState && hasEffects) return false;
      if (ts.isCallExpression(node)) {
        const name = getCalleeName(node.expression);
        if (name && REACT_HOOK_STATE.has(name)) hasInternalState = true;
        if (name && REACT_HOOK_EFFECT.has(name)) hasEffects = true;
      }
      return true;
    });
  }

  return { hasInternalState, hasEffects, isControlled: inferControlled(props) };
}

function visit(node: ts.Node, fn: (n: ts.Node) => boolean | void): void {
  if (fn(node) === false) return;
  ts.forEachChild(node, (child) => visit(child, fn));
}

function inferControlled(props: ReadonlyArray<PropFact>): ControlledFlag {
  const names = new Set(props.map((p) => p.name));
  const hasValue = names.has("value") || names.has("checked");
  const hasOnChange =
    names.has("onChange") || names.has("onValueChange") || names.has("onCheckedChange");
  const hasDefault = names.has("defaultValue") || names.has("defaultChecked");

  if (hasValue && hasOnChange && hasDefault) return "mixed";
  if (hasValue && hasOnChange) return true;
  if (hasDefault && !hasValue) return false;
  return "unknown";
}

// ---------------------------------------------------------------------------
// compoundChildren
// ---------------------------------------------------------------------------

/**
 * Build a `rootName → compound child names` map for the entire file in a single
 * pass over the top-level statements.
 *
 * Patterns recognized:
 * - `export const Card = Object.assign(CardRoot, { Header })` — keyed by the variable name
 * - `Object.assign(Card, { Header })` at module scope — keyed by the assign target
 * - `Card.Footer = ...` at module scope — keyed by the property-access object
 */
export function mapFileCompoundChildren(sourceFile: ts.SourceFile): Map<string, string[]> {
  const result = new Map<string, Set<string>>();

  function bucket(name: string): Set<string> {
    let set = result.get(name);
    if (!set) {
      set = new Set();
      result.set(name, set);
    }
    return set;
  }

  for (const stmt of sourceFile.statements) {
    if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
      const target = readObjectAssignTarget(stmt.expression);
      if (target) collectAssignedMembers(stmt.expression, bucket(target.text));
    }

    if (ts.isVariableStatement(stmt)) {
      for (const decl of stmt.declarationList.declarations) {
        if (
          ts.isIdentifier(decl.name) &&
          decl.initializer &&
          ts.isCallExpression(decl.initializer) &&
          isObjectAssignCallee(decl.initializer.expression)
        ) {
          collectAssignedMembers(decl.initializer, bucket(decl.name.text));
        }
      }
    }

    if (ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression)) {
      const expr = stmt.expression;
      if (
        expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
        ts.isPropertyAccessExpression(expr.left) &&
        ts.isIdentifier(expr.left.expression)
      ) {
        bucket(expr.left.expression.text).add(expr.left.name.text);
      }
    }
  }

  const flattened = new Map<string, string[]>();
  for (const [name, set] of result) flattened.set(name, [...set]);
  return flattened;
}

function isObjectAssignCallee(callee: ts.Expression): boolean {
  return (
    ts.isPropertyAccessExpression(callee) &&
    ts.isIdentifier(callee.expression) &&
    callee.expression.text === "Object" &&
    callee.name.text === "assign"
  );
}

function readObjectAssignTarget(call: ts.CallExpression): ts.Identifier | null {
  if (!isObjectAssignCallee(call.expression)) return null;
  const target = call.arguments[0];
  return target && ts.isIdentifier(target) ? target : null;
}

function collectAssignedMembers(call: ts.CallExpression, into: Set<string>): void {
  const members = call.arguments[1];
  if (!members || !ts.isObjectLiteralExpression(members)) return;
  for (const prop of members.properties) {
    if (ts.isShorthandPropertyAssignment(prop)) {
      into.add(prop.name.text);
    } else if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
      into.add(prop.name.text);
    }
  }
}

// ---------------------------------------------------------------------------
// styleSystem + classNamesUsed
// ---------------------------------------------------------------------------

export function mapStyleSystem(
  sourceFile: ts.SourceFile,
  imports: ReadonlyArray<ImportRecord>
): { styleSystem: StyleSystem; classNamesUsed: string[] } {
  const styleSystem = detectStyleSystem(imports);
  const classNamesUsed = collectClassNames(sourceFile);
  return { styleSystem, classNamesUsed };
}

function detectStyleSystem(imports: ReadonlyArray<ImportRecord>): StyleSystem {
  for (const record of imports) {
    const spec = record.moduleSpecifier;
    if (/\.module\.(s?c|sa)ss$/.test(spec)) return "cssModules";
    if (spec === "styled-components") return "styledComponents";
    if (spec === "@emotion/styled" || spec === "@emotion/react") return "cssInJs";
    if (spec === "@stitches/react") return "cssInJs";
    if (/^@?tailwind/.test(spec) || spec === "tailwindcss") return "tailwind";
    if (/\.scss$/.test(spec) || /\.sass$/.test(spec)) return "sass";
    if (/\.css$/.test(spec)) return "plainCss";
  }
  return "unknown";
}

function collectClassNames(sourceFile: ts.SourceFile): string[] {
  const seen = new Set<string>();

  visit(sourceFile, (node) => {
    if (seen.size >= MAX_CLASSNAMES) return false;
    if (!ts.isJsxAttribute(node)) return true;
    if (node.name.getText() !== "className") return true;
    if (!node.initializer) return true;

    if (ts.isStringLiteral(node.initializer)) {
      addClasses(node.initializer.text, seen);
      return true;
    }
    if (ts.isJsxExpression(node.initializer) && node.initializer.expression) {
      const expr = node.initializer.expression;
      if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
        addClasses(expr.text, seen);
      }
    }
    return true;
  });

  return [...seen];
}

function addClasses(raw: string, seen: Set<string>): void {
  for (const cls of raw.split(/\s+/)) {
    if (!cls) continue;
    if (seen.size >= MAX_CLASSNAMES) return;
    seen.add(cls);
  }
}

// ---------------------------------------------------------------------------
// jsdoc + file header
// ---------------------------------------------------------------------------

export function mapJsdoc(
  declaration: ts.Node | null,
  sourceFile: ts.SourceFile
): { jsdocDescription?: string; fileHeaderComment?: string } {
  const result: { jsdocDescription?: string; fileHeaderComment?: string } = {};

  if (declaration) {
    const jsDocs = ts.getJSDocCommentsAndTags(declaration);
    for (const doc of jsDocs) {
      if (ts.isJSDoc(doc) && doc.comment) {
        const text = jsdocCommentToString(doc.comment);
        const firstSentence = text.split(/(?<=\.)\s/)[0]?.trim();
        if (firstSentence) {
          result.jsdocDescription = firstSentence;
          break;
        }
      }
    }
  }

  const text = sourceFile.getFullText();
  const headerMatch = text.match(/^\s*\/\*\*[\s\S]*?\*\//);
  if (headerMatch) {
    result.fileHeaderComment = headerMatch[0]
      .replace(/^\s*\/\*\*/, "")
      .replace(/\*\/\s*$/, "")
      .replace(/^\s*\*\s?/gm, "")
      .trim();
  }

  return result;
}

function jsdocCommentToString(comment: string | ts.NodeArray<ts.JSDocComment> | undefined): string {
  if (!comment) return "";
  if (typeof comment === "string") return comment;
  return comment.map((c) => c.text ?? "").join("");
}

// ---------------------------------------------------------------------------
// events + slots — empty for React
// ---------------------------------------------------------------------------

export function mapEvents(): EventFact[] {
  return [];
}

export function mapSlots(): SlotFact[] {
  return [];
}
