/**
 * ComponentExtractor — persistent LanguageService-based prop extraction.
 *
 * Replaces auto-props.ts with:
 * - Persistent ts.LanguageService (not throwaway ts.createProgram())
 * - Incremental invalidation via projectVersion
 * - Compound component (Object.assign) sub-component prop extraction
 * - Full type serialization to PropMeta format
 *
 * Zero additional dependencies — uses raw TypeScript APIs.
 */

import ts from 'typescript';
import { existsSync, readFileSync, statSync, readdirSync } from 'node:fs';
import { resolve, dirname, join } from 'node:path';

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

export interface ComponentExtractor {
  /** Extract metadata for a single component by export name */
  extract(filePath: string, exportName?: string): ComponentMeta | null;

  /** Extract all exported components found in a file */
  extractAll(filePath: string): ComponentMeta[];

  /** Notify extractor that a file changed (incremental) */
  invalidate(filePath: string): void;

  /** Clean up resources */
  dispose(): void;
}

export interface ComponentMeta {
  name: string;
  filePath: string;
  description: string;
  props: Record<string, PropMeta>;
  composition: CompositionMeta | null;
  exports: string[];
  dependencies: string[];
}

export interface PropMeta {
  name: string;
  type: string;
  typeKind: PropTypeKind;
  values?: string[];
  default?: string;
  description?: string;
  required: boolean;
  source: 'local' | 'inherited';
}

export type PropTypeKind =
  | 'string' | 'number' | 'boolean'
  | 'enum'
  | 'function'
  | 'node'
  | 'element'
  | 'object' | 'array'
  | 'union'
  | 'custom';

export interface CompositionMeta {
  pattern: 'compound' | 'controlled' | 'simple';
  parts: PartMeta[];
  required: string[];
}

export interface PartMeta {
  name: string;
  props: Record<string, PropMeta>;
}

// ---------------------------------------------------------------------------
// Internal types
// ---------------------------------------------------------------------------

interface ResolvedComponent {
  name: string;
  propsType: ts.Type | null;
  componentNode: ts.Node | null;
  compoundParts: Map<string, ts.Type> | null;
}

// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------

