<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\flowdrop_workflow\Diagnostic\DoctorReport;
use Drupal\flowdrop_workflow\Diagnostic\Finding;
use Drupal\flowdrop_workflow\Diagnostic\RemedySelection;
use Drupal\flowdrop_workflow\Diagnostic\Severity;
use Drupal\flowdrop_workflow\Service\WorkflowDoctorInterface;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Two-step admin screen for diagnosing and repairing a stored workflow.
 *
 * Step "review" runs {@see WorkflowDoctorInterface::diagnose()} and renders the
 * findings grouped by severity, each with a radio set of remedies (defaulting
 * to "Leave as is"). Submitting collects the picks as
 * {@see RemedySelection}s — the only thing the client sends — and rebuilds into
 * step "confirm".
 *
 * Step "confirm" is the interstitial: it renders
 * {@see WorkflowDoctorInterface::previewRemedies()} (an explicit list of what
 * will change, derived server-side from the selections), and on
 * confirmation calls {@see WorkflowDoctorInterface::applyRemedies()} — which
 * re-diagnoses and rebuilds the mutations from scratch, closing the
 * time-of-check/time-of-use gap. "Back" returns to review with the picks
 * preserved. Selections live in $form_state (small, per-entity) so no tempstore
 * is needed.
 *
 * @internal
 */
final class WorkflowDoctorForm extends FormBase {

  /**
   * The radio value standing for "do nothing about this finding".
   */
  private const LEAVE = '__leave';

  /**
   * Constructs a WorkflowDoctorForm.
   *
   * The doctor is held in a protected, non-readonly property on purpose: a
   * multistep form is serialized into the form cache between the review and
   * confirm requests, and DependencySerializationTrait's __sleep()/__wakeup()
   * run in the FormBase scope — they can only see and restore this injected
   * service when the property is visible there (protected, not
   * subclass-private) and writable on wake-up (not readonly).
   *
   * @param \Drupal\flowdrop_workflow\Service\WorkflowDoctorInterface $doctor
   *   The headless diagnose/preview/apply service.
   */
  public function __construct(
    protected WorkflowDoctorInterface $doctor,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('flowdrop_workflow.doctor'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'flowdrop_workflow_doctor';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, ?WorkflowDefinitionInterface $flowdrop_workflow = NULL): array {
    $workflow = $flowdrop_workflow ?? $this->workflowFromRoute();
    if (!$workflow instanceof WorkflowDefinitionInterface) {
      throw new \LogicException('The workflow doctor form requires a workflow route parameter.');
    }

    return $form_state->get('step') === 'confirm'
      ? $this->buildConfirmStep($form, $form_state, $workflow)
      : $this->buildReviewStep($form, $form_state, $workflow);
  }

  /**
   * Builds the review step: findings grouped by severity, each with remedies.
   */
  private function buildReviewStep(array $form, FormStateInterface $form_state, WorkflowDefinitionInterface $workflow): array {
    $report = $this->doctor->diagnose($workflow);

    if ($report->isHealthy()) {
      $form['healthy'] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['messages', 'messages--status']],
        'message' => [
          '#markup' => $this->t('This workflow is healthy. No problems were found.'),
        ],
      ];
      return $form;
    }

    $form['intro'] = [
      '#markup' => '<p>' . $this->t('The doctor found problems with the stored workflow. Choose a remedy for each, then review the exact changes before they are applied.') . '</p>',
    ];

    // Picks preserved across a "Back" from the confirm step.
    $defaults = $form_state->get('choices') ?? [];

    $form['findings'] = ['#tree' => TRUE];
    foreach ([Severity::Error, Severity::Warning, Severity::Info] as $severity) {
      $findings = $report->bySeverity($severity);
      if ($findings === []) {
        continue;
      }
      $group = $severity->value;
      $form['findings'][$group] = [
        '#type' => 'details',
        '#title' => $this->severityHeading($severity, count($findings)),
        '#open' => TRUE,
      ];
      foreach ($findings as $finding) {
        $form['findings'][$group][$finding->id()] = $this->findingElement($finding, $defaults[$finding->id()] ?? []);
      }
    }

    $form['actions'] = ['#type' => 'actions'];
    $form['actions']['review'] = [
      '#type' => 'submit',
      '#value' => $this->t('Review changes'),
      '#submit' => ['::reviewSubmit'],
    ];

