#!/usr/bin/env php
<?php

/**
 * @file
 * Generates API reference Markdown from @api-annotated PHP classes.
 *
 * Uses nikic/php-parser for AST parsing and phpdocumentor/reflection-docblock
 * for docblock parsing. No Drupal bootstrap required.
 *
 * Usage:
 *   php scripts/generate-api-docs.php
 *
 * Prerequisites:
 *   composer require --dev nikic/php-parser phpdocumentor/reflection-docblock
 *
 * Output:
 *   docs/reference/api/node-processor.md
 *   docs/reference/api/orchestrator.md
 *   docs/reference/api/data-types.md
 *   docs/reference/api/exceptions.md
 */

declare(strict_types=1);

// phpcs:disable Drupal.Commenting.VariableComment.Missing
// phpcs:disable Drupal.Classes.ClassFileName.NoMatch

// ---------------------------------------------------------------------------
// 1. Find Composer autoloader
// ---------------------------------------------------------------------------
$autoloaderPaths = [
  __DIR__ . '/../vendor/autoload.php',
    // Drupal site root (module at
    // modules/custom/flowdrop/ or modules/contrib/flowdrop/).
  __DIR__ . '/../../../vendor/autoload.php',
  __DIR__ . '/../../../../vendor/autoload.php',
];

$autoloaderFound = FALSE;
foreach ($autoloaderPaths as $path) {
  if (file_exists($path)) {
    require_once $path;
    $autoloaderFound = TRUE;
    break;
  }
}

if (!$autoloaderFound) {
  fwrite(STDERR, <<<'MSG'
    Error: Could not find Composer autoloader.

    Run one of:
      composer install                (from the Drupal site root)
      composer require --dev nikic/php-parser phpdocumentor/reflection-docblock

    MSG);
  exit(1);
}

use Node\Expr\New_;
use Node\Expr\ClassConstFetch;
use Node\Expr\Array_;
use Node\Expr\ConstFetch;
use Node\Scalar\Float_;
use Node\Scalar\Int_;
use Node\Scalar\String_;
use Node\Expr;
use Node\IntersectionType;
use Node\UnionType;
use Node\NullableType;
use Node\Name;
use Node\Identifier;
use Node\Const_;
use Stmt\ClassConst;
use Stmt\ClassMethod;
use Stmt\Enum_;
use Stmt\Interface_;
use Stmt\Class_;
use Stmt\Namespace_;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\ParserFactory;
use phpDocumentor\Reflection\DocBlockFactory;

// ---------------------------------------------------------------------------
// 2. Configuration
// ---------------------------------------------------------------------------
$projectRoot = realpath(__DIR__ . '/..');
$outputDir = $projectRoot . '/docs/reference/api';

// Source directories to scan (relative to project root).
$sourceDirs = [
  'src',
  'modules/flowdrop_orchestration/src',
];