export function createComponentExtractor(tsconfigPath?: string): ComponentExtractor {
  let projectVersion = 0;
  const fileVersions = new Map<string, number>();
  const fileSnapshots = new Map<string, ts.IScriptSnapshot>();

  // Parse tsconfig or use inferred settings
  const rootDir = tsconfigPath ? dirname(resolve(tsconfigPath)) : process.cwd();
  const parsedCommandLine = tsconfigPath
    ? parseTsConfig(tsconfigPath)
    : inferCompilerOptions(rootDir);

  const scriptFileNames = new Set(parsedCommandLine.fileNames);

  const host: ts.LanguageServiceHost = {
    getProjectVersion: () => projectVersion.toString(),
    getScriptVersion: (fileName) => (fileVersions.get(fileName) ?? 0).toString(),
    getScriptSnapshot: (fileName) => {
      const cached = fileSnapshots.get(fileName);
      if (cached) return cached;

      let text: string;
      try {
        text = readFileSync(fileName, 'utf-8');
      } catch {
        return undefined;
      }
      const snapshot = ts.ScriptSnapshot.fromString(text);
      fileSnapshots.set(fileName, snapshot);
      return snapshot;
    },
    getScriptFileNames: () => [...scriptFileNames],
    getCompilationSettings: () => parsedCommandLine.options,
    getCurrentDirectory: () => rootDir,
    getDefaultLibFileName: ts.getDefaultLibFilePath,
    fileExists: ts.sys.fileExists,
    readFile: ts.sys.readFile,
    readDirectory: ts.sys.readDirectory,
    directoryExists: ts.sys.directoryExists,
    getDirectories: ts.sys.getDirectories,
    resolveModuleNames: (moduleNames, containingFile) =>
      moduleNames.map((moduleName) => {
        const resolved = ts.resolveModuleName(
          moduleName,
          containingFile,
          parsedCommandLine.options,
          {
            fileExists: ts.sys.fileExists,
            readFile: ts.sys.readFile,
            realpath: ts.sys.realpath,
            directoryExists: ts.sys.directoryExists,
            getDirectories: ts.sys.getDirectories,
            getCurrentDirectory: () => rootDir,
          }
        );

        return resolved.resolvedModule;
      }),
  };

  const languageService = ts.createLanguageService(host, ts.createDocumentRegistry());

  function ensureFile(filePath: string): void {
    const resolved = resolve(filePath);
    if (!scriptFileNames.has(resolved)) {
      scriptFileNames.add(resolved);
      projectVersion++;
    }
  }

  function getChecker(): ts.TypeChecker {
    const program = languageService.getProgram();
    if (!program) throw new Error('Failed to get program from LanguageService');
    return program.getTypeChecker();
  }

  function getSourceFile(filePath: string): ts.SourceFile | undefined {
    return languageService.getProgram()?.getSourceFile(resolve(filePath));
  }

  return {
    extract(filePath: string, exportName?: string): ComponentMeta | null {
      const resolved = resolve(filePath);
      ensureFile(resolved);

      const sourceFile = getSourceFile(resolved);
      if (!sourceFile) return null;

      const checker = getChecker();
      const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
      if (!moduleSymbol) return null;

      const exports = checker.getExportsOfModule(moduleSymbol);
      const targetName = exportName ?? findPrimaryExport(exports, sourceFile);
      if (!targetName) return null;

      const exportSymbol = exports.find(s => s.getName() === targetName);
      if (!exportSymbol) return null;

      const component = resolveExportedComponent(checker, exportSymbol, sourceFile);
      if (!component) return null;

      return buildComponentMeta(checker, component, resolved, sourceFile, exports);
    },

    extractAll(filePath: string): ComponentMeta[] {
      const resolved = resolve(filePath);
      ensureFile(resolved);

      const sourceFile = getSourceFile(resolved);
      if (!sourceFile) return [];

      const checker = getChecker();
      const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
      if (!moduleSymbol) return [];

      const exports = checker.getExportsOfModule(moduleSymbol);
      const results: ComponentMeta[] = [];

      for (const exportSymbol of exports) {
        const name = exportSymbol.getName();
        // Only consider PascalCase exports as potential components
        if (!/^[A-Z]/.test(name)) continue;

        const component = resolveExportedComponent(checker, exportSymbol, sourceFile);
        if (!component) continue;

        const meta = buildComponentMeta(checker, component, resolved, sourceFile, exports);
        if (meta) results.push(meta);
      }

      return results;
    },

    invalidate(filePath: string): void {
      const resolved = resolve(filePath);
      fileVersions.set(resolved, (fileVersions.get(resolved) ?? 0) + 1);
      fileSnapshots.delete(resolved);
      projectVersion++;
    },

    dispose(): void {
      languageService.dispose();
      fileSnapshots.clear();
      fileVersions.clear();
    },
  };
}

// ---------------------------------------------------------------------------
// tsconfig parsing
// ---------------------------------------------------------------------------

function parseTsConfig(tsconfigPath: string): ts.ParsedCommandLine {
  const resolved = resolve(tsconfigPath);
  const configFile = ts.readConfigFile(resolved, ts.sys.readFile);
  if (configFile.error) {
    throw new Error(`Failed to read tsconfig: ${ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n')}`);
  }

  return ts.parseJsonConfigFileContent(
    configFile.config,
    ts.sys,
    dirname(resolved),
    undefined,
    resolved,
  );
}

function inferCompilerOptions(rootDir: string): ts.ParsedCommandLine {
  return {
    options: {
      target: ts.ScriptTarget.ES2022,
      module: ts.ModuleKind.ESNext,
      moduleResolution: ts.ModuleResolutionKind.Bundler,
      jsx: ts.JsxEmit.ReactJSX,
      allowSyntheticDefaultImports: true,
      esModuleInterop: true,
      skipLibCheck: true,
      strict: false,
      noEmit: true,
    },
    fileNames: [],
    errors: [],
  };
}

// ---------------------------------------------------------------------------
// Export resolution
// ---------------------------------------------------------------------------

/**
 * Find the "primary" export — prefers the default export or the first PascalCase export.
 */
function findPrimaryExport(exports: ts.Symbol[], sourceFile: ts.SourceFile): string | null {
  // Default export
  const defaultExport = exports.find(s => s.getName() === 'default');
  if (defaultExport) return 'default';

  // First PascalCase named export
  for (const s of exports) {
    if (/^[A-Z][a-zA-Z0-9]*$/.test(s.getName())) {
      return s.getName();
    }
  }

  return null;
}