    return $form;
  }

  /**
   * Builds the render array for one finding and its remedy choices.
   *
   * @param \Drupal\flowdrop_workflow\Diagnostic\Finding $finding
   *   The finding.
   * @param array<string, mixed> $defaults
   *   Preserved picks for this finding: ['choice' => …, 'node_type' => …].
   *
   * @return array<string, mixed>
   *   The fieldset element.
   */
  private function findingElement(Finding $finding, array $defaults): array {
    $element = [
      '#type' => 'fieldset',
      '#title' => $finding->summary,
    ];
    $element['locator'] = [
      '#markup' => '<p class="description">' . $this->t('Affected: @locator', ['@locator' => $this->humanLocator($finding->locator)]) . '</p>',
    ];

    if ($finding->remedies === []) {
      $element['none'] = [
        '#markup' => '<p>' . $this->t('No automatic remedy is available yet. Fix this in the editor.') . '</p>',
      ];
      return $element;
    }

    $options = [self::LEAVE => $this->t('Leave as is')];
    $descriptions = [];
    $replaceChoices = [];
    foreach ($finding->remedies as $remedy) {
      $label = $remedy->destructive
        ? $this->t('@label (destructive)', ['@label' => $remedy->label])
        : $remedy->label;
      $options[$remedy->id] = $label;
      $descriptions[$remedy->id] = $remedy->description;
      if ($remedy->id === 'replace_node_type' && is_array($remedy->params['choices'] ?? NULL)) {
        $replaceChoices = $remedy->params['choices'];
      }
    }

    $element['choice'] = [
      '#type' => 'radios',
      '#title' => $this->t('Remedy'),
      '#options' => $options,
      '#default_value' => $defaults['choice'] ?? self::LEAVE,
      '#description' => $this->remedyDescriptionList($descriptions),
    ];

    if ($replaceChoices !== []) {
      $radioName = 'findings[' . $finding->severity->value . '][' . $finding->id() . '][choice]';
      $element['node_type'] = [
        '#type' => 'select',
        '#title' => $this->t('Replacement node type'),
        '#options' => $replaceChoices,
        '#default_value' => $defaults['node_type'] ?? NULL,
        '#empty_option' => $this->t('- Select -'),
        '#states' => [
          'visible' => [
            ':input[name="' . $radioName . '"]' => ['value' => 'replace_node_type'],
          ],
        ],
      ];
    }

    return $element;
  }

  /**
   * Builds the confirm step: a preview of exactly what applying would change.
   */
  private function buildConfirmStep(array $form, FormStateInterface $form_state, WorkflowDefinitionInterface $workflow): array {
    $selections = $this->selectionsFromState($form_state);
    $plan = $this->doctor->previewRemedies($workflow, $selections);

    $form['heading'] = [
      '#markup' => '<h2>' . $this->t('Confirm changes') . '</h2>',
    ];

    if (!$plan->hasChanges()) {
      $form['none'] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['messages', 'messages--warning']],
        'message' => [
          '#markup' => $this->t('Nothing will be changed. Every finding was left as is, or its problem is no longer present.'),
        ],
      ];
    }
    else {
      if ($plan->isDestructive()) {
        $form['warning'] = [
          '#type' => 'container',
          '#attributes' => ['class' => ['messages', 'messages--warning']],
          'message' => [
            '#markup' => $this->t('Some changes remove nodes or edges. This cannot be undone.'),
          ],
        ];
      }
      $form['changes'] = [
        '#theme' => 'item_list',
        '#title' => $this->t('The following changes will be made:'),
        '#items' => $plan->changes(),
      ];
    }

    if ($plan->skipped() !== []) {
      $form['skipped'] = [
        '#markup' => '<p class="description">' . $this->formatPlural(
          count($plan->skipped()),
          '1 selection was skipped because its problem is no longer present.',
          '@count selections were skipped because their problems are no longer present.',
        ) . '</p>',
      ];
    }

    $form['actions'] = ['#type' => 'actions'];
    $form['actions']['apply'] = [
      '#type' => 'submit',
      '#value' => $this->t('Apply changes'),
      '#submit' => ['::applySubmit'],
      '#disabled' => !$plan->hasChanges(),
      '#button_type' => 'primary',
    ];
    $form['actions']['back'] = [
      '#type' => 'submit',
      '#value' => $this->t('Back'),
      '#submit' => ['::backSubmit'],
      '#limit_validation_errors' => [],
    ];

    return $form;
  }

  /**
   * Review-step submit: collect the picks and rebuild into the confirm step.
   */
  public function reviewSubmit(array &$form, FormStateInterface $form_state): void {
    $selections = [];
    $choices = [];
    foreach (($form_state->getValue('findings') ?? []) as $group) {
      if (!is_array($group)) {
        continue;
      }
      foreach ($group as $findingId => $values) {
        $choice = $values['choice'] ?? self::LEAVE;
        if (!is_string($choice) || $choice === self::LEAVE || $choice === '') {
          continue;
        }
        $params = [];
        if ($choice === 'replace_node_type') {
          $params['node_type_id'] = (string) ($values['node_type'] ?? '');
        }
        $selections[] = ['finding' => (string) $findingId, 'remedy' => $choice, 'params' => $params];
        $choices[(string) $findingId] = [
          'choice' => $choice,
          'node_type' => $values['node_type'] ?? NULL,
        ];
      }
    }

    $form_state->set('selections', $selections);
    $form_state->set('choices', $choices);
    $form_state->set('step', 'confirm');
    $form_state->setRebuild(TRUE);
  }

  /**
   * Confirm-step submit: apply the remedies and re-report.
   */
  public function applySubmit(array &$form, FormStateInterface $form_state): void {
    $workflow = $this->workflowFromRoute();
    if (!$workflow instanceof WorkflowDefinitionInterface) {
      return;
    }

    $report = $this->doctor->applyRemedies($workflow, $this->selectionsFromState($form_state));
    $this->reportApplyMessages($report);

    // Clear the multistep state and return to a fresh diagnosis.
    $form_state->set('step', 'review');
    $form_state->set('selections', NULL);
    $form_state->set('choices', NULL);
    $form_state->setRedirectUrl(Url::fromRoute('flowdrop.workflow.doctor', [
      'flowdrop_workflow' => $workflow->id(),
    ]));
  }

  /**
   * Confirm-step "Back": return to review with the picks preserved.
   */
  public function backSubmit(array &$form, FormStateInterface $form_state): void {
    $form_state->set('step', 'review');
    $form_state->setRebuild(TRUE);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    // All work happens in the per-button submit handlers above.
  }

  /**
   * Surfaces the apply outcome as status messages.
   */
  private function reportApplyMessages(DoctorReport $report): void {
    if ($report->isHealthy()) {
      $this->messenger()->addStatus($this->t('The workflow is now healthy.'));
      return;
    }
    $this->messenger()->addStatus($this->t('Remedies applied. @count problem(s) remain.', [
      '@count' => count($report->findings()),
    ]));
  }

  /**
   * Rebuilds RemedySelection objects from the serialized form_state picks.
   *
   * @return list<\Drupal\flowdrop_workflow\Diagnostic\RemedySelection>
   *   The selections.
   */
  private function selectionsFromState(FormStateInterface $form_state): array {
    $selections = [];
    foreach (($form_state->get('selections') ?? []) as $selection) {
      $selections[] = new RemedySelection(
        (string) ($selection['finding'] ?? ''),
        (string) ($selection['remedy'] ?? ''),
        is_array($selection['params'] ?? NULL) ? $selection['params'] : [],
      );
    }
    return $selections;
  }

  /**
   * The workflow being doctored, from the upcast route parameter.
   */
  private function workflowFromRoute(): ?WorkflowDefinitionInterface {
    $workflow = $this->getRouteMatch()->getParameter('flowdrop_workflow');
    return $workflow instanceof WorkflowDefinitionInterface ? $workflow : NULL;
  }

  /**
   * A human-readable label for a severity group heading.
   */
  private function severityHeading(Severity $severity, int $count): string {
    return match ($severity) {
      Severity::Error => (string) $this->formatPlural($count, '1 error', '@count errors'),
      Severity::Warning => (string) $this->formatPlural($count, '1 warning', '@count warnings'),
      Severity::Info => (string) $this->formatPlural($count, '1 notice', '@count notices'),
    };
  }

  /**
   * Renders the per-remedy descriptions as a small definition list.
   *
   * @param array<string, \Drupal\Core\StringTranslation\TranslatableMarkup> $descriptions
   *   Remedy id => description.
   *
   * @return array<string, mixed>
   *   A render array.
   */
  private function remedyDescriptionList(array $descriptions): array {
    return [
      '#theme' => 'item_list',
      '#items' => array_values($descriptions),
    ];
  }

  /**
   * Translates a stable locator into a human-readable phrase.
   *
   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
   *   A markup phrase for known locator shapes, else the raw locator string.
   */
  private function humanLocator(string $locator): string|TranslatableMarkup {
    if (str_starts_with($locator, 'node.')) {
      return $this->t('node "@id"', ['@id' => substr($locator, strlen('node.'))]);
    }
    if (str_starts_with($locator, 'edge.')) {
      return $this->t('edge "@id"', ['@id' => substr($locator, strlen('edge.'))]);
    }
    if (str_starts_with($locator, 'exposure.')) {
      return $this->t('exposure "@id"', ['@id' => substr($locator, strlen('exposure.'))]);
    }
    return $locator;
  }

}