// Page definitions: key => [title, intro, namespace patterns].
$pages = [
  'node-processor' => [
    'title' => 'Node Processor Plugin API',
    'intro' => 'The FlowDropNodeProcessor plugin system is the primary extension point for FlowDrop. Use these classes to create custom node types that appear in the visual workflow editor.',
    'patterns' => [
      'Drupal\\flowdrop\\Attribute\\FlowDropNodeProcessor',
      'Drupal\\flowdrop\\Plugin\\FlowDropNodeProcessor\\',
      'Drupal\\flowdrop\\Service\\FlowDropNodeProcessorPluginManager',
    ],
    'order' => [
      'FlowDropNodeProcessor',
      'FlowDropNodeProcessorInterface',
      'AbstractFlowDropNodeProcessor',
      'NodeExecutorInterface',
      'ConfigEditProviderInterface',
      'ExecutionContextAwareInterface',
      'TriggerNodeProcessorInterface',
      'FlowDropNodeProcessorPluginManager',
    ],
    'see_also' => [
      '[Create a Node Processor](../../development/quick-reference/define-node.md) — step-by-step tutorial',
      '[Node Processor Concepts](../../development/flowdrop-node-processor.md) — deep dive into the plugin system',
      '[BC Policy](bc-policy.md) — backward compatibility guarantees',
    ],
  ],
  'orchestrator' => [
    'title' => 'Orchestrator Plugin API',
    'intro' => 'The Orchestrator plugin system controls how workflows are executed. Use these classes to create custom execution strategies (synchronous, asynchronous, stateful, etc.).',
    'patterns' => [
      'Drupal\\flowdrop_orchestration\\',
    ],
    'order' => [
      'Orchestrator',
      'OrchestratorPluginInterface',
      'OrchestratorPluginBase',
      'OrchestratorInterface',
      'InterruptExceptionInterface',
      'FlowDropInterruptException',
    ],
    'see_also' => [
      '[Workflow Snapshots & Orchestrators](../../development/workflow-snapshots-and-orchestrators.md) — concepts and usage',
      '[flowdrop_orchestration module](../../modules/flowdrop_orchestration.md) — module overview',
      '[BC Policy](bc-policy.md) — backward compatibility guarantees',
    ],
  ],
  'data-types' => [
    'title' => 'Data Types & DTOs',
    'intro' => 'Data transfer objects used throughout the FlowDrop system. These classes carry data between node processors, the runtime, and the orchestration layer.',
    'patterns' => [
      'Drupal\\flowdrop\\DTO\\',
    ],
    'order' => [
      'ParameterBagInterface',
      'ParameterBag',
      'DataInterface',
      'Data',
      'OutputInterface',
      'Output',
      'ValidationResult',
      'ValidationError',
      'ValidationWarning',
      'ExecutionContextDTO',
      'NodeMetadata',
      'NodeMetadataBuilder',
      'NodePort',
      'ConfigEdit',
      'DynamicSchemaEndpoint',
      'ExternalEditLink',
      'ConfigEditApiEndpoints',
    ],
    'see_also' => [
      '[Node Processor Plugin API](node-processor.md) — plugin interfaces that use these DTOs',
      '[BC Policy](bc-policy.md) — backward compatibility guarantees',
    ],
  ],
  'exceptions' => [
    'title' => 'Exceptions & Constants',
    'intro' => 'Exception classes for error handling and constant classes for well-known string values used across the FlowDrop system.',
    'patterns' => [
      'Drupal\\flowdrop\\Exception\\',
      'Drupal\\flowdrop\\Constants\\',
    ],
    'order' => [
      'FlowDropException',
      'NodeProcessingException',
      'EntityProcessingException',
      'InvalidNodeConfigurationException',
      'PortDirection',
      'EdgeKeys',
    ],
    'see_also' => [
      '[Node Processor Plugin API](node-processor.md) — where these exceptions are typically thrown',
      '[BC Policy](bc-policy.md) — backward compatibility guarantees',
    ],
  ],
];

// ---------------------------------------------------------------------------
// 3. AST parsing: collect @api class information
// ---------------------------------------------------------------------------

/**
 * Holds parsed class/interface info.
 */
class ClassInfo {
  public string $name = '';
  public string $namespace = '';
  public string $fqcn = '';
  /**
   * Class, interface, abstract class, trait.
   */
  public string $kind = 'class';
  public bool $isFinal = FALSE;
  public bool $isAbstract = FALSE;
  public ?string $extends = NULL;
  public array $implements = [];
  public string $summary = '';
  public string $description = '';
  public bool $isAttribute = FALSE;
  /**
   * Parsed methods.
   *
   * @var MethodInfo[]
   */
  public array $methods = [];
  /**
   * Constructor promoted properties.
   *
   * @var PropertyInfo[]
   */
  public array $constructorParams = [];
  /**
   * Parsed constants.
   *
   * @var ConstantInfo[]
   */
  public array $constants = [];
  public string $relativeFile = '';

}

/**
 * Holds parsed method info.
 */
class MethodInfo {
  public string $name = '';
  public string $visibility = 'public';
  public bool $isStatic = FALSE;
  public bool $isAbstract = FALSE;
  public string $returnType = '';
  public string $summary = '';
  /**
   * Parsed parameters.
   *
   * @var ParamInfo[]
   */
  public array $params = [];
  /**
   * Exception types thrown.
   *
   * @var string[]
   */
  public array $throws = [];

}

/**
 * Holds parsed parameter info.
 */
class ParamInfo {
  public string $name = '';
  public string $type = '';
  public ?string $default = NULL;
  public string $description = '';

}

/**
 * Holds parsed constant info.
 */
class ConstantInfo {
  public string $name = '';
  public string $type = '';
  public ?string $value = NULL;
  public string $description = '';

}