/**
 * Resolve an export symbol to a component with its props type.
 * Handles: direct functions, forwardRef, memo, Object.assign, FC<Props>.
 */
function resolveExportedComponent(
  checker: ts.TypeChecker,
  exportSymbol: ts.Symbol,
  sourceFile: ts.SourceFile,
): ResolvedComponent | null {
  const name = exportSymbol.getName();

  // Follow aliases (re-exports)
  let symbol = exportSymbol;
  if (symbol.flags & ts.SymbolFlags.Alias) {
    symbol = checker.getAliasedSymbol(symbol);
  }

  const declarations = symbol.getDeclarations();
  if (!declarations || declarations.length === 0) return null;

  const declaration = declarations[0];

  // Variable declaration: const X = ... (forwardRef, memo, Object.assign, arrow, FC)
  if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
    return resolveFromExpression(checker, name, declaration.initializer, declaration, sourceFile);
  }

  // Function declaration: function X(props: Props) { ... }
  if (ts.isFunctionDeclaration(declaration)) {
    const propsType = extractPropsFromFunctionLike(checker, declaration);
    return propsType ? { name, propsType, componentNode: declaration, compoundParts: null } : null;
  }

  // Export assignment: export default X
  if (ts.isExportAssignment(declaration) && declaration.expression) {
    return resolveFromExpression(checker, name, declaration.expression, null, sourceFile);
  }

  return null;
}

function resolveFromExpression(
  checker: ts.TypeChecker,
  name: string,
  expression: ts.Expression,
  variableDecl: ts.VariableDeclaration | null,
  sourceFile: ts.SourceFile,
): ResolvedComponent | null {
  // Unwrap parentheses, type assertions
  expression = unwrapExpression(expression);

  // Arrow function or function expression
  if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
    const propsType = extractPropsFromFunctionLike(checker, expression);
    return propsType ? { name, propsType, componentNode: expression, compoundParts: null } : null;
  }

  // Call expression: React.forwardRef, React.memo, Object.assign
  if (ts.isCallExpression(expression)) {
    return resolveCallExpression(checker, name, expression, variableDecl, sourceFile);
  }

  // Identifier reference — follow to its declaration
  if (ts.isIdentifier(expression)) {
    const sym = checker.getSymbolAtLocation(expression);
    if (sym) {
      const decls = sym.getDeclarations();
      if (decls && decls.length > 0) {
        const decl = decls[0];
        if (ts.isVariableDeclaration(decl) && decl.initializer) {
          return resolveFromExpression(checker, name, decl.initializer, decl, sourceFile);
        }
        if (ts.isFunctionDeclaration(decl)) {
          const propsType = extractPropsFromFunctionLike(checker, decl);
          return propsType ? { name, propsType, componentNode: decl, compoundParts: null } : null;
        }
      }
    }
  }

  // FC<Props> typed variable
  if (variableDecl?.type && ts.isTypeReferenceNode(variableDecl.type)) {
    const typeName = variableDecl.type.typeName.getText(sourceFile);
    if (typeName.includes('FC') || typeName.includes('FunctionComponent')) {
      const typeArg = variableDecl.type.typeArguments?.[0];
      if (typeArg) {
        const propsType = checker.getTypeFromTypeNode(typeArg);
        return { name, propsType, componentNode: expression, compoundParts: null };
      }
    }
  }

  return null;
}

function resolveCallExpression(
  checker: ts.TypeChecker,
  name: string,
  call: ts.CallExpression,
  variableDecl: ts.VariableDeclaration | null,
  sourceFile: ts.SourceFile,
): ResolvedComponent | null {
  const callee = call.expression;

  // Object.assign(Root, { Header, Body, ... })
  if (isObjectAssignCall(callee, sourceFile)) {
    return resolveObjectAssign(checker, name, call, sourceFile);
  }

  // React.forwardRef<Ref, Props>(fn) or forwardRef<Ref, Props>(fn)
  if (isForwardRefCall(callee)) {
    return resolveForwardRef(checker, name, call, sourceFile);
  }

  // React.memo(Component) or memo(Component)
  if (isMemoCall(callee)) {
    const innerArg = call.arguments[0];
    if (innerArg) {
      return resolveFromExpression(checker, name, innerArg, variableDecl, sourceFile);
    }
  }

  return null;
}

