/**
 * Storybook CSF Parser (pure).
 *
 * Parses Storybook Component Story Format (CSF) file *content* and extracts
 * structured data (component name, import path, argTypes, stories/variants).
 * No filesystem access — callers supply the file content as a string, so this
 * runs identically in the CLI (local fs), the Cloud scan (GitHub API content),
 * and the browser. Supports CSF 2.0 and 3.0.
 */

import type { ParsedStoryFile, ParsedMeta, ParsedArgType, ParsedStory } from "./types.js";
import { parse as parseBabel } from "@babel/parser";
import type { StoryFacts, StoryFactsStory, StoryFactsRung } from "./types.js";

/**
 * Parse story content into structured data.
 */
export function parseStoryContent(content: string, filePath: string): ParsedStoryFile {
  const warnings: string[] = [];

  // Parse meta/default export
  const meta = parseMeta(content, filePath, warnings);

  // Parse argTypes
  const argTypes = parseArgTypes(content, warnings);

  // Extract top-level const declarations for spread resolution
  const constDeclarations = extractConstDeclarations(content);

  // Parse individual stories (pass const declarations for spread resolution)
  const stories = parseStories(content, meta.componentName, warnings, constDeclarations);

  // Calculate confidence score based on extraction quality
  const confidence = calculateConfidence(meta, argTypes, stories, warnings);

  return {
    filePath,
    meta,
    argTypes,
    stories,
    warnings,
    confidence,
  };
}

/** Parse story content into the locked StoryFacts IR. */
export function parseStoryFacts(content: string, filePath: string): StoryFacts {
  const syntaxDiagnostic = parseSyntaxDiagnostic(content);
  if (syntaxDiagnostic) {
    return {
      file: filePath,
      tags: [],
      metaArgs: {},
      argTypes: {},
      stories: [],
      rung: "skipped",
      diagnostics: [syntaxDiagnostic],
    };
  }

  const warnings: string[] = [];
  const meta = parseMeta(content, filePath, warnings);
  const metaObject = extractMetaObjectContent(content);
  const metaArgs = metaObject
    ? parseStaticArgsObject(extractTopLevelObjectProperty(metaObject, "args"))
    : {};
  const stories = extractStoryFactsStories(content, filePath);
  const argTypes = parseArgTypes(content, warnings);
  const diagnostics = warnings.map((message) => ({
    level: "warning" as const,
    message,
  }));

  return {
    file: filePath,
    ...(meta.title ? { title: meta.title } : {}),
    ...(meta.componentImport
      ? {
          componentImport: {
            specifier: meta.componentImport,
            name: meta.componentName,
          },
        }
      : {}),
    tags: meta.tags ?? [],
    metaArgs,
    argTypes,
    stories,
    rung: resolveStoryFactsRung(meta, stories),
    diagnostics,
    ...(meta.description ? { autodocsDescription: meta.description } : {}),
  };
}

function parseSyntaxDiagnostic(content: string): { level: "error"; message: string } | null {
  try {
    parseBabel(content, {
      sourceType: "module",
      plugins: ["typescript", "jsx"],
    });
    return null;
  } catch (error) {
    const detail = error instanceof Error ? error.message : "unknown syntax error";
    return { level: "error", message: `Storybook CSF parse failed: ${detail}` };
  }
}

function resolveStoryFactsRung(meta: ParsedMeta, stories: StoryFactsStory[]): StoryFactsRung {
  if (stories.length === 0) return "presence-only";
  const storiesWithArgs = stories.filter((story) => story.args !== undefined).length;
  if (meta.componentImport && meta.title && storiesWithArgs === stories.length) {
    return "full";
  }
  if (storiesWithArgs > 0 || meta.componentImport || meta.title) return "partial";
  return "presence-only";
}

/**
 * Calculate confidence score based on parsing quality.
 * Higher score = more reliable extraction.
 */
function calculateConfidence(
  meta: ParsedMeta,
  argTypes: Record<string, ParsedArgType>,
  stories: ParsedStory[],
  warnings: string[]
): number {
  let score = 1.0;

  // Penalize for warnings
  score -= warnings.length * 0.1;

  // Penalize if component name is unknown
  if (meta.componentName === "Unknown") {
    score -= 0.3;
  }

  // Penalize if no component import found
  if (!meta.componentImport) {
    score -= 0.1;
  }

  // Penalize if no argTypes found (less prop info)
  if (Object.keys(argTypes).length === 0) {
    score -= 0.1;
  }

  // Penalize if no stories found
  if (stories.length === 0) {
    score -= 0.3;
  }

  // Penalize for stories with custom renders (can't auto-migrate)
  const customRenderCount = stories.filter((s) => s.hasCustomRender).length;
  if (customRenderCount > 0) {
    score -= (customRenderCount / Math.max(stories.length, 1)) * 0.2;
  }

  // Clamp to [0, 1]
  return Math.max(0, Math.min(1, score));
}

/**
 * Extract top-level const declarations that can be used for spread resolution.
 * Looks for patterns like: const defaultArgs = { ... }
 * Returns a map of variable name -> parsed object value.
 */