/**
 * Holds parsed property info.
 */
class PropertyInfo {
  public string $name = '';
  public string $type = '';
  public ?string $default = NULL;
  public string $description = '';
  public string $visibility = 'public';
  public bool $isReadonly = FALSE;

}

/**
 * Visitor that collects @api-annotated classes.
 */
class ApiClassCollector extends NodeVisitorAbstract {

  /**
   * Collected API classes.
   *
   * @var ClassInfo[]
   */
  public array $classes = [];

  private ?DocBlockFactory $docBlockFactory = NULL;
  private string $currentNamespace = '';
  private string $relativeFile = '';

  public function __construct(string $relativeFile) {
    $this->docBlockFactory = DocBlockFactory::createInstance();
    $this->relativeFile = $relativeFile;
  }

  /**
   * Enters a node for processing.
   */
  public function enterNode(Node $node) {
    if ($node instanceof Namespace_) {
      $this->currentNamespace = $node->name ? $node->name->toString() : '';
    }

    if ($node instanceof
    Class_          || $node instanceof
    Interface_          || $node instanceof
    Enum_) {
      $docComment = $node->getDocComment();
      if ($docComment === NULL) {
        return NULL;
      }

      $rawDoc = $docComment->getText();
      if (strpos($rawDoc, '@api') === FALSE) {
        return NULL;
      }

      $info = new ClassInfo();
      $info->name = $node->name ? $node->name->toString() : '';
      $info->namespace = $this->currentNamespace;
      $info->fqcn = $this->currentNamespace . '\\' . $info->name;
      $info->relativeFile = $this->relativeFile;

      if ($node instanceof Interface_) {
        $info->kind = 'interface';
      }
      elseif ($node instanceof Class_) {
        $info->isFinal = $node->isFinal();
        $info->isAbstract = $node->isAbstract();
        $info->kind = $info->isAbstract ? 'abstract class' : 'class';

        // Check for #[Attribute].
        foreach ($node->attrGroups as $attrGroup) {
          foreach ($attrGroup->attrs as $attr) {
            if ($attr->name->toString() === 'Attribute') {
              $info->isAttribute = TRUE;
            }
          }
        }
      }

      // Extends.
      if ($node instanceof Class_ && $node->extends) {
        $info->extends = $node->extends->toString();
      }
      if ($node instanceof Interface_ && !empty($node->extends)) {
        $info->extends = implode(', ', array_map(fn($e) => $e->toString(), $node->extends));
      }

      // Implements.
      if ($node instanceof Class_ && !empty($node->implements)) {
        $info->implements = array_map(fn($i) => $i->toString(), $node->implements);
      }

      // Parse class docblock.
      try {
        $docBlock = $this->docBlockFactory->create($rawDoc);
        $info->summary = $docBlock->getSummary();
        $desc = (string) $docBlock->getDescription();
        // Strip @see, @api, and other tags from description.
        $info->description = trim(preg_replace('/@\w+.*$/m', '', $desc));
      }
      catch (\Throwable $e) {
        // Silently skip unparseable docblocks.
      }

      // Parse methods.
      foreach ($node->stmts ?? [] as $stmt) {
        if ($stmt instanceof ClassMethod) {
          $method = $this->parseMethod($stmt, $info->kind === 'interface');
          if ($method !== NULL) {
            $info->methods[] = $method;
          }
        }
        if ($stmt instanceof ClassConst) {
          foreach ($stmt->consts as $const) {
            $constInfo = $this->parseConstant($const, $stmt);
            if ($constInfo !== NULL) {
              $info->constants[] = $constInfo;
            }
          }
        }
      }

      // Extract constructor promoted properties.
      foreach ($node->stmts ?? [] as $stmt) {
        if ($stmt instanceof ClassMethod && $stmt->name->toString() === '__construct') {
          foreach ($stmt->params as $param) {
            if ($param->flags !== 0) {
              // Promoted property.
              $prop = new PropertyInfo();
              $prop->name = '$' . $param->var->name;
              $prop->type = $this->resolveType($param->type);
              $prop->default = $this->resolveDefault($param->default);
              $prop->isReadonly = (bool) ($param->flags & Class_::MODIFIER_READONLY);
              if ($param->flags & Class_::MODIFIER_PUBLIC) {
                $prop->visibility = 'public';
              }
              elseif ($param->flags & Class_::MODIFIER_PROTECTED) {
                $prop->visibility = 'protected';
              }
              else {
                $prop->visibility = 'private';
              }
              $info->constructorParams[] = $prop;
            }
          }
          // Parse constructor docblock for param descriptions.
          $constructorDoc = $stmt->getDocComment();
          if ($constructorDoc) {
            try {
              $docBlock = $this->docBlockFactory->create($constructorDoc->getText());
              $paramTags = $docBlock->getTagsByName('param');
              foreach ($paramTags as $tag) {
                $paramName = '$' . $tag->getVariableName();
                foreach ($info->constructorParams as $prop) {
                  if ($prop->name === $paramName) {
                    $prop->description = (string) $tag->getDescription();
                  }
                }
              }
            }
            catch (\Throwable $e) {
              // Skip.
            }
          }
        }
      }

      $this->classes[] = $info;
    }

    return NULL;
  }