// ---------------------------------------------------------------------------
// Object.assign compound component resolution
// ---------------------------------------------------------------------------

function resolveObjectAssign(
  checker: ts.TypeChecker,
  name: string,
  call: ts.CallExpression,
  sourceFile: ts.SourceFile,
): ResolvedComponent | null {
  if (call.arguments.length < 2) return null;

  // First arg is the root component
  const rootExpr = call.arguments[0];
  const rootResult = resolveFromExpression(checker, name, rootExpr, null, sourceFile);

  // Second arg is the object literal with sub-components
  const subsArg = call.arguments[1];
  const compoundParts = new Map<string, ts.Type>();

  if (ts.isObjectLiteralExpression(subsArg)) {
    for (const prop of subsArg.properties) {
      let subName: string | null = null;
      let subExpression: ts.Expression | null = null;

      if (ts.isShorthandPropertyAssignment(prop)) {
        subName = prop.name.text;
        // Resolve the identifier to its type
        const sym = checker.getSymbolAtLocation(prop.name);
        if (sym) {
          const subPropsType = extractPropsFromComponentSymbol(checker, sym);
          if (subPropsType) {
            compoundParts.set(subName, subPropsType);
          }
        }
      } else if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
        subName = prop.name.text;
        subExpression = prop.initializer;
        if (subExpression) {
          const subType = extractPropsFromExpression(checker, subExpression, sourceFile);
          if (subType) {
            compoundParts.set(subName, subType);
          }
        }
      }
    }
  }

  return {
    name,
    propsType: rootResult?.propsType ?? null,
    componentNode: rootResult?.componentNode ?? rootExpr,
    compoundParts: compoundParts.size > 0 ? compoundParts : null,
  };
}

/**
 * Extract props type from a component symbol (function, variable, etc.)
 */
function extractPropsFromComponentSymbol(checker: ts.TypeChecker, symbol: ts.Symbol): ts.Type | null {
  // Follow aliases
  let sym = symbol;
  if (sym.flags & ts.SymbolFlags.Alias) {
    sym = checker.getAliasedSymbol(sym);
  }

  const decls = sym.getDeclarations();
  if (!decls || decls.length === 0) return null;

  const decl = decls[0];

  if (ts.isFunctionDeclaration(decl)) {
    return extractPropsFromFunctionLike(checker, decl);
  }

  if (ts.isVariableDeclaration(decl) && decl.initializer) {
    // forwardRef, memo, arrow function, etc.
    return extractPropsFromExpressionDeep(checker, decl);
  }

  return null;
}

/**
 * Extract props type from an arbitrary expression (used for Object.assign values).
 */
function extractPropsFromExpression(checker: ts.TypeChecker, expr: ts.Expression, sourceFile: ts.SourceFile): ts.Type | null {
  expr = unwrapExpression(expr);

  if (ts.isIdentifier(expr)) {
    const sym = checker.getSymbolAtLocation(expr);
    if (sym) return extractPropsFromComponentSymbol(checker, sym);
  }

  if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
    return extractPropsFromFunctionLike(checker, expr);
  }

  if (ts.isCallExpression(expr)) {
    if (isForwardRefCall(expr.expression)) {
      const typeArg = expr.typeArguments?.[1];
      if (typeArg) return checker.getTypeFromTypeNode(typeArg);
      const innerArg = expr.arguments[0];
      if (innerArg && (ts.isArrowFunction(innerArg) || ts.isFunctionExpression(innerArg))) {
        return extractPropsFromFunctionLike(checker, innerArg);
      }
    }
    if (isMemoCall(expr.expression) && expr.arguments[0]) {
      return extractPropsFromExpression(checker, expr.arguments[0], sourceFile);
    }
  }

  return null;
}

/**
 * Deep extraction from a variable declaration.
 */
