<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Service;

use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Psr\Log\LoggerInterface;

/**
 * Rebuilds and versions a workflow's schema snapshot on save.
 *
 * Owns the rebuild-and-bump pipeline that used to live in
 * {@see \Drupal\flowdrop_workflow\Entity\FlowDropWorkflow::preSave()}.
 * Pulling it out of the entity means the entity no longer reaches into the
 * container via the service locator, and the pipeline is reachable from
 * tests, drush commands, and post_update hooks the same way.
 *
 * Trigger semantics match the previous in-entity logic:
 *  - If the new input/output port lists are deep-equal to the original
 *    entity's, the call is a no-op (snapshot is already current).
 *  - The drift banner's "Refresh schema" flag forces a rebuild regardless.
 *  - {@see FlowDropWorkflowInterface::isSyncing()} short-circuits the whole
 *    pipeline so config imports keep their stamped snapshot + version
 *    verbatim.
 *
 * @internal
 */
final class WorkflowSchemaSnapshotter implements WorkflowSchemaSnapshotterInterface {

  public function __construct(
    private readonly WorkflowSchemaBuilderInterface $builder,
    private readonly WorkflowSchemaDifferInterface $differ,
    private readonly LoggerInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function rebuildIfNeeded(
    FlowDropWorkflowInterface $workflow,
    ?FlowDropWorkflowInterface $original,
    bool $forced,
  ): void {
    if ($workflow->isSyncing()) {
      return;
    }

    if (!$forced && $original !== NULL) {
      $inputUnchanged = $workflow->getInputPorts() === $original->getInputPorts();
      $outputUnchanged = $workflow->getOutputPorts() === $original->getOutputPorts();
      if ($inputUnchanged && $outputUnchanged) {
        return;
      }
    }

    $previousParameterSchema = $original?->getParameterSchema();
    $previousOutputSchema = $original?->getOutputSchema();
    $previousVersion = $original?->getSchemaVersion() ?? '0.0.0';

    try {
      $newParameterSchema = $workflow->getInputPorts() === []
        ? NULL
        : $this->builder->build($workflow, $workflow->getInputPorts(), 'input');
      $newOutputSchema = $workflow->getOutputPorts() === []
        ? NULL
        : $this->builder->build($workflow, $workflow->getOutputPorts(), 'output');
    }
    catch (\Throwable $e) {
      $this->logger->error('Schema rebuild failed for workflow @workflow_id: @class @message', [
        '@workflow_id' => $workflow->id() ?? '(new)',
        '@class' => $e::class,
        '@message' => $e->getMessage(),
      ]);
      // Preserve previous snapshot; let the save proceed.
      return;
    }

    $diff = $this->differ->diff(
      $previousParameterSchema, $newParameterSchema,
      $previousOutputSchema, $newOutputSchema,
    );

    $workflow->setParameterSchema($newParameterSchema);
    $workflow->setOutputSchema($newOutputSchema);

    $newVersion = $this->bumpVersion($previousVersion, $diff['level']);
    if ($newVersion !== $previousVersion) {
      $workflow->setSchemaVersion($newVersion);
      $this->logger->notice('Workflow @workflow_id schema rebuilt: @old -> @new (@level)', [
        '@workflow_id' => $workflow->id() ?? '(new)',
        '@old' => $previousVersion,
        '@new' => $newVersion,
        '@level' => $diff['level'],
      ]);
    }
    else {
      $this->logger->debug('Workflow @workflow_id schema rebuilt with no version change.', [
        '@workflow_id' => $workflow->id() ?? '(new)',
      ]);
    }
  }

  /**
   * Bumps a semver string according to the differ's classification.
   *
   * 0.0.0 → 1.0.0 on first declaration regardless of level — minor or major
   * changes against an undeclared schema both produce the initial 1.0.0.
   */
  private function bumpVersion(string $previous, string $level): string {
    if (!preg_match('/^(\d+)\.(\d+)\.(\d+)$/', $previous, $m)) {
      $major = $minor = $patch = 0;
    }
    else {
      [, $major, $minor, $patch] = $m;
      $major = (int) $major;
      $minor = (int) $minor;
      $patch = (int) $patch;
    }

    if ($level === 'major') {
      return $previous === '0.0.0' ? '1.0.0' : ($major + 1) . '.0.0';
    }
    if ($level === 'minor') {
      if ($previous === '0.0.0') {
        return '1.0.0';
      }
      return $major . '.' . ($minor + 1) . '.0';
    }
    return $previous;
  }

}