  /**
   * Parses a class method.
   */
  private function parseMethod(ClassMethod $method, bool $isInterface): ?MethodInfo {
    $name = $method->name->toString();

    // Skip magic methods except __construct
    // (handled separately for promoted props).
    if (str_starts_with($name, '__')) {
      return NULL;
    }

    // For classes, only include public and protected methods.
    $visibility = 'public';
    if ($method->isProtected()) {
      $visibility = 'protected';
    }
    elseif ($method->isPrivate()) {
      // Skip private.
      return NULL;
    }

    $info = new MethodInfo();
    $info->name = $name;
    $info->visibility = $visibility;
    $info->isStatic = $method->isStatic();
    $info->isAbstract = $method->isAbstract();
    $info->returnType = $this->resolveType($method->returnType);

    // Parse parameters.
    foreach ($method->params as $param) {
      $p = new ParamInfo();
      $p->name = '$' . $param->var->name;
      $p->type = $this->resolveType($param->type);
      $p->default = $this->resolveDefault($param->default);
      $info->params[] = $p;
    }

    // Parse docblock.
    $docComment = $method->getDocComment();
    if ($docComment) {
      try {
        $docBlock = $this->docBlockFactory->create($docComment->getText());
        $summary = $docBlock->getSummary();
        // If summary is just {@inheritdoc}, skip.
        if (stripos($summary, '{@inheritdoc}') !== FALSE) {
          // Try description for more info.
          $desc = (string) $docBlock->getDescription();
          if ($desc && stripos($desc, '{@inheritdoc}') === FALSE) {
            $info->summary = trim($desc);
          }
        }
        else {
          $info->summary = $summary;
        }

        // Enrich param descriptions.
        $paramTags = $docBlock->getTagsByName('param');
        foreach ($paramTags as $tag) {
          $tagName = '$' . $tag->getVariableName();
          foreach ($info->params as $p) {
            if ($p->name === $tagName) {
              $p->description = (string) $tag->getDescription();
              // If AST didn't have a type, use docblock type.
              if ($p->type === '' && $tag->getType()) {
                $p->type = (string) $tag->getType();
              }
            }
          }
        }

        // Return type from docblock if not in AST.
        if ($info->returnType === '') {
          $returnTags = $docBlock->getTagsByName('return');
          foreach ($returnTags as $tag) {
            $info->returnType = (string) $tag->getType();
          }
        }

        // Throws.
        $throwsTags = $docBlock->getTagsByName('throws');
        foreach ($throwsTags as $tag) {
          $info->throws[] = (string) $tag->getType();
        }
      }
      catch (\Throwable $e) {
        // Skip.
      }
    }

    return $info;
  }

  /**
   * Parses a class constant.
   */
  private function parseConstant(Const_ $const, ClassConst $parent): ?ConstantInfo {
    // Only include public constants.
    if ($parent->isPrivate() || $parent->isProtected()) {
      return NULL;
    }

    $info = new ConstantInfo();
    $info->name = $const->name->toString();
    $info->value = $this->resolveDefault($const->value);

    $docComment = $parent->getDocComment();
    if ($docComment) {
      try {
        $docBlock = $this->docBlockFactory->create($docComment->getText());
        $info->description = $docBlock->getSummary();
      }
      catch (\Throwable $e) {
        // Skip.
      }
    }

    return $info;
  }