function extractPropsFromExpressionDeep(checker: ts.TypeChecker, decl: ts.VariableDeclaration): ts.Type | null {
  if (!decl.initializer) return null;
  const expr = unwrapExpression(decl.initializer);

  if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
    return extractPropsFromFunctionLike(checker, expr);
  }

  if (ts.isCallExpression(expr)) {
    if (isForwardRefCall(expr.expression)) {
      const typeArg = expr.typeArguments?.[1];
      if (typeArg) return checker.getTypeFromTypeNode(typeArg);
      const innerArg = expr.arguments[0];
      if (innerArg && (ts.isArrowFunction(innerArg) || ts.isFunctionExpression(innerArg))) {
        return extractPropsFromFunctionLike(checker, innerArg);
      }
    }
    if (isMemoCall(expr.expression) && expr.arguments[0]) {
      return extractPropsFromExpressionDeep(checker, {
        ...decl,
        initializer: expr.arguments[0],
      } as ts.VariableDeclaration);
    }
  }

  return null;
}

// ---------------------------------------------------------------------------
// forwardRef resolution
// ---------------------------------------------------------------------------

function resolveForwardRef(
  checker: ts.TypeChecker,
  name: string,
  call: ts.CallExpression,
  sourceFile: ts.SourceFile,
): ResolvedComponent | null {
  // Try type arguments first: forwardRef<Ref, Props>(...)
  const propsTypeArg = call.typeArguments?.[1];
  if (propsTypeArg) {
    const propsType = checker.getTypeFromTypeNode(propsTypeArg);
    const innerArg = call.arguments[0];
    const componentNode = innerArg && (ts.isArrowFunction(innerArg) || ts.isFunctionExpression(innerArg))
      ? innerArg : null;
    return { name, propsType, componentNode, compoundParts: null };
  }

  // Fall back to inner function's parameter type
  const innerArg = call.arguments[0];
  if (innerArg) {
    if (ts.isArrowFunction(innerArg) || ts.isFunctionExpression(innerArg)) {
      const propsType = extractPropsFromFunctionLike(checker, innerArg);
      return propsType ? { name, propsType, componentNode: innerArg, compoundParts: null } : null;
    }
    if (ts.isIdentifier(innerArg)) {
      const sym = checker.getSymbolAtLocation(innerArg);
      if (sym) {
        const propsType = extractPropsFromComponentSymbol(checker, sym);
        if (propsType) return { name, propsType, componentNode: innerArg, compoundParts: null };
      }
    }
  }

  return null;
}

// ---------------------------------------------------------------------------
// Props extraction from function-like declarations
// ---------------------------------------------------------------------------

/**
 * Extract the props type from a function's first parameter.
 */
function extractPropsFromFunctionLike(
  checker: ts.TypeChecker,
  func: ts.FunctionLikeDeclaration,
): ts.Type | null {
  const firstParam = func.parameters[0];
  if (!firstParam) return null;

  // If there's a type annotation, use it
  if (firstParam.type) {
    const directType = checker.getTypeFromTypeNode(firstParam.type);
    if (checker.getPropertiesOfType(directType).length > 0) {
      return directType;
    }

    const apparentType = checker.getApparentType(directType);
    if (checker.getPropertiesOfType(apparentType).length > 0) {
      return apparentType;
    }

    const paramType = checker.getTypeAtLocation(firstParam);
    if (checker.getPropertiesOfType(paramType).length > 0) {
      return paramType;
    }

    const constrainedType = checker.getBaseConstraintOfType(directType);
    if (constrainedType && checker.getPropertiesOfType(constrainedType).length > 0) {
      return constrainedType;
    }

    return directType;
  }

  // Otherwise try to infer from the parameter's symbol
  const paramSymbol = checker.getSymbolAtLocation(firstParam.name);
  if (paramSymbol) {
    return checker.getTypeOfSymbolAtLocation(paramSymbol, firstParam);
  }

  return null;
}

// ---------------------------------------------------------------------------
// Build ComponentMeta from resolved component
// ---------------------------------------------------------------------------

