<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Service;

use Drupal\flowdrop_workflow\Diagnostic\DiagnosticContext;
use Drupal\flowdrop_workflow\Diagnostic\DoctorReport;
use Drupal\flowdrop_workflow\Diagnostic\RemedyPlan;
use Drupal\flowdrop_workflow\Diagnostic\RemedySelection;
use Drupal\flowdrop_workflow\Diagnostic\WorkflowDiagnosticInterface;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use Psr\Log\LoggerInterface;

/**
 * Default WorkflowDoctor implementation.
 *
 * @internal
 */
final class WorkflowDoctor implements WorkflowDoctorInterface {

  /**
   * The registered diagnostics.
   *
   * @var list<\Drupal\flowdrop_workflow\Diagnostic\WorkflowDiagnosticInterface>
   */
  private readonly array $diagnostics;

  /**
   * Diagnostics indexed by their id.
   *
   * @var array<string, \Drupal\flowdrop_workflow\Diagnostic\WorkflowDiagnosticInterface>
   */
  private readonly array $diagnosticsById;

  /**
   * Constructs a WorkflowDoctor.
   *
   * @param iterable<\Drupal\flowdrop_workflow\Diagnostic\WorkflowDiagnosticInterface> $diagnostics
   *   The tagged diagnostics.
   * @param \Drupal\flowdrop_workflow\Service\WorkflowMutatorInterface $mutator
   *   The only component that mutates the workflow's stored arrays.
   * @param \Drupal\flowdrop_workflow\Service\WorkflowValidatorInterface $validator
   *   Re-validates after applying remedies (for logging the residual state).
   * @param \Psr\Log\LoggerInterface $logger
   *   The flowdrop_workflow logger channel.
   */
  public function __construct(
    iterable $diagnostics,
    private readonly WorkflowMutatorInterface $mutator,
    private readonly WorkflowValidatorInterface $validator,
    private readonly LoggerInterface $logger,
  ) {
    $list = $diagnostics instanceof \Traversable
      ? iterator_to_array($diagnostics, FALSE)
      : array_values($diagnostics);
    $this->diagnostics = $list;

    $byId = [];
    foreach ($list as $diagnostic) {
      $byId[$diagnostic->id()] = $diagnostic;
    }
    $this->diagnosticsById = $byId;
  }

  /**
   * {@inheritdoc}
   */
  public function diagnose(WorkflowDefinitionInterface $workflow): DoctorReport {
    $context = new DiagnosticContext($workflow);
    $findings = [];
    foreach ($this->diagnostics as $diagnostic) {
      foreach ($diagnostic->diagnose($context) as $finding) {
        $findings[] = $finding;
      }
    }
    return new DoctorReport($findings);
  }

  /**
   * {@inheritdoc}
   */
  public function previewRemedies(WorkflowDefinitionInterface $workflow, array $selections): RemedyPlan {
    $context = new DiagnosticContext($workflow);
    $report = $this->diagnose($workflow);

    $changes = [];
    $skipped = [];
    $destructive = FALSE;

    foreach ($selections as $selection) {
      $mutations = $this->resolveSelection($selection, $report, $context, $skipped);
      if ($mutations === []) {
        continue;
      }
      $finding = $report->findingById($selection->findingId);
      if ($finding?->remedy($selection->remedyId)?->destructive) {
        $destructive = TRUE;
      }
      foreach ($mutations as $mutation) {
        $changes[] = $mutation->summary();
      }
    }

    return new RemedyPlan($changes, $destructive, $skipped);
  }

  /**
   * {@inheritdoc}
   */
  public function applyRemedies(WorkflowDefinitionInterface $workflow, array $selections): DoctorReport {
    $context = new DiagnosticContext($workflow);
    $report = $this->diagnose($workflow);

    $mutations = [];
    $skipped = [];
    foreach ($selections as $selection) {
      foreach ($this->resolveSelection($selection, $report, $context, $skipped) as $mutation) {
        $mutations[] = $mutation;
      }
    }

    if ($skipped !== []) {
      $this->logger->notice('Workflow doctor skipped @count selection(s) on "@id" whose findings were no longer present.', [
        '@count' => count($skipped),
        '@id' => (string) $workflow->id(),
      ]);
    }

    if ($mutations === []) {
      return $report;
    }

    $this->mutator->apply($workflow, $mutations);

    // Re-validate purely to record the residual state: the author may have
    // remedied only some findings, so remaining errors must not block saving
    // the partial repair of an already-stored (and already-broken) workflow.
    $result = $this->validator->validate(
      $workflow->getNodes(),
      $workflow->getEdges(),
      $workflow->getInputPorts(),
      $workflow->getOutputPorts(),
    );
    if (!$result->isValid()) {
      $this->logger->info('Workflow "@id" still has @count validation error(s) after applying remedies; saving the partial fix.', [
        '@id' => (string) $workflow->id(),
        '@count' => count($result->getErrors()),
      ]);
    }

    $workflow->save();

    return $this->diagnose($workflow);
  }

  /**
   * Resolves one selection to its server-rebuilt mutations.
   *
   * Matches the selection to a still-present finding by stable id and asks the
   * owning diagnostic to rebuild the mutations. A selection whose finding
   * vanished, or whose diagnostic is unknown, is recorded as skipped.
   *
   * @param \Drupal\flowdrop_workflow\Diagnostic\RemedySelection $selection
   *   The user's choice.
   * @param \Drupal\flowdrop_workflow\Diagnostic\DoctorReport $report
   *   A fresh diagnosis to match against.
   * @param \Drupal\flowdrop_workflow\Diagnostic\DiagnosticContext $context
   *   The working set for rebuilding mutations.
   * @param list<string> $skipped
   *   Accumulator of skipped finding ids, by reference.
   *
   * @return list<\Drupal\flowdrop_workflow\Diagnostic\Mutation\WorkflowMutationInterface>
   *   The mutations for this selection (empty when skipped or a no-op).
   */
  private function resolveSelection(RemedySelection $selection, DoctorReport $report, DiagnosticContext $context, array &$skipped): array {
    $finding = $report->findingById($selection->findingId);
    if ($finding === NULL) {
      $skipped[] = $selection->findingId;
      return [];
    }
    $diagnostic = $this->diagnosticsById[$finding->diagnosticId] ?? NULL;
    if (!$diagnostic instanceof WorkflowDiagnosticInterface) {
      $skipped[] = $selection->findingId;
      return [];
    }
    return $diagnostic->buildMutations($finding, $selection->remedyId, $selection->params, $context);
  }

}