  /**
   * Resolves a type node to string.
   */
  private function resolveType(?Node $type): string {
    if ($type === NULL) {
      return '';
    }
    if ($type instanceof Identifier) {
      return $type->toString();
    }
    if ($type instanceof Name) {
      return $type->toString();
    }
    if ($type instanceof NullableType) {
      return '?' . $this->resolveType($type->type);
    }
    if ($type instanceof UnionType) {
      return implode('|', array_map([$this, 'resolveType'], $type->types));
    }
    if ($type instanceof IntersectionType) {
      return implode('&', array_map([$this, 'resolveType'], $type->types));
    }
    return (string) $type;
  }

  /**
   * Resolves a default value expression to string.
   */
  private function resolveDefault(?Expr $expr): ?string {
    if ($expr === NULL) {
      return NULL;
    }
    if ($expr instanceof String_) {
      return '"' . $expr->value . '"';
    }
    if ($expr instanceof Int_) {
      return (string) $expr->value;
    }
    if ($expr instanceof Float_) {
      return (string) $expr->value;
    }
    if ($expr instanceof ConstFetch) {
      return $expr->name->toString();
    }
    if ($expr instanceof Array_) {
      return '[]';
    }
    if ($expr instanceof ClassConstFetch) {
      $class = $expr->class instanceof Name ? $expr->class->toString() : (string) $expr->class;
      $const = $expr->name instanceof Identifier ? $expr->name->toString() : (string) $expr->name;
      return $class . '::' . $const;
    }
    if ($expr instanceof New_) {
      if ($expr->class instanceof Name) {
        return 'new ' . $expr->class->toString() . '(...)';
      }
    }
    return '...';
  }

}

// ---------------------------------------------------------------------------
// 4. Scan and parse files
// ---------------------------------------------------------------------------
$parser = (new ParserFactory())->createForHostVersion();
$allClasses = [];

foreach ($sourceDirs as $dir) {
  $fullDir = $projectRoot . '/' . $dir;
  if (!is_dir($fullDir)) {
    fwrite(STDERR, "Warning: directory not found: {$dir}\n");
    continue;
  }

  $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($fullDir, FilesystemIterator::SKIP_DOTS)
    );

  foreach ($iterator as $file) {
    if ($file->getExtension() !== 'php') {
      continue;
    }

    $filePath = $file->getPathname();
    $relativeFile = str_replace($projectRoot . '/', '', $filePath);
    $code = file_get_contents($filePath);

    // Quick pre-filter: skip files without @api.
    if (strpos($code, '@api') === FALSE) {
      continue;
    }

    try {
      $ast = $parser->parse($code);
    }
    catch (\Throwable $e) {
      fwrite(STDERR, "Warning: parse error in {$relativeFile}: {$e->getMessage()}\n");
      continue;
    }

    $traverser = new NodeTraverser();
    $collector = new ApiClassCollector($relativeFile);
    $traverser->addVisitor($collector);
    $traverser->traverse($ast);

    foreach ($collector->classes as $classInfo) {
      $allClasses[$classInfo->fqcn] = $classInfo;
    }
  }
}

fprintf(STDERR, "Found %d @api-annotated classes.\n", count($allClasses));

// ---------------------------------------------------------------------------
// 5. Group classes into pages
// ---------------------------------------------------------------------------

/**
 * Determines which page a class belongs to.
 *
 * @return string|null
 *   The page key, or NULL if no match found.
 */
function classify_class(ClassInfo $class, array $pages): ?string {
  foreach ($pages as $pageKey => $pageDef) {
    foreach ($pageDef['patterns'] as $pattern) {
      if ($class->fqcn === $pattern || str_starts_with($class->fqcn, $pattern)) {
        return $pageKey;
      }
    }
  }
  return NULL;
}

$grouped = [];
$unmatched = [];
foreach ($allClasses as $fqcn => $classInfo) {
  $page = classify_class($classInfo, $pages);
  if ($page !== NULL) {
    $grouped[$page][$classInfo->name] = $classInfo;
  }
  else {
    $unmatched[] = $fqcn;
  }
}

if (!empty($unmatched)) {
  fprintf(STDERR, "Warning: %d @api classes not matched to any page:\n", count($unmatched));
  foreach ($unmatched as $fqcn) {
    fprintf(STDERR, "  - %s\n", $fqcn);
  }
}