function buildComponentMeta(
  checker: ts.TypeChecker,
  component: ResolvedComponent,
  filePath: string,
  sourceFile: ts.SourceFile,
  moduleExports: ts.Symbol[],
): ComponentMeta | null {
  const props: Record<string, PropMeta> = {};
  const sourceFilePath = toPosixPath(sourceFile.fileName);

  if (component.propsType) {
    extractPropsFromType(checker, component.propsType, props, sourceFilePath);
    if (Object.keys(props).length === 0) {
      const apparentPropsType = checker.getApparentType(component.propsType);
      if (apparentPropsType !== component.propsType) {
        extractPropsFromType(checker, apparentPropsType, props, sourceFilePath);
      }
    }
    if (Object.keys(props).length === 0) {
      const constrainedPropsType = checker.getBaseConstraintOfType(component.propsType);
      if (constrainedPropsType) {
        extractPropsFromType(checker, constrainedPropsType, props, sourceFilePath);
      }
    }
  }

  // Promote explicitly destructured props to "local" so wrapper components like
  // shadcn Input surface the props they actively customize from inherited DOM types.
  const destructuredProps = component.componentNode
    ? extractDestructuredPropNames(component.componentNode)
    : new Set<string>();
  for (const propName of destructuredProps) {
    if (props[propName]) {
      props[propName].source = 'local';
    }
  }

  // Extract defaults from the component function body
  const defaults = component.componentNode
    ? extractDefaultValues(component.componentNode)
    : {};

  // Apply defaults
  for (const [propName, defaultVal] of Object.entries(defaults)) {
    if (props[propName]) {
      props[propName].default = defaultVal;
    }
  }

  // Build composition meta
  let composition: CompositionMeta | null = null;
  if (component.compoundParts && component.compoundParts.size > 0) {
    const parts: PartMeta[] = [];
    for (const [partName, partType] of component.compoundParts) {
      const partProps: Record<string, PropMeta> = {};
      extractPropsFromType(checker, partType, partProps, sourceFilePath);
      parts.push({ name: partName, props: partProps });
    }

    composition = {
      pattern: 'compound',
      parts,
      required: [], // Could be inferred from usage patterns
    };
  }

  // Extract description from JSDoc
  let description = '';
  const componentSymbol = moduleExports.find(s => s.getName() === component.name);
  if (componentSymbol) {
    description = extractJSDocDescription(checker, componentSymbol);
  }

  // Collect export names
  const exportNames = moduleExports
    .filter(s => /^[A-Z]/.test(s.getName()))
    .map(s => s.getName());

  // Collect import dependencies (other components imported)
  const dependencies = extractDependencies(sourceFile);

  return {
    name: component.name,
    filePath,
    description,
    props,
    composition,
    exports: exportNames,
    dependencies,
  };
}

// ---------------------------------------------------------------------------
// Type → PropMeta extraction
// ---------------------------------------------------------------------------

function extractPropsFromType(
  checker: ts.TypeChecker,
  propsType: ts.Type,
  result: Record<string, PropMeta>,
  sourceFilePath: string,
): void {
  for (const symbol of checker.getPropertiesOfType(propsType)) {
    const propName = symbol.getName();

    // Skip internal/private props
    if (propName.startsWith('_') || propName.startsWith('$')) continue;

    // Skip React internal props
    if (propName === 'key' || propName === 'ref') continue;

    const declarations = symbol.getDeclarations() ?? [];

    // Determine source: local vs inherited
    const isLocal = declarations.some(
      d => toPosixPath(d.getSourceFile().fileName) === sourceFilePath,
    );

    const referenceNode = declarations[0];
    if (!referenceNode) continue;

    const propType = checker.getTypeOfSymbolAtLocation(symbol, referenceNode);
    const serialized = serializePropType(checker, propType);
    const description = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
    const required = (symbol.flags & ts.SymbolFlags.Optional) === 0;

    // Extract @default from JSDoc tags
    let jsDocDefault: string | undefined;
    const jsDocTags = symbol.getJsDocTags(checker);
    const defaultTag = jsDocTags.find(t => t.name === 'default');
    if (defaultTag?.text) {
      jsDocDefault = ts.displayPartsToString(defaultTag.text).trim();
    }

    result[propName] = {
      name: propName,
      type: serialized.type,
      typeKind: serialized.typeKind,
      values: serialized.values,
      default: jsDocDefault,
      description: description || undefined,
      required,
      source: isLocal ? 'local' : 'inherited',
    };
  }
}

// ---------------------------------------------------------------------------
// Type serializer: ts.Type → PropMeta shape
// ---------------------------------------------------------------------------