function extractConstDeclarations(content: string): Map<string, Record<string, unknown>> {
  const declarations = new Map<string, Record<string, unknown>>();

  // Pattern: const variableName = { ... } or const variableName: Type = { ... }
  // We need to find each const declaration and extract its object value
  const constPattern = /const\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*\{/g;

  let match;
  while ((match = constPattern.exec(content)) !== null) {
    const varName = match[1];

    // Skip common non-args patterns
    if (varName === "meta" || varName === "default" || varName === "Template") {
      continue;
    }

    // Find the opening brace position
    const braceStart = match.index + match[0].length - 1;

    // Find matching closing brace
    const braceEnd = findMatchingBraceInContent(content, braceStart, "{", "}");
    if (braceEnd === -1) {
      continue;
    }

    // Extract the object content
    const objectContent = content.slice(braceStart + 1, braceEnd);

    // Parse it (without spread resolution to avoid infinite loops)
    const parsed = parseArgsSimple(objectContent);

    // Only store if we got something useful
    if (Object.keys(parsed).length > 0) {
      declarations.set(varName, parsed);
    }
  }

  return declarations;
}

function extractMetaObjectContent(content: string): string | null {
  const inlineDefault = extractObjectAfterPattern(content, /export\s+default\s*\{/);
  if (inlineDefault) return inlineDefault;

  const exportedName = content.match(/export\s+default\s+(\w+)/)?.[1];
  if (exportedName) {
    const exportedObject = extractObjectAfterPattern(
      content,
      new RegExp(
        `(?:const|let|var)\\s+${escapeRegExp(exportedName)}(?:\\s*:\\s*[^=]+)?\\s*=\\s*\\{`
      )
    );
    if (exportedObject) return exportedObject;
  }

  return extractObjectAfterPattern(content, /const\s+meta(?:\s*:\s*[^=]+)?\s*=\s*\{/);
}

function extractObjectAfterPattern(content: string, pattern: RegExp): string | null {
  const match = pattern.exec(content);
  if (!match || match.index === undefined) return null;
  const braceStart = match.index + match[0].length - 1;
  const braceEnd = findMatchingBraceInContent(content, braceStart, "{", "}");
  if (braceEnd === -1) return null;
  return content.slice(braceStart + 1, braceEnd);
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function extractTopLevelObjectProperty(objectContent: string, propertyName: string): string | null {
  let depth = 0;
  let inString: string | null = null;

  for (let i = 0; i < objectContent.length; i++) {
    const char = objectContent[i];
    const prevChar = i > 0 ? objectContent[i - 1] : "";

    if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
      if (inString === char) {
        inString = null;
      } else if (inString === null) {
        inString = char;
      }
      continue;
    }

    if (inString !== null) continue;
    if (char === "{" || char === "[" || char === "(") depth++;
    if (char === "}" || char === "]" || char === ")") depth--;
    if (depth !== 0) continue;

    const rest = objectContent.slice(i);
    const match = rest.match(new RegExp(`^${propertyName}\\s*:\\s*\\{`));
    if (!match) continue;

    const braceStart = i + match[0].length - 1;
    const braceEnd = findMatchingBraceInContent(objectContent, braceStart, "{", "}");
    if (braceEnd === -1) return null;
    return objectContent.slice(braceStart + 1, braceEnd);
  }

  return null;
}

function extractTopLevelStringProperty(objectContent: string, propertyName: string): string | null {
  let depth = 0;
  let inString: string | null = null;

  for (let i = 0; i < objectContent.length; i++) {
    const char = objectContent[i];
    const prevChar = i > 0 ? objectContent[i - 1] : "";

    if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
      if (inString === char) {
        inString = null;
      } else if (inString === null) {
        inString = char;
      }
      continue;
    }

    if (inString !== null) continue;
    if (char === "{" || char === "[" || char === "(") depth++;
    if (char === "}" || char === "]" || char === ")") depth--;
    if (depth !== 0) continue;

    const rest = objectContent.slice(i);
    const match = rest.match(new RegExp(`^${propertyName}\\s*:\\s*(['"\`])([^'"\`]+)\\1`));
    if (match) return match[2];
  }

  return null;
}

function parseStaticArgsObject(content: string | null): Record<string, unknown> {
  if (!content) return {};
  return stripUnsafeStaticValues(parseArgs(content));
}

function stripUnsafeStaticValues(value: unknown): Record<string, unknown> {
  if (!isPlainRecord(value)) return {};
  const result: Record<string, unknown> = {};

  for (const [key, entry] of Object.entries(value)) {
    if (key.startsWith("__")) continue;
    const stripped = stripUnsafeStaticValue(entry);
    if (stripped !== undefined) result[key] = stripped;
  }

  return result;
}

function stripUnsafeStaticValue(value: unknown): unknown {
  if (typeof value === "string" && value.startsWith("__")) return undefined;
  if (Array.isArray(value)) {
    const items = value
      .map((item) => stripUnsafeStaticValue(item))
      .filter((item) => item !== undefined);
    return items;
  }
  if (isPlainRecord(value)) return stripUnsafeStaticValues(value);
  return value;
}

function isPlainRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

/**
 * Simple args parser that doesn't do spread resolution.
 * Used for extracting const declarations to avoid infinite recursion.
 */
function parseArgsSimple(content: string): Record<string, unknown> {
  const args: Record<string, unknown> = {};

  const pairs = splitAtTopLevelCommas(content);

  for (const pair of pairs) {
    const trimmed = pair.trim();
    if (!trimmed) continue;

    // Skip spreads in simple parsing
    if (trimmed.startsWith("...")) {
      continue;
    }

    // Handle shorthand property
    if (/^\w+$/.test(trimmed)) {
      args[trimmed] = `__REF__${trimmed}`;
      continue;
    }

    // Pattern: key: value
    const colonIndex = trimmed.indexOf(":");
    if (colonIndex > 0) {
      const key = trimmed.slice(0, colonIndex).trim();
      const valueStr = trimmed.slice(colonIndex + 1).trim();

      if (/^\w+$/.test(key)) {
        args[key] = parseArgValue(valueStr);
      }
    }
  }

  return args;
}

/**
 * Parse the default export (meta) from a story file.
 */
function parseMeta(content: string, filePath: string, warnings: string[]): ParsedMeta {
  const result: ParsedMeta = {
    title: "",
    componentName: "",
  };

  // Storybook title is a top-level meta field. Nested props like
  // `args: { title: "Hello" }` are component data and must not rename the
  // component.
  const metaObject = extractMetaObjectContent(content);
  const selectedTitle = metaObject ? extractTopLevelStringProperty(metaObject, "title") : null;

  if (selectedTitle) {
    result.title = selectedTitle;
    // Extract component name from title (last fragment)
    const fragments = result.title.split("/");
    result.componentName = fragments[fragments.length - 1];
  }

  // Try to extract component reference
  // Pattern: component: Button or component: MyComponent
  const componentMatch = content.match(/component:\s*(\w+)/);
  if (componentMatch) {
    // Use component name from component field if title didn't provide one
    if (!result.componentName) {
      result.componentName = componentMatch[1];
    }
    // Try to find the import for this component
    const importMatch = content.match(
      new RegExp(
        `import\\s*{[^}]*\\b${componentMatch[1]}\\b[^}]*}\\s*from\\s*['"\`]([^'"\`]+)['"\`]`
      )
    );
    if (importMatch) {
      result.componentImport = importMatch[1];
      result.isDefaultImport = false;
    } else {
      // Try default import
      const defaultImportMatch = content.match(
        new RegExp(`import\\s+${componentMatch[1]}\\s+from\\s*['"\`]([^'"\`]+)['"\`]`)
      );
      if (defaultImportMatch) {
        result.componentImport = defaultImportMatch[1];
        result.isDefaultImport = true;
      }
    }
  }

  // Extract tags
  // Pattern: tags: ['autodocs', 'stable']
  const tagsMatch = content.match(/tags:\s*\[([^\]]+)\]/);
  if (tagsMatch) {
    const tagsContent = tagsMatch[1];
    result.tags = tagsContent
      .split(",")
      .map((t) => t.trim().replace(/['"`]/g, ""))
      .filter(Boolean);
  }

  // Extract description from parameters.docs
  // Pattern: description: { component: '...' }
  const descMatch = content.match(/description:\s*\{[^}]*component:\s*['"`]([^'"`]+)['"`]/);
  if (descMatch) {
    result.description = descMatch[1];
  }

  // Fallback: infer component name from file path
  if (!result.componentName) {
    const match = filePath.match(/([^/\\]+)\.stories\.(tsx?|jsx?|mdx)$/);
    if (match) {
      result.componentName = match[1];
    } else {
      result.componentName = "Unknown";
      warnings.push("Could not determine component name");
    }
  }

  // Fallback: generate title from component name
  if (!result.title) {
    result.title = `Components/${result.componentName}`;
    warnings.push(`No title found, using default: ${result.title}`);
  }

  return result;
}

/**
 * Parse argTypes from the meta export.
 */
function parseArgTypes(content: string, warnings: string[]): Record<string, ParsedArgType> {
  const result: Record<string, ParsedArgType> = {};

  // Find argTypes block - match until we find the closing brace at the right level
  const argTypesStart = content.indexOf("argTypes:");
  if (argTypesStart === -1) {
    return result;
  }

  // Find the opening brace
  const braceStart = content.indexOf("{", argTypesStart);
  if (braceStart === -1) {
    return result;
  }

  // Find matching closing brace by counting nesting
  let depth = 1;
  let braceEnd = braceStart + 1;
  while (depth > 0 && braceEnd < content.length) {
    if (content[braceEnd] === "{") depth++;
    if (content[braceEnd] === "}") depth--;
    braceEnd++;
  }

  const argTypesContent = content.slice(braceStart + 1, braceEnd - 1);

  // Parse individual argType entries by looking for property patterns
  // Match property: { ... } at the top level of argTypes
  let pos = 0;
  while (pos < argTypesContent.length) {
    // Skip whitespace and commas
    while (pos < argTypesContent.length && /[\s,]/.test(argTypesContent[pos])) {
      pos++;
    }

    // Find property name
    const nameMatch = argTypesContent.slice(pos).match(/^(\w+)\s*:\s*\{/);
    if (!nameMatch) break;

    const propName = nameMatch[1];
    pos += nameMatch[0].length - 1; // Position at opening brace

    // Find matching closing brace
    let propDepth = 1;
    const propStart = pos + 1;
    pos++;
    while (propDepth > 0 && pos < argTypesContent.length) {
      if (argTypesContent[pos] === "{") propDepth++;
      if (argTypesContent[pos] === "}") propDepth--;
      pos++;
    }

    const propContent = argTypesContent.slice(propStart, pos - 1);
    result[propName] = parseArgTypeContent(propContent, warnings);
  }

  return result;
}

/**
 * Parse the content of a single argType definition.
 */
function parseArgTypeContent(content: string, _warnings: string[]): ParsedArgType {
  const result: ParsedArgType = {};

  // Extract control type
  // Pattern: control: 'select' or control: { type: 'select' }
  const controlMatch = content.match(/control:\s*['"`](\w+)['"`]/);
  if (controlMatch) {
    result.control = controlMatch[1];
  } else {
    const controlTypeMatch = content.match(/control:\s*\{[^}]*type:\s*['"`](\w+)['"`]/);
    if (controlTypeMatch) {
      result.control = controlTypeMatch[1];
    }
  }

  // Extract options
  // Pattern: options: ['a', 'b', 'c']
  const optionsMatch = content.match(/options:\s*\[([^\]]+)\]/);
  if (optionsMatch) {
    result.options = optionsMatch[1]
      .split(",")
      .map((o) => o.trim().replace(/['"`]/g, ""))
      .filter(Boolean);
  }

  // Extract description
  // Pattern: description: '...'
  const descMatch = content.match(/description:\s*['"`]([^'"`]+)['"`]/);
  if (descMatch) {
    result.description = descMatch[1];
  }

  // Extract default value
  // Pattern: defaultValue: { summary: '...' } or defaultValue: '...'
  const defaultMatch = content.match(/defaultValue:\s*\{[^}]*summary:\s*['"`]([^'"`]+)['"`]/);
  if (defaultMatch) {
    result.defaultValue = defaultMatch[1];
  } else {
    const simpleDefaultMatch = content.match(/defaultValue:\s*['"`]?([^,\s'"`]+)['"`]?/);
    if (simpleDefaultMatch && simpleDefaultMatch[1] !== "{") {
      result.defaultValue = simpleDefaultMatch[1];
    }
  }

  return result;
}

function extractStoryFactsStories(content: string, filePath: string): StoryFactsStory[] {
  const stories: StoryFactsStory[] = [];
  const exportPattern = /export\s+const\s+([A-Za-z_$][\w$]*)(?:\s*:\s*[^=]+)?\s*=\s*/g;
  let match;

  while ((match = exportPattern.exec(content)) !== null) {
    const name = match[1];
    if (name === "meta" || name === "default") continue;

    const initializerStart = skipWhitespace(content, match.index + match[0].length);
    const story: StoryFactsStory = {
      ref: `${filePath}#${name}`,
      name,
    };

    if (content[initializerStart] === "{") {
      const end = findMatchingBraceInContent(content, initializerStart, "{", "}");
      if (end !== -1) {
        const storyObject = content.slice(initializerStart + 1, end);
        const argsContent = extractTopLevelObjectProperty(storyObject, "args");
        const args = parseStaticArgsObject(argsContent);
        if (Object.keys(args).length > 0 || argsContent !== null) story.args = args;
        exportPattern.lastIndex = end + 1;
      }
      stories.push(story);
      continue;
    }

    const callArgs = extractCallFirstObjectArgument(content, initializerStart);
    if (callArgs) {
      const args = parseStaticArgsObject(callArgs);
      if (Object.keys(args).length > 0) story.args = args;
    }

    stories.push(story);
  }

  return stories;
}

function skipWhitespace(content: string, start: number): number {
  let index = start;
  while (index < content.length && /\s/.test(content[index])) index++;
  return index;
}

function extractCallFirstObjectArgument(content: string, start: number): string | null {
  const parenStart = content.indexOf("(", start);
  if (parenStart === -1) return null;

  const beforeParen = content.slice(start, parenStart).trim();
  if (!/^[A-Za-z_$][\w$.]*$/.test(beforeParen)) return null;

  const firstArgStart = skipWhitespace(content, parenStart + 1);
  if (content[firstArgStart] !== "{") return null;

  const firstArgEnd = findMatchingBraceInContent(content, firstArgStart, "{", "}");
  if (firstArgEnd === -1) return null;
  return content.slice(firstArgStart + 1, firstArgEnd);
}

/**
 * Parse individual story exports.
 *
 * Supports both CSF 2.0 and CSF 3.0 formats:
 * - CSF 3.0: export const Story = { args: {...} }
 * - CSF 2.0: export const Story = Template.bind({}) + Story.args = {...}
 */
function parseStories(
  content: string,
  componentName: string,
  warnings: string[],
  constDeclarations: Map<string, Record<string, unknown>>
): ParsedStory[] {
  const stories: ParsedStory[] = [];
  const storyNames = new Set<string>();

  // CSF 3.0: export const StoryName: Story = { ... }
  const csf3Pattern = /export\s+const\s+(\w+)(?::\s*\w+)?\s*=\s*\{([^;]*(?:\{[^}]*\}[^;]*)*)\}/g;

  let match;
  while ((match = csf3Pattern.exec(content)) !== null) {
    const storyName = match[1];
    const storyContent = match[2];

    // Skip if it's the meta export
    if (storyName === "default" || storyName === "meta") {
      continue;
    }

    // Skip if it looks like a type definition
    if (storyContent.includes("typeof")) {
      continue;
    }

    const story = parseStoryContent2(
      storyName,
      storyContent,
      componentName,
      warnings,
      constDeclarations
    );
    if (story) {
      stories.push(story);
      storyNames.add(storyName);
    }
  }

  // CSF 2.0: export const StoryName = Template.bind({})
  // Followed by StoryName.args = { ... }
  const csf2Pattern = /export\s+const\s+(\w+)\s*=\s*(\w+)\.bind\s*\(\s*\{\s*\}\s*\)/g;

  while ((match = csf2Pattern.exec(content)) !== null) {
    const storyName = match[1];
    const templateName = match[2];

    // Skip if already found via CSF 3.0 pattern
    if (storyNames.has(storyName)) {
      continue;
    }

    // Skip meta-like names
    if (storyName === "default" || storyName === "meta") {
      continue;
    }

    // Look for StoryName.args = { ... } using proper brace matching
    const argsContent = extractArgsContent(content, storyName);

    const story: ParsedStory = {
      name: storyName,
      args: argsContent ? parseArgs(argsContent, constDeclarations) : {},
      hasCustomRender: isCustomTemplate(content, templateName),
    };

    // Look for StoryName.parameters.docs.description.story
    const descPattern = new RegExp(
      `${storyName}\\.parameters\\s*=\\s*\\{[^}]*docs:\\s*\\{[^}]*description:\\s*\\{[^}]*story:\\s*['"\`]([^'"\`]+)['"\`]`
    );
    const descMatch = content.match(descPattern);
    if (descMatch) {
      story.description = descMatch[1];
    }

    stories.push(story);
    storyNames.add(storyName);
  }

  return stories;
}

/**
 * Extract the content inside StoryName.args = { ... } using proper brace matching.
 */
function extractArgsContent(content: string, storyName: string): string | null {
  // Find the position of StoryName.args =
  const argsAssignPattern = new RegExp(`${storyName}\\.args\\s*=\\s*\\{`);
  const argsMatch = content.match(argsAssignPattern);

  if (!argsMatch || argsMatch.index === undefined) {
    return null;
  }

  // Find the opening brace position
  const startPos = argsMatch.index + argsMatch[0].length - 1; // Position of {

  // Find the matching closing brace
  const closingIndex = findMatchingBraceInContent(content, startPos, "{", "}");

  if (closingIndex === -1) {
    return null;
  }

  // Return the content between the braces
  return content.slice(startPos + 1, closingIndex);
}

/**
 * Find matching brace in content string, respecting strings and nesting.
 */
function findMatchingBraceInContent(
  content: string,
  startIndex: number,
  openChar: string,
  closeChar: string
): number {
  let depth = 0;
  let inString: string | null = null;

  for (let i = startIndex; i < content.length; i++) {
    const char = content[i];
    const prevChar = i > 0 ? content[i - 1] : "";

    // Handle string boundaries (but not escaped quotes)
    if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
      if (inString === char) {
        inString = null;
      } else if (inString === null) {
        inString = char;
      }
    }

    if (inString === null) {
      if (char === openChar) {
        depth++;
      } else if (char === closeChar) {
        depth--;
        if (depth === 0) {
          return i;
        }
      }
    }
  }

  return -1;
}

/**
 * Check if a template has custom rendering logic beyond simple args spreading.
 *
 * Simple templates look like: (args) => <Component {...args} />
 * Custom templates have hooks, state, or complex logic.
 */
function isCustomTemplate(content: string, templateName: string): boolean {
  // Look for the template definition
  const templatePattern = new RegExp(
    `const\\s+${templateName}[^=]*=\\s*\\([^)]*\\)\\s*=>\\s*([\\s\\S]*?)(?=\\n(?:export|const\\s+\\w+\\s*=)|$)`,
    "m"
  );
  const match = content.match(templatePattern);

  if (!match) {
    return false;
  }

  const templateBody = match[1].trim();

  // Simple template patterns:
  // (args) => <Component {...args} />
  // (args) => <Component {...args}>text</Component>
  // (args) => (<Component {...args} />)
  const simplePatterns = [
    /^<\w+\s+\{\.\.\.args\}\s*\/?>/, // <Comp {...args} /> or <Comp {...args}>
    /^\(\s*<\w+\s+\{\.\.\.args\}\s*\/?>\s*\)/, // (<Comp {...args} />)
  ];

  for (const pattern of simplePatterns) {
    if (pattern.test(templateBody)) {
      return false; // It's a simple template
    }
  }

  // Check for hooks or complex patterns that indicate custom logic
  const customIndicators = [
    "useState",
    "useEffect",
    "useRef",
    "useCallback",
    "useMemo",
    "useContext",
    "return (", // Multi-line return with logic
  ];

  for (const indicator of customIndicators) {
    if (templateBody.includes(indicator)) {
      return true;
    }
  }

  // Check for variable declarations inside template (indicates logic)
  // But exclude the template itself
  const bodyWithoutFirstLine = templateBody.split("\n").slice(1).join("\n");
  if (/const\s+\w+\s*=/.test(bodyWithoutFirstLine)) {
    return true;
  }

  // If the template body is a single JSX element with {...args}, it's simple
  // Otherwise assume it might have complexity we can't detect
  if (templateBody.includes("{...args}") && !templateBody.includes("return")) {
    return false;
  }

  // Default: if we're not sure, treat as simple to get the args
  return false;
}

/**
 * Parse a single story's content.
 */
function parseStoryContent2(
  name: string,
  content: string,
  _componentName: string,
  _warnings: string[],
  constDeclarations: Map<string, Record<string, unknown>>
): ParsedStory | null {
  const result: ParsedStory = {
    name,
    args: {},
  };

  // Extract args
  // Pattern: args: { ... }
  const argsMatch = content.match(/args:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/);
  if (argsMatch) {
    result.args = parseArgs(argsMatch[1], constDeclarations);
  }

  // Check for custom render function
  // Note: We only mark hasCustomRender=true, we don't try to extract the render code.
  // Regex-based extraction fails on complex JSX with nested braces.
  // Stories with custom renders should be migrated manually or loaded directly via viewer.
  if (content.includes("render:") || content.includes("render(")) {
    result.hasCustomRender = true;
  }

  // Extract story description if present
  const descMatch = content.match(
    /parameters:\s*\{[^}]*docs:\s*\{[^}]*description:\s*\{[^}]*story:\s*['"`]([^'"`]+)['"`]/
  );
  if (descMatch) {
    result.description = descMatch[1];
  }

  return result;
}

/**
 * Parse args object content into a Record.
 *
 * Handles nested objects, arrays, and various value types.
 * Uses brace/bracket matching to correctly split at top-level commas.
 *
 * @param content - The string content inside the args object
 * @param constDeclarations - Optional map of const variable names to their parsed values,
 *                           used to resolve spread syntax like ...defaultArgs
 */
function parseArgs(
  content: string,
  constDeclarations?: Map<string, Record<string, unknown>>
): Record<string, unknown> {
  const args: Record<string, unknown> = {};

  // Split into key-value pairs at top-level commas only
  const pairs = splitAtTopLevelCommas(content);

  for (const pair of pairs) {
    const trimmed = pair.trim();
    if (!trimmed) continue;

    // Handle spread syntax: ...variable or ...variable.property
    if (trimmed.startsWith("...")) {
      const spreadValue = trimmed.slice(3).trim();

      // Try to resolve the spread from const declarations
      if (constDeclarations && /^\w+$/.test(spreadValue)) {
        const resolved = constDeclarations.get(spreadValue);
        if (resolved) {
          // Inline all properties from the resolved object
          for (const [key, value] of Object.entries(resolved)) {
            // Don't overwrite existing properties (spread comes first, explicit props override)
            if (!(key in args)) {
              args[key] = value;
            }
          }
          continue;
        }
      }

      // Couldn't resolve - mark as unresolvable reference
      args["__SPREAD__"] = `__REF__${spreadValue}`;
      continue;
    }

    // Handle shorthand property: just `varName` instead of `varName: varName`
    if (/^\w+$/.test(trimmed)) {
      // Variable reference shorthand - mark as unresolved reference
      args[trimmed] = `__REF__${trimmed}`;
      continue;
    }

    // Pattern: key: value
    const colonIndex = trimmed.indexOf(":");
    if (colonIndex > 0) {
      const key = trimmed.slice(0, colonIndex).trim();
      const valueStr = trimmed.slice(colonIndex + 1).trim();

      // Only parse if key is a valid identifier
      if (/^\w+$/.test(key)) {
        args[key] = parseArgValue(valueStr, constDeclarations);
      }
    }
  }

  return args;
}

/**
 * Split a string at top-level commas, respecting nested braces/brackets/parens.
 */
function splitAtTopLevelCommas(content: string): string[] {
  const parts: string[] = [];
  let current = "";
  let depth = 0;
  let inString: string | null = null;

  for (let i = 0; i < content.length; i++) {
    const char = content[i];
    const prevChar = i > 0 ? content[i - 1] : "";

    // Handle string boundaries
    if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
      if (inString === char) {
        inString = null;
      } else if (inString === null) {
        inString = char;
      }
    }

    // Track nesting depth (only when not in a string)
    if (inString === null) {
      if (char === "{" || char === "[" || char === "(") {
        depth++;
      } else if (char === "}" || char === "]" || char === ")") {
        depth--;
      } else if (char === "," && depth === 0) {
        parts.push(current);
        current = "";
        continue;
      }
    }

    current += char;
  }

  if (current.trim()) {
    parts.push(current);
  }

  return parts;
}

/**
 * Resolve JavaScript escape sequences in a string.
 * Converts \n, \t, \\, \", \', etc. to their actual characters.
 */
function resolveEscapeSequences(str: string): string {
  let result = "";
  let i = 0;

  while (i < str.length) {
    if (str[i] === "\\" && i + 1 < str.length) {
      const next = str[i + 1];
      switch (next) {
        case "n":
          result += "\n";
          i += 2;
          break;
        case "t":
          result += "\t";
          i += 2;
          break;
        case "r":
          result += "\r";
          i += 2;
          break;
        case "\\":
          result += "\\";
          i += 2;
          break;
        case '"':
          result += '"';
          i += 2;
          break;
        case "'":
          result += "'";
          i += 2;
          break;
        case "`":
          result += "`";
          i += 2;
          break;
        default:
          // Keep unrecognized escape sequences as-is
          result += str[i];
          i += 1;
      }
    } else {
      result += str[i];
      i += 1;
    }
  }

  return result;
}

/**
 * Try to parse and resolve string concatenation.
 * Returns the joined string if the value is string concatenation, null otherwise.
 *
 * Examples:
 *   "text1" + "text2" -> "text1text2"
 *   'text1' + 'text2' -> "text1text2"
 */
function tryParseStringConcatenation(value: string): string | null {
  // Quick check: must contain a + operator with quotes on both sides
  if (!value.includes("+")) {
    return null;
  }

  // Pattern for string concatenation: strings joined by +
  // Split by + but respect string boundaries
  const parts: string[] = [];
  let current = "";
  let inString: string | null = null;

  for (let i = 0; i < value.length; i++) {
    const char = value[i];
    const prevChar = i > 0 ? value[i - 1] : "";

    // Handle string boundaries
    if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
      if (inString === char) {
        inString = null;
      } else if (inString === null) {
        inString = char;
      }
    }

    // Split at + when not in a string
    if (char === "+" && inString === null) {
      const trimmed = current.trim();
      if (trimmed) {
        parts.push(trimmed);
      }
      current = "";
      continue;
    }

    current += char;
  }

  // Don't forget the last part
  const lastTrimmed = current.trim();
  if (lastTrimmed) {
    parts.push(lastTrimmed);
  }

  // Need at least 2 parts for concatenation
  if (parts.length < 2) {
    return null;
  }

  // All parts must be string literals
  const stringParts: string[] = [];
  for (const part of parts) {
    // Check if it's a quoted string
    if (
      (part.startsWith('"') && part.endsWith('"')) ||
      (part.startsWith("'") && part.endsWith("'"))
    ) {
      // Resolve escape sequences like \" \' \\ \n etc.
      stringParts.push(resolveEscapeSequences(part.slice(1, -1)));
    } else if (part.startsWith("`") && part.endsWith("`")) {
      // Template literal without interpolation
      if (!part.includes("${")) {
        stringParts.push(resolveEscapeSequences(part.slice(1, -1)));
      } else {
        return null; // Has interpolation, can't resolve statically
      }
    } else {
      // Not a string literal, can't resolve statically
      return null;
    }
  }

  // Join all string parts
  return stringParts.join("");
}

/**
 * Parse a single arg value, handling nested structures.
 *
 * @param value - The string value to parse
 * @param constDeclarations - Optional map of const variable names to their parsed values
 */
function parseArgValue(
  value: string,
  constDeclarations?: Map<string, Record<string, unknown>>
): unknown {
  value = value.trim();

  // Remove trailing comma if present
  value = value.replace(/,\s*$/, "");

  if (!value) return undefined;

  // Handle string concatenation: "text1" + "text2" + "text3"
  // This is common in Storybook for long strings
  const concatenationResult = tryParseStringConcatenation(value);
  if (concatenationResult !== null) {
    return concatenationResult;
  }

  // Handle TypeScript "as const" assertions - treat as the underlying value
  // "left" as const -> "left"
  const asConstMatch = value.match(/^(['"`])(.+?)\1\s+as\s+const$/);
  if (asConstMatch) {
    return resolveEscapeSequences(asConstMatch[2]); // Return just the string content
  }

  // String (single or double quoted)
  if (
    (value.startsWith("'") && value.endsWith("'")) ||
    (value.startsWith('"') && value.endsWith('"'))
  ) {
    return resolveEscapeSequences(value.slice(1, -1));
  }

  // Template literal - just extract the content
  if (value.startsWith("`") && value.endsWith("`")) {
    return resolveEscapeSequences(value.slice(1, -1));
  }

  // Boolean
  if (value === "true") return true;
  if (value === "false") return false;

  // Number
  if (/^-?\d+(\.\d+)?$/.test(value)) {
    return parseFloat(value);
  }

  // null/undefined
  if (value === "null") return null;
  if (value === "undefined") return undefined;

  // Nested object: { ... } - find matching braces
  if (value.startsWith("{")) {
    const closingIndex = findMatchingBrace(value, 0, "{", "}");
    if (closingIndex !== -1) {
      const inner = value.slice(1, closingIndex).trim();
      if (inner) {
        return parseArgs(inner, constDeclarations);
      }
      return {};
    }
  }

  // Array: [ ... ] - find matching brackets
  if (value.startsWith("[")) {
    const closingIndex = findMatchingBrace(value, 0, "[", "]");
    if (closingIndex !== -1) {
      const inner = value.slice(1, closingIndex).trim();
      if (!inner) return [];

      const items = splitAtTopLevelCommas(inner);
      return items
        .map((item) => parseArgValue(item.trim(), constDeclarations))
        .filter((v) => v !== undefined);
    }
  }

  // JSX element: <Component ... /> or <Component>...</Component>
  if (value.startsWith("<")) {
    const trimmed = value.trimEnd();
    if (trimmed.endsWith("/>") || trimmed.endsWith(">")) {
      return `__JSX__`;
    }
  }

  // Function call or complex expression
  if (value.includes("(") || value.includes("=>")) {
    return `__EXPR__`;
  }

  // TypeScript `as` type assertions (not `as const` which we handled above)
  // e.g., `someVar as SomeType` or `obj as Type<T>["prop"]`
  // These are expressions we can't safely convert to static JSX
  if (/\s+as\s+[A-Z]/.test(value)) {
    return `__EXPR__`;
  }

  // Variable reference (identifier) - try to resolve from constDeclarations
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value)) {
    // Check if we can resolve this variable from const declarations
    if (constDeclarations) {
      const resolved = constDeclarations.get(value);
      if (resolved !== undefined) {
        return resolved;
      }
    }
    return `__REF__${value}`;
  }

  // Property access like obj.prop or obj?.prop - try to resolve from constDeclarations
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)+$/.test(value)) {
    // Try to resolve the base variable and access the property
    if (constDeclarations) {
      const parts = value.replace(/\?/g, "").split(".");
      const baseVar = parts[0];
      const resolved = constDeclarations.get(baseVar);
      if (resolved !== undefined) {
        // Navigate through the property path
        let current: unknown = resolved;
        for (let i = 1; i < parts.length && current !== undefined; i++) {
          if (typeof current === "object" && current !== null) {
            current = (current as Record<string, unknown>)[parts[i]];
          } else {
            current = undefined;
          }
        }
        if (current !== undefined) {
          return current;
        }
      }
    }
    return `__REF__${value}`;
  }

  // Array index access with optional property chain and non-null assertions
  // Examples: arr[0], arr[0]!, obj.arr[0], mockOptions[0]!.label
  if (
    /^[a-zA-Z_$][a-zA-Z0-9_$]*(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\[\d+\]!?(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(
      value
    )
  ) {
    return `__REF__${value}`;
  }

  // TypeScript non-null assertion on variable: variable!
  if (/^[a-zA-Z_$][a-zA-Z0-9_$]*!$/.test(value)) {
    return `__REF__${value}`;
  }

  // Return as-is for other values
  return value;
}

/**
 * Find the index of the matching closing brace/bracket.
 * Returns the index of the closing char, or -1 if not found.
 */
function findMatchingBrace(
  content: string,
  startIndex: number,
  openChar: string,
  closeChar: string
): number {
  let depth = 0;
  let inString: string | null = null;

  for (let i = startIndex; i < content.length; i++) {
    const char = content[i];
    const prevChar = i > 0 ? content[i - 1] : "";

    // Handle string boundaries
    if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
      if (inString === char) {
        inString = null;
      } else if (inString === null) {
        inString = char;
      }
    }

    if (inString === null) {
      if (char === openChar) {
        depth++;
      } else if (char === closeChar) {
        depth--;
        if (depth === 0) {
          return i;
        }
      }
    }
  }

  return -1;
}

/**
 * Convert PascalCase to Title Case with spaces.
 */
export function storyNameToTitle(name: string): string {
  // Handle common patterns
  // PrimaryButton -> Primary Button
  // DisabledState -> Disabled State
  return name
    .replace(/([A-Z])/g, " $1")
    .trim()
    .replace(/\s+/g, " ");
}

/**
 * Extract category from Storybook title path.
 */
export function extractCategory(title: string): string {
  const fragments = title.split("/");

  // If we have at least 2 fragments, use the second-to-last as category
  // "Components/Forms/Input" -> "forms"
  // "Actions/Button" -> "actions"
  if (fragments.length >= 2) {
    const category = fragments[fragments.length - 2];
    return category.toLowerCase();
  }

  // Default to "components" if no category in path
  return "components";
}

// Export internal functions for testing
export const __testing = {
  parseMeta,
  parseArgTypes,
  parseArgTypeContent,
  parseStories,
  parseStoryContent2,
  parseArgs,
  parseArgsSimple,
  parseArgValue,
  extractConstDeclarations,
  extractArgsContent,
  findMatchingBrace,
  findMatchingBraceInContent,
  splitAtTopLevelCommas,
  resolveEscapeSequences,
  tryParseStringConcatenation,
  isCustomTemplate,
  calculateConfidence,
};