// Sort classes within each page by the defined order.
foreach ($pages as $pageKey => $pageDef) {
  if (!isset($grouped[$pageKey])) {
    continue;
  }
  $order = $pageDef['order'] ?? [];
  uksort($grouped[$pageKey], function (string $a, string $b) use ($order): int {
      $posA = array_search($a, $order);
      $posB = array_search($b, $order);
    if ($posA === FALSE) {
      $posA = 999;
    }
    if ($posB === FALSE) {
      $posB = 999;
    }
      return $posA - $posB;
  });
}

// ---------------------------------------------------------------------------
// 6. Generate Markdown
// ---------------------------------------------------------------------------

/**
 * Extracts the short class name from a fully qualified class name.
 *
 * @return string
 *   The short class name.
 */
function short_name(string $fqcn): string {
  $parts = explode('\\', $fqcn);
  return end($parts);
}

/**
 * Escapes pipe characters for use in Markdown tables.
 *
 * @return string
 *   The escaped string.
 */
function escape_markdown(string $text): string {
  return str_replace('|', '\\|', $text);
}

/**
 * Collapses multi-line text into a single line for use in table cells.
 *
 * @return string
 *   The collapsed and escaped string.
 */
function table_safe(string $text): string {
  $text = trim($text);
  // Replace newlines with spaces, collapse multiple spaces.
  $text = preg_replace('/\s*\n\s*/', ' ', $text);
  $text = preg_replace('/\s{2,}/', ' ', $text);
  return escape_markdown($text);
}

/**
 * Generates Markdown documentation for a single class.
 *
 * @return string
 *   The generated Markdown string.
 */
function generate_class_markdown(ClassInfo $class): string {
  $md = '';

  // Class heading.
  $label = $class->name;
  if ($class->isAttribute) {
    $label .= ' (Attribute)';
  }
  $md .= "## {$label}\n\n";

  // Metadata.
  $md .= "**Namespace:** `{$class->namespace}`  \n";
  $type = $class->kind;
  if ($class->isFinal) {
    $type = 'final ' . $type;
  }
  $md .= "**Type:** {$type}  \n";
  if ($class->extends) {
    $md .= "**Extends:** `" . short_name($class->extends) . "`  \n";
  }
  if (!empty($class->implements)) {
    $names = array_map(fn($i) => '`' . short_name($i) . '`', $class->implements);
    $md .= "**Implements:** " . implode(', ', $names) . "  \n";
  }
  $md .= "**Source:** `{$class->relativeFile}`  \n";
  $md .= "\n";

  // Summary.
  if ($class->summary) {
    $md .= "{$class->summary}\n\n";
  }
  if ($class->description) {
    $md .= "{$class->description}\n\n";
  }

  // Constructor promoted properties (for DTOs and attributes).
  if (!empty($class->constructorParams)) {
    $md .= "### Constructor Parameters\n\n";
    $md .= "| Parameter | Type | Default | Description |\n";
    $md .= "|-----------|------|---------|-------------|\n";
    foreach ($class->constructorParams as $prop) {
      $type = $prop->type ?: 'mixed';
      $default = $prop->default ?? '—';
      $desc = table_safe($prop->description);
      $md .= "| `{$prop->name}` | `{$type}` | {$default} | {$desc} |\n";
    }
    $md .= "\n";
  }

  // Constants.
  if (!empty($class->constants)) {
    $md .= "### Constants\n\n";
    $md .= "| Constant | Value | Description |\n";
    $md .= "|----------|-------|-------------|\n";
    foreach ($class->constants as $const) {
      $value = $const->value ?? '—';
      $desc = table_safe($const->description);
      $md .= "| `{$const->name}` | {$value} | {$desc} |\n";
    }
    $md .= "\n";
  }

  // Methods.
  if (!empty($class->methods)) {
    $md .= "### Methods\n\n";
    foreach ($class->methods as $method) {
      $md .= generate_method_markdown($method);
    }
  }

  $md .= "---\n\n";
  return $md;
}

/**
 * Generates Markdown documentation for a single method.
 *
 * @return string
 *   The generated Markdown string.
 */