function serializePropType(
  checker: ts.TypeChecker,
  type: ts.Type,
): Pick<PropMeta, 'type' | 'typeKind' | 'values'> {
  // Check for ReactNode/ReactElement BEFORE union decomposition,
  // because ReactNode is defined as a large union in React's types.
  const aliasSymbol = type.aliasSymbol;
  if (aliasSymbol) {
    const aliasName = aliasSymbol.getName();
    if (aliasName === 'ReactNode') {
      return { type: 'ReactNode', typeKind: 'node' };
    }
    if (aliasName === 'ReactElement') {
      return { type: 'ReactElement', typeKind: 'element' };
    }
  }

  // Handle union types (including optional which adds undefined)
  if (type.isUnion()) {
    const nonNullableTypes = type.types.filter(
      t => !((t.flags & ts.TypeFlags.Undefined) || (t.flags & ts.TypeFlags.Null) || (t.flags & ts.TypeFlags.Void)),
    );

    // Single type after filtering nullable
    if (nonNullableTypes.length === 1) {
      return serializePropType(checker, nonNullableTypes[0]);
    }

    // All string literals → enum
    if (nonNullableTypes.length > 0 && nonNullableTypes.every(t => t.isStringLiteral())) {
      const values = nonNullableTypes.map(t => (t as ts.StringLiteralType).value);
      return {
        type: values.map(v => `"${v}"`).join(' | '),
        typeKind: 'enum',
        values,
      };
    }

    // All boolean-like
    if (nonNullableTypes.every(t => isBooleanLike(t))) {
      return { type: 'boolean', typeKind: 'boolean' };
    }

    // Mixed union
    const typeStr = checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation);
    return { type: typeStr, typeKind: 'union' };
  }

  // Check for ReactNode / ReactElement
  const typeStr = checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation);
  if (typeStr.includes('ReactNode')) {
    return { type: 'ReactNode', typeKind: 'node' };
  }
  if (typeStr.includes('ReactElement') || typeStr.includes('JSX.Element')) {
    return { type: 'ReactElement', typeKind: 'element' };
  }

  // Function types
  if (type.getCallSignatures().length > 0) {
    return { type: typeStr, typeKind: 'function' };
  }

  // Primitives
  if (type.flags & ts.TypeFlags.String) return { type: 'string', typeKind: 'string' };
  if (type.flags & ts.TypeFlags.Number) return { type: 'number', typeKind: 'number' };
  if (type.flags & ts.TypeFlags.Boolean || type.flags & ts.TypeFlags.BooleanLiteral) {
    return { type: 'boolean', typeKind: 'boolean' };
  }

  // String literal (not in union)
  if (type.isStringLiteral()) {
    return { type: `"${type.value}"`, typeKind: 'enum', values: [type.value] };
  }

  // Array
  if (checker.isArrayType(type) || checker.isTupleType(type)) {
    return { type: typeStr, typeKind: 'array' };
  }

  // Object
  if (type.flags & ts.TypeFlags.Object) {
    return { type: typeStr, typeKind: 'object' };
  }

  return { type: typeStr, typeKind: 'custom' };
}

// ---------------------------------------------------------------------------
// Default value extraction
// ---------------------------------------------------------------------------

function extractDefaultValues(node: ts.Node): Record<string, string> {
  const defaults: Record<string, string> = {};

  // Find function-like node
  let funcNode: ts.FunctionLikeDeclaration | null = null;
  if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
    funcNode = node;
  }

  if (!funcNode?.parameters?.length) return defaults;

  const firstParam = funcNode.parameters[0];
  if (!ts.isObjectBindingPattern(firstParam.name)) return defaults;

  for (const element of firstParam.name.elements) {
    let propName: string | null = null;

    if (element.propertyName) {
      if (ts.isIdentifier(element.propertyName) || ts.isStringLiteral(element.propertyName)) {
        propName = element.propertyName.text;
      }
    } else if (ts.isIdentifier(element.name)) {
      propName = element.name.text;
    }

    if (!propName || !element.initializer) continue;

    const value = readLiteralValue(element.initializer);
    if (value !== undefined) {
      defaults[propName] = value;
    }
  }

  return defaults;
}

function extractDestructuredPropNames(node: ts.Node): Set<string> {
  const names = new Set<string>();

  let funcNode: ts.FunctionLikeDeclaration | null = null;
  if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
    funcNode = node;
  }

  if (!funcNode?.parameters?.length) return names;

  const firstParam = funcNode.parameters[0];
  if (!ts.isObjectBindingPattern(firstParam.name)) return names;

  for (const element of firstParam.name.elements) {
    if (element.dotDotDotToken || !ts.isIdentifier(element.name)) continue;

    const propName = element.propertyName
      ? element.propertyName.getText()
      : element.name.text;

    if (propName === 'className' || propName === 'children' || propName === 'ref') {
      continue;
    }

    names.add(propName);
  }

  return names;
}