function generate_method_markdown(MethodInfo $method): string {
  $md = '';

  // Method signature.
  $prefix = '';
  if ($method->visibility !== 'public') {
    $prefix .= $method->visibility . ' ';
  }
  if ($method->isStatic) {
    $prefix .= 'static ';
  }

  $params = [];
  foreach ($method->params as $p) {
    $sig = '';
    if ($p->type) {
      $sig .= $p->type . ' ';
    }
    $sig .= $p->name;
    if ($p->default !== NULL) {
      $sig .= ' = ' . $p->default;
    }
    $params[] = $sig;
  }
  $paramStr = implode(', ', $params);
  $returnStr = $method->returnType ? ': ' . $method->returnType : '';

  $md .= "#### `{$prefix}{$method->name}({$paramStr}){$returnStr}`\n\n";

  // Summary.
  if ($method->summary) {
    $md .= "{$method->summary}\n\n";
  }

  // Parameter details (if any have descriptions).
  $hasDescriptions = FALSE;
  foreach ($method->params as $p) {
    if ($p->description) {
      $hasDescriptions = TRUE;
      break;
    }
  }
  if ($hasDescriptions) {
    $md .= "| Parameter | Type | Description |\n";
    $md .= "|-----------|------|-------------|\n";
    foreach ($method->params as $p) {
      $type = $p->type ?: 'mixed';
      $desc = table_safe($p->description);
      $md .= "| `{$p->name}` | `{$type}` | {$desc} |\n";
    }
    $md .= "\n";
  }

  // Throws.
  if (!empty($method->throws)) {
    $throwsList = array_map(fn($t) => '`' . short_name($t) . '`', $method->throws);
    $md .= "**Throws:** " . implode(', ', $throwsList) . "\n\n";
  }

  return $md;
}

/**
 * Generates a complete Markdown page for a group of classes.
 *
 * @return string
 *   The generated Markdown page content.
 */
function generate_page_markdown(string $title, string $intro, array $classes, array $seeAlso): string {
  $md = "# {$title}\n\n";
  $md .= "!!! info \"Stable API\"\n";
  $md .= "    Classes on this page are marked `@api` and are guaranteed backward compatible in the 1.x release line.\n";
  $md .= "    See the [BC Policy](bc-policy.md) for details.\n\n";
  $md .= "{$intro}\n\n";

  // Table of contents.
  if (count($classes) > 3) {
    $md .= "## Overview\n\n";
    $md .= "| Class | Type | Description |\n";
    $md .= "|-------|------|-------------|\n";
    foreach ($classes as $class) {
      $anchor = strtolower(str_replace(' ', '-', $class->name));
      if ($class->isAttribute) {
        $anchor .= '-attribute';
      }
      $kind = $class->kind;
      if ($class->isFinal) {
        $kind = 'final ' . $kind;
      }
      $summary = table_safe($class->summary);
      $md .= "| [`{$class->name}`](#{$anchor}) | {$kind} | {$summary} |\n";
    }
    $md .= "\n";
  }

  // Class details.
  foreach ($classes as $class) {
    $md .= generate_class_markdown($class);
  }

  // See also.
  if (!empty($seeAlso)) {
    $md .= "## See Also\n\n";
    foreach ($seeAlso as $link) {
      $md .= "- {$link}\n";
    }
    $md .= "\n";
  }

  // Footer.
  $md .= "---\n\n";
  $md .= "*This page was auto-generated from source code PHPDoc annotations. ";
  $md .= "Run `php scripts/generate-api-docs.php` to regenerate.*\n";

  return $md;
}

// ---------------------------------------------------------------------------
// 7. Write output files
// ---------------------------------------------------------------------------
if (!is_dir($outputDir)) {
  mkdir($outputDir, 0755, TRUE);
}

$totalClasses = 0;
foreach ($pages as $pageKey => $pageDef) {
  $classes = $grouped[$pageKey] ?? [];
  if (empty($classes)) {
    fprintf(STDERR, "Warning: no classes for page '%s'\n", $pageKey);
    continue;
  }

  $markdown = generate_page_markdown(
        $pageDef['title'],
        $pageDef['intro'],
        $classes,
        $pageDef['see_also'] ?? []
    );

  $outputFile = $outputDir . '/' . $pageKey . '.md';
  file_put_contents($outputFile, $markdown);
  fprintf(STDERR, "  Written: %s (%d classes)\n", $pageKey . '.md', count($classes));
  $totalClasses += count($classes);
}

fprintf(STDERR, "\nDone. Generated %d pages covering %d @api classes.\n", count($pages), $totalClasses);
fprintf(STDERR, "Output directory: %s\n", $outputDir);