function readLiteralValue(expression: ts.Expression): string | undefined {
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
    return expression.text;
  }
  if (ts.isNumericLiteral(expression)) {
    return expression.text;
  }
  if (expression.kind === ts.SyntaxKind.TrueKeyword) return 'true';
  if (expression.kind === ts.SyntaxKind.FalseKeyword) return 'false';
  if (expression.kind === ts.SyntaxKind.NullKeyword) return 'null';
  if (
    ts.isPrefixUnaryExpression(expression) &&
    expression.operator === ts.SyntaxKind.MinusToken &&
    ts.isNumericLiteral(expression.operand)
  ) {
    return `-${expression.operand.text}`;
  }

  return undefined;
}

// ---------------------------------------------------------------------------
// JSDoc extraction
// ---------------------------------------------------------------------------

function extractJSDocDescription(checker: ts.TypeChecker, symbol: ts.Symbol): string {
  // Follow aliases
  let sym = symbol;
  if (sym.flags & ts.SymbolFlags.Alias) {
    sym = checker.getAliasedSymbol(sym);
  }

  const docComment = ts.displayPartsToString(sym.getDocumentationComment(checker)).trim();
  if (docComment) return docComment;

  // Try to get from the Props interface
  const decls = sym.getDeclarations();
  if (decls) {
    for (const decl of decls) {
      // Look for the Props type
      const sourceFile = decl.getSourceFile();
      for (const stmt of sourceFile.statements) {
        if (
          (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) &&
          stmt.name.text === `${symbol.getName()}Props`
        ) {
          const propsDoc = ts.displayPartsToString(
            checker.getSymbolAtLocation(stmt.name)?.getDocumentationComment(checker) ?? [],
          ).trim();
          if (propsDoc) return propsDoc;
        }
      }
    }
  }

  return '';
}

// ---------------------------------------------------------------------------
// Import dependency extraction
// ---------------------------------------------------------------------------

function extractDependencies(sourceFile: ts.SourceFile): string[] {
  const deps: string[] = [];
  for (const stmt of sourceFile.statements) {
    if (!ts.isImportDeclaration(stmt)) continue;
    if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue;

    const modulePath = stmt.moduleSpecifier.text;
    // Only relative imports to other components
    if (!modulePath.startsWith('.') && !modulePath.startsWith('/')) continue;

    const clause = stmt.importClause;
    if (!clause) continue;

    // Default import
    if (clause.name && /^[A-Z]/.test(clause.name.text)) {
      deps.push(clause.name.text);
    }

    // Named imports
    if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
      for (const element of clause.namedBindings.elements) {
        if (/^[A-Z]/.test(element.name.text)) {
          deps.push(element.name.text);
        }
      }
    }
  }
  return deps;
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function unwrapExpression(expr: ts.Expression): ts.Expression {
  while (true) {
    if (ts.isParenthesizedExpression(expr)) {
      expr = expr.expression;
    } else if (ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr)) {
      expr = expr.expression;
    } else {
      return expr;
    }
  }
}

function isObjectAssignCall(callee: ts.Expression, sourceFile: ts.SourceFile): boolean {
  if (
    ts.isPropertyAccessExpression(callee) &&
    ts.isIdentifier(callee.expression) &&
    callee.expression.text === 'Object' &&
    callee.name.text === 'assign'
  ) {
    return true;
  }
  return false;
}

function isForwardRefCall(callee: ts.Expression): boolean {
  // React.forwardRef(...)
  if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'forwardRef') {
    return true;
  }
  // forwardRef(...)
  if (ts.isIdentifier(callee) && callee.text === 'forwardRef') {
    return true;
  }
  return false;
}

function isMemoCall(callee: ts.Expression): boolean {
  if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'memo') {
    return true;
  }
  if (ts.isIdentifier(callee) && callee.text === 'memo') {
    return true;
  }
  return false;
}

function isBooleanLike(type: ts.Type): boolean {
  return (
    (type.flags & ts.TypeFlags.BooleanLike) !== 0 ||
    type.flags === ts.TypeFlags.BooleanLiteral
  );
}

function toPosixPath(filePath: string): string {
  return filePath.replace(/\\/g, '/');
}
