<?php

declare(strict_types=1);

namespace Drupal\native_observability_report\Form;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Drupal\native_observability_dashboard\Service\DashboardCacheBuilder;
use Drupal\native_observability_dashboard\Service\DashboardDatabaseBuilder;
use Drupal\native_observability_dashboard\Service\DashboardOverviewBuilder;
use Drupal\native_observability_dashboard\ValueObject\TimeRange;
use Drupal\native_observability_report\Service\ObservabilityEnvironmentResolver;
use Drupal\native_observability_report\Service\ObservabilityImpactEstimator;
use Drupal\native_observability_report\Service\ObservabilityScoringService;
use Drupal\native_observability_report\Service\ReportNarrativeLoader;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Builds the observability report preview form.
 */
final class ObservabilityReportBuilderForm extends FormBase {

  /**
   * The report config factory.
   */
  protected ConfigFactoryInterface $reportConfigFactory;

  /**
   * The dashboard overview builder.
   */
  protected DashboardOverviewBuilder $overviewBuilder;

  /**
   * The dashboard cache builder.
   */
  protected DashboardCacheBuilder $cacheBuilder;

  /**
   * The dashboard database builder.
   */
  protected DashboardDatabaseBuilder $databaseBuilder;

  /**
   * The report narrative loader.
   */
  protected ReportNarrativeLoader $narrativeLoader;

  /**
   * The observability environment resolver.
   */
  protected ObservabilityEnvironmentResolver $environmentResolver;

  /**
   * The observability impact estimator.
   */
  protected ObservabilityImpactEstimator $impactEstimator;

  /**
   * The observability scoring service.
   */
  protected ObservabilityScoringService $scoringService;

  /**
   * The private temp store factory.
   */
  protected PrivateTempStoreFactory $tempStoreFactory;

  /**
   * Creates a new report builder form instance.
   */
  public function __construct(
    ConfigFactoryInterface $reportConfigFactory,
    DashboardOverviewBuilder $overviewBuilder,
    DashboardCacheBuilder $cacheBuilder,
    DashboardDatabaseBuilder $databaseBuilder,
    ReportNarrativeLoader $narrativeLoader,
    ObservabilityEnvironmentResolver $environmentResolver,
    ObservabilityImpactEstimator $impactEstimator,
    ObservabilityScoringService $scoringService,
    PrivateTempStoreFactory $tempStoreFactory,
  ) {
    $this->reportConfigFactory = $reportConfigFactory;
    $this->overviewBuilder = $overviewBuilder;
    $this->cacheBuilder = $cacheBuilder;
    $this->databaseBuilder = $databaseBuilder;
    $this->narrativeLoader = $narrativeLoader;
    $this->environmentResolver = $environmentResolver;
    $this->impactEstimator = $impactEstimator;
    $this->scoringService = $scoringService;
    $this->tempStoreFactory = $tempStoreFactory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): self {
    return new self(
      $container->get('config.factory'),
      $container->get('native_observability_dashboard.overview_builder'),
      $container->get('native_observability_dashboard.cache_builder'),
      $container->get('native_observability_dashboard.database_builder'),
      $container->get('native_observability_report.narrative_loader'),
      $container->get('native_observability_report.environment_resolver'),
      $container->get('native_observability_report.impact_estimator'),
      $container->get('native_observability_report.scoring'),
      $container->get('tempstore.private'),
    );
  }

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

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array {
    $config = $this->reportConfigFactory->get('native_observability_report.settings');
    $narrativeLanguage = trim((string) ($form_state->getValue('language') ?: ($config->get('fallback_language') ?? 'en')));
    $narrative = $this->narrativeLoader->load($narrativeLanguage);

    $form['workflow'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => ['no-report-builder__workflow'],
      ],
      'title' => [
        '#markup' => '<h2 class="no-report-builder__workflow-title">' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.workflow_title', 'Report workflow')) . '</h2>',
      ],
      'intro' => [
        '#markup' => '<p class="no-report-builder__workflow-intro">' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.workflow_intro', 'Configure the report scope, generate or refresh the preview, then open the frozen snapshot in printable HTML and save it as PDF from your browser if needed.')) . '</p>',
      ],
      'steps' => [
        '#type' => 'container',
        '#attributes' => [
          'class' => ['no-report-builder__workflow-grid'],
        ],
        'configure' => [
          '#markup' => '<div class="no-report-builder__workflow-card"><h3>' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.workflow_step_configure_title', '1. Configure the report')) . '</h3><p>' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.workflow_step_configure_body', 'Choose the time range, detail level, included sections, privacy rules, and branding settings.')) . '</p></div>',
        ],
        'preview' => [
          '#markup' => '<div class="no-report-builder__workflow-card"><h3>' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.workflow_step_preview_title', '2. Generate or refresh the preview')) . '</h3><p>' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.workflow_step_preview_body', 'Use the main action button to capture a fresh preview snapshot from the selected runtime range.')) . '</p></div>',
        ],
        'export' => [
          '#markup' => '<div class="no-report-builder__workflow-card"><h3>' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.workflow_step_export_title', '3. Open printable HTML and print from the browser')) . '</h3><p>' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.workflow_step_export_body', 'After the preview is ready, open the printable HTML document, review layout, and use your browser print dialog to save the page as PDF when needed.')) . '</p></div>',
        ],
      ],
    ];

    $form['intro'] = [
      '#type' => 'item',
      '#markup' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.intro', $this->t('Generate a structured observability report that explains overview, cache, and database signals using configurable narrative definitions and privacy-aware options.')->render()),
    ];

    $form['composition'] = [
      '#type' => 'details',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.composition_title', $this->t('Report composition')->render()),
      '#open' => TRUE,
    ];

    $form['composition']['report_title'] = [
      '#type' => 'textfield',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.report_title', $this->t('Report title')->render()),
      '#default_value' => 'Native Observability Report',
      '#maxlength' => 255,
      '#required' => TRUE,
    ];

    $form['composition']['detail_level'] = [
      '#type' => 'select',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.detail_level', $this->t('Detail level')->render()),
      '#default_value' => (string) ($config->get('default_detail_level') ?? 'intermediate'),
      '#options' => [
        'basic' => $this->t('Basic'),
        'intermediate' => $this->t('Intermediate'),
        'advanced' => $this->t('Advanced'),
      ],
      '#description' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.detail_level_help', $this->t('Controls how deeply the generated report explains each metric, state, and recommendation.')->render()),
    ];

    $form['composition']['language'] = [
      '#type' => 'textfield',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.narrative_language', $this->t('Narrative language')->render()),
      '#default_value' => (string) ($config->get('fallback_language') ?? 'en'),
      '#maxlength' => 12,
      '#required' => TRUE,
    ];

    $form['composition']['range_key'] = [
      '#type' => 'select',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.observed_time_range', $this->t('Observed time range')->render()),
      '#default_value' => '30m',
      '#options' => [
        '15m' => $this->t('Last 15 minutes'),
        '30m' => $this->t('Last 30 minutes'),
        '1h' => $this->t('Last 1 hour'),
        '6h' => $this->t('Last 6 hours'),
        '24h' => $this->t('Last 24 hours'),
        '7d' => $this->t('Last 7 days'),
      ],
    ];

    $form['composition']['include_overview'] = [
      '#type' => 'checkbox',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.include_overview', $this->t('Include Overview')->render()),
      '#default_value' => TRUE,
    ];

    $form['composition']['include_cache'] = [
      '#type' => 'checkbox',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.include_cache', $this->t('Include Cache')->render()),
      '#default_value' => TRUE,
    ];

    $form['composition']['include_database'] = [
      '#type' => 'checkbox',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.include_database', $this->t('Include Database')->render()),
      '#default_value' => TRUE,
    ];

    $form['composition']['include_overhead_section'] = [
      '#type' => 'checkbox',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.include_overhead_section', $this->t('Include observability overhead')->render()),
      '#default_value' => (bool) $config->get('default_include_overhead_section'),
    ];

    $form['privacy'] = [
      '#type' => 'details',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.privacy_title', $this->t('Privacy options')->render()),
      '#open' => TRUE,
    ];

    $form['privacy']['mask_user_identifiers'] = [
      '#type' => 'checkbox',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.mask_user_identifiers', $this->t('Mask user identifiers')->render()),
      '#default_value' => (bool) $config->get('default_mask_user_identifiers'),
    ];

    $form['privacy']['hide_ip_addresses'] = [
      '#type' => 'checkbox',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.hide_ip_addresses', $this->t('Hide IP addresses')->render()),
      '#default_value' => (bool) $config->get('default_hide_ip_addresses'),
    ];

    $form['privacy']['hide_route_parameters'] = [
      '#type' => 'checkbox',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.hide_route_parameters', $this->t('Hide route parameters')->render()),
      '#default_value' => (bool) $config->get('default_hide_route_parameters'),
    ];

    $form['branding'] = [
      '#type' => 'details',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.branding_title', $this->t('Branding')->render()),
      '#open' => TRUE,
    ];

    $form['branding']['organization_name'] = [
      '#type' => 'textfield',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.organization_name', $this->t('Organization name')->render()),
      '#default_value' => '',
      '#maxlength' => 255,
    ];

    $form['branding']['header_text'] = [
      '#type' => 'textfield',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.header_text', $this->t('Header text')->render()),
      '#default_value' => '',
      '#maxlength' => 255,
    ];

    $form['branding']['footer_text'] = [
      '#type' => 'textfield',
      '#title' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.footer_text', $this->t('Footer text')->render()),
      '#default_value' => 'Generated by Native Observability Report',
      '#maxlength' => 255,
    ];

    $form['actions'] = [
      '#type' => 'actions',
      '#attributes' => [
        'class' => ['no-report-builder__actions'],
      ],
    ];

    $form['actions']['generate_preview'] = [
      '#type' => 'submit',
      '#value' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.generate_preview', $this->t('Generate or refresh preview')->render()),
      '#button_type' => 'primary',
      '#attributes' => [
        'class' => ['button', 'button--primary', 'button--large'],
      ],
    ];

    $form['actions_help'] = [
      '#type' => 'item',
      '#markup' => '<div class="no-report-builder__actions-help">' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.generate_preview_help', 'This action regenerates the preview snapshot using the current form settings. Export buttons appear only after a preview has been built.')) . '</div>',
    ];

    if ($form_state->get('report_preview') !== NULL) {
      $preview = $form_state->get('report_preview');

      $form['preview_ready'] = [
        '#type' => 'container',
        '#attributes' => [
          'class' => ['no-report-builder__preview-ready'],
        ],
        'title' => [
          '#markup' => '<h2 class="no-report-builder__preview-ready-title">' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.preview_ready_title', 'Preview ready')) . '</h2>',
        ],
        'body' => [
          '#markup' => '<p class="no-report-builder__preview-ready-body">' . htmlspecialchars((string) $this->narrativeLoader->getValue($narrative, 'document.form.preview_ready_body', 'Review the generated preview below. When the printable HTML looks correct, open it in a new tab and use the browser print dialog to save the report as PDF if needed.')) . '</p>',
        ],
      ];

      if (!empty($preview['export_urls'])) {
        $form['preview_actions'] = [
          '#type' => 'container',
          '#attributes' => [
            'class' => ['no-report-preview__export-actions'],
          ],
          'buttons' => [
            '#type' => 'container',
            '#attributes' => [
              'class' => ['no-report-preview__export-action-buttons'],
            ],
            'refresh' => [
              '#type' => 'submit',
              '#value' => (string) $this->narrativeLoader->getValue($narrative, 'document.form.generate_preview', $this->t('Generate or refresh preview')->render()),
              '#button_type' => 'primary',
              '#attributes' => [
                'class' => ['button', 'button--primary'],
              ],
            ],
            'html' => [
              '#type' => 'link',
              '#title' => (string) ($preview['narrative_labels']['export_html_label'] ?? 'Open printable HTML'),
              '#url' => Url::fromUri('internal:' . $preview['export_urls']['html']),
              '#attributes' => [
                'class' => ['button'],
                'target' => '_blank',
                'rel' => 'noopener noreferrer',
              ],
            ],
          ],
        ];
      }

      $form['preview'] = $this->buildPreviewSection($preview);
    }

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $range = $this->buildTimeRange((string) $form_state->getValue('range_key'));
    $language = trim((string) $form_state->getValue('language'));
    $detailLevel = (string) $form_state->getValue('detail_level');

    $narrative = $this->narrativeLoader->load($language);
    $narrativeMeta = $this->narrativeLoader->inspect($language);

    $overviewPayload = (bool) $form_state->getValue('include_overview')
      ? $this->overviewBuilder->build($range)
      : [];
    $cachePayload = (bool) $form_state->getValue('include_cache')
      ? $this->cacheBuilder->build($range)
      : [];
    $databasePayload = (bool) $form_state->getValue('include_database')
      ? $this->databaseBuilder->build($range)
      : [];

    $environment = $this->environmentResolver->resolve();
    $isProduction = $this->environmentResolver->isProduction();
    $impact = (bool) $form_state->getValue('include_overhead_section')
      ? $this->impactEstimator->estimate()
      : [];

    $preview = [
      'report_title' => (string) $form_state->getValue('report_title'),
      'organization_name' => trim((string) $form_state->getValue('organization_name')),
      'header_text' => trim((string) $form_state->getValue('header_text')),
      'footer_text' => trim((string) $form_state->getValue('footer_text')),
      'detail_level' => $detailLevel,
      'language' => $language,
      'range_label' => $range->getLabel(),
      'range_key' => $range->getKey(),
      'environment' => $environment,
      'narrative_meta' => $narrativeMeta,
      'narrative_labels' => [
        'preview_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.title', 'Report preview'),
        'preview_metadata_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.metadata_title', 'Preview metadata'),
        'preview_metadata_template' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.metadata_template', 'Environment: @environment | Detail level: @detail | Narrative file: @path'),
        'table_of_contents_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.table_of_contents_title', 'Provisional table of contents'),
        'section_scope_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_scope_title', 'What this section covers'),
        'section_importance_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_importance_title', 'Why it matters'),
        'section_assessment_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_assessment_title', 'Current assessment'),
        'section_recommendation_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_recommendation_title', 'Recommended action'),
        'section_chart_explanations_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_chart_explanations_title', 'Chart explanations'),
        'section_interpretation_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_interpretation_title', 'Metric interpretation guide'),
        'chart_reading_guide_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.chart_reading_guide_title', 'What this chart shows'),
        'chart_interpretation_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.chart_interpretation_title', 'How to interpret it'),
        'generation_feedback_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.generation_feedback_title', 'Generation feedback'),
        'executive_kpis_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.executive_kpis_title', 'Executive KPI snapshot'),
        'executive_summary_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.executive_summary_title', 'Executive summary'),
        'executive_summary_lead' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.executive_summary_lead', 'This condensed summary highlights the dominant operational signals detected in the selected time range.'),
        'cross_section_insights_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.cross_section_insights_title', 'Cross-section insights'),
        'cross_section_insights_lead' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.cross_section_insights_lead', 'These insights correlate the strongest signals across overview, cache, and database evidence.'),
        'priority_actions_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.priority_actions_title', 'Priority actions'),
        'priority_actions_lead' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.priority_actions_lead', 'This block helps prioritise what to review first when the report shows multiple attention points.'),
        'privacy_summary_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.privacy_summary_title', 'Privacy and scope summary'),
        'report_readiness_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.report_readiness_title', 'Report readiness'),
        'appendix_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.appendix_title', 'Report appendix snapshot'),
        'appendix_lead' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.appendix_lead', 'This appendix-oriented block captures the structural context that will feed the final document layout.'),
        'section_summary_cards_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_summary_cards_title', 'Section KPI summary'),
        'section_tables_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_tables_title', 'Supporting evidence tables'),
        'table_empty_message' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.table_empty_message', 'No rows are currently available for this dataset.'),
        'export_actions_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.export_actions_title', 'Step 2 — Review and export this snapshot'),
        'export_actions_help' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.export_actions_help', 'Use printable HTML to validate layout, tables, and charts. When the document looks correct, use your browser print dialog to save it as PDF.'),
        'export_html_label' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.export_html_label', 'Open printable HTML'),
        'document_contents_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.export.contents_title', 'Document contents'),
        'document_contents_lead' => (string) $this->narrativeLoader->getValue($narrative, 'document.export.contents_lead', 'This structured index summarises the printable report blocks and the pages where each block starts.'),
        'document_page_map_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.export.page_map_title', 'Page map'),
        'document_section_charts_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.export.section_charts_title', 'Charts'),
        'document_section_tables_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.export.section_tables_title', 'Evidence tables'),
        'document_no_charts_message' => (string) $this->narrativeLoader->getValue($narrative, 'document.export.no_charts_message', 'No chart snapshots are available for this section in the current export.'),
        'document_page_label' => (string) $this->narrativeLoader->getValue($narrative, 'document.export.page_label', 'Page'),
        'document_print_notice' => (string) $this->narrativeLoader->getValue($narrative, 'document.export.print_notice', 'Interactive charts are rendered in printable HTML. PDF rendering may fall back to narrative and tables when a server-side chart snapshot is not available.'),
        'overhead_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.overhead_title', 'Observability overhead'),
        'overhead_summary_template' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.overhead_summary_template', 'Estimated overhead: @percent% | Impact level: @level'),
        'overhead_contributors_title' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.overhead_contributors_title', 'Main contributors'),
        'section_title_template' => (string) $this->narrativeLoader->getValue($narrative, 'document.preview.section_title_template', '@title — @status (@score/100)'),
      ],
      'privacy' => [
        'mask_user_identifiers' => (bool) $form_state->getValue('mask_user_identifiers'),
        'hide_ip_addresses' => (bool) $form_state->getValue('hide_ip_addresses'),
        'hide_route_parameters' => (bool) $form_state->getValue('hide_route_parameters'),
      ],
      'sections' => [],
      'impact' => $impact,
      'chart_groups' => [],
      'executive_kpis' => [],
      'executive_summary' => [],
      'cross_section_insights' => [],
      'priority_actions' => [],
      'privacy_summary' => [],
      'report_readiness' => [],
      'appendix_cards' => [],
      'structured_toc' => [],
      'generation_feedback' => [],
    ];

    if ($overviewPayload !== []) {
      $overviewScore = $this->scoringService->scoreOverview($overviewPayload);
      $overviewCharts = $this->buildOverviewPreviewCharts($overviewPayload, $narrative);

      $preview['sections']['overview'] = [
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.title', 'Overview'),
        'introduction' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.introduction', ''),
        'importance' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.importance', ''),
        'status' => $overviewScore['status'],
        'score' => $overviewScore['score'],
        'status_label' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.statuses.' . $overviewScore['status'] . '.label', strtoupper($overviewScore['status'])),
        'message' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.statuses.' . $overviewScore['status'] . '.' . $detailLevel, ''),
        'recommendation' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.statuses.' . $overviewScore['status'] . '.recommendation', ''),
        'chart_explanations' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.chart_explanations', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.interpretation', [])),
        'summary_cards' => $this->buildOverviewSummaryCards($overviewPayload),
        'tables' => $this->buildOverviewTables($overviewPayload),
        'charts' => $overviewCharts,
        'payload' => $overviewPayload,
      ];

      foreach ($overviewCharts as $chart) {
        if (!empty($chart['chart_id']) && !empty($chart['settings'])) {
          $preview['chart_groups'][$chart['chart_id']] = $chart['settings'];
        }
      }
    }

    if ($cachePayload !== []) {
      $cacheScore = $this->scoringService->scoreCache($cachePayload, $isProduction);
      $cacheCharts = $this->buildCachePreviewCharts($cachePayload, $narrative);

      $preview['sections']['cache'] = [
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.title', 'Cache'),
        'introduction' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.introduction', ''),
        'importance' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.importance', ''),
        'status' => $cacheScore['status'],
        'score' => $cacheScore['score'],
        'status_label' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.statuses.' . $cacheScore['status'] . '.label', strtoupper($cacheScore['status'])),
        'message' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.statuses.' . $cacheScore['status'] . '.' . $detailLevel, ''),
        'recommendation' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.statuses.' . $cacheScore['status'] . '.recommendation', ''),
        'chart_explanations' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.chart_explanations', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.interpretation', [])),
        'summary_cards' => $this->buildCacheSummaryCards($cachePayload),
        'tables' => $this->buildCacheTables($cachePayload),
        'charts' => $cacheCharts,
        'payload' => $cachePayload,
      ];

      foreach ($cacheCharts as $chart) {
        if (!empty($chart['chart_id']) && !empty($chart['settings'])) {
          $preview['chart_groups'][$chart['chart_id']] = $chart['settings'];
        }
      }
    }

    if ($databasePayload !== []) {
      $databaseScore = $this->scoringService->scoreDatabase($databasePayload);
      $databaseCharts = $this->buildDatabasePreviewCharts($databasePayload, $narrative);

      $preview['sections']['database'] = [
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.title', 'Database'),
        'introduction' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.introduction', ''),
        'importance' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.importance', ''),
        'status' => $databaseScore['status'],
        'score' => $databaseScore['score'],
        'status_label' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.statuses.' . $databaseScore['status'] . '.label', strtoupper($databaseScore['status'])),
        'message' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.statuses.' . $databaseScore['status'] . '.' . $detailLevel, ''),
        'recommendation' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.statuses.' . $databaseScore['status'] . '.recommendation', ''),
        'chart_explanations' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.chart_explanations', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.interpretation', [])),
        'summary_cards' => $this->buildDatabaseSummaryCards($databasePayload),
        'tables' => $this->buildDatabaseTables($databasePayload),
        'charts' => $databaseCharts,
        'payload' => $databasePayload,
      ];

      foreach ($databaseCharts as $chart) {
        if (!empty($chart['chart_id']) && !empty($chart['settings'])) {
          $preview['chart_groups'][$chart['chart_id']] = $chart['settings'];
        }
      }
    }

    $preview['executive_kpis'] = $this->buildExecutiveKpiCards($overviewPayload, $cachePayload, $databasePayload);
    $preview['executive_summary'] = $this->buildExecutiveSummaryItems($preview, $narrative);
    $preview['cross_section_insights'] = $this->buildCrossSectionInsights($overviewPayload, $cachePayload, $databasePayload, $preview);
    $preview['priority_actions'] = $this->buildPriorityActions($preview);
    $preview['privacy_summary'] = $this->buildPrivacySummaryCards($preview);
    $preview['report_readiness'] = $this->buildReportReadinessCards($preview);
    $preview['appendix_cards'] = $this->buildAppendixCards($preview);
    $preview['structured_toc'] = $this->buildStructuredToc($preview);
    $preview['generation_feedback'] = $this->buildGenerationFeedback($preview);

    $this->messenger()->addStatus($this->t('Report preview generated for @range.', ['@range' => $range->getLabel()]));
    $this->messenger()->addStatus($this->t('Sections: @count | Charts: @charts | Executive KPIs: @kpis', [
      '@count' => count($preview['sections']),
      '@charts' => count($preview['chart_groups']),
      '@kpis' => count($preview['executive_kpis']),
    ]));

    $exportKey = 'report_' . $this->currentUser()->id() . '_' . time();
    $tempStore = $this->tempStoreFactory->get('native_observability_report');
    $tempStore->set($exportKey, $preview);
    $preview['export_urls'] = [
      'html' => Url::fromRoute('native_observability_report.export_html', ['export_key' => $exportKey])->toString(),
    ];

    $this->messenger()->addStatus($this->t('Printable HTML is now available for this preview snapshot. Open it in a new tab and use your browser print dialog to save it as PDF when needed.'));

    $form_state->set('report_preview', $preview);
    $form_state->setRebuild(TRUE);
  }

  /**
   * Builds the report preview section.
   */
  private function buildPreviewSection(array $preview): array {
    $labels = $preview['narrative_labels'] ?? [];
    $sections = [];

    foreach ($preview['sections'] as $sectionKey => $section) {
      $sectionBuild = [
        '#type' => 'details',
        '#title' => $this->replaceTokens(
          (string) ($labels['section_title_template'] ?? '@title — @status (@score/100)'),
          [
            '@title' => (string) ($section['title'] ?? ''),
            '@status' => (string) ($section['status_label'] ?? strtoupper((string) ($section['status'] ?? ''))),
            '@score' => (string) ($section['score'] ?? 0),
          ],
        ),
        '#open' => TRUE,
        '#attributes' => [
          'class' => ['no-report-preview__section'],
        ],
        'introduction' => [
          '#type' => 'item',
          '#title' => (string) ($labels['section_scope_title'] ?? 'What this section covers'),
          '#markup' => nl2br(htmlspecialchars((string) ($section['introduction'] ?? ''))),
        ],
        'importance' => [
          '#type' => 'item',
          '#title' => (string) ($labels['section_importance_title'] ?? 'Why it matters'),
          '#markup' => nl2br(htmlspecialchars((string) ($section['importance'] ?? ''))),
        ],
        'message' => [
          '#type' => 'item',
          '#title' => (string) ($labels['section_assessment_title'] ?? 'Current assessment'),
          '#markup' => nl2br(htmlspecialchars((string) ($section['message'] ?? ''))),
        ],
        'recommendation' => [
          '#type' => 'item',
          '#title' => (string) ($labels['section_recommendation_title'] ?? 'Recommended action'),
          '#markup' => nl2br(htmlspecialchars((string) ($section['recommendation'] ?? ''))),
        ],
      ];

      if (!empty($section['summary_cards'])) {
        $sectionBuild['summary_cards'] = $this->buildSummaryCardsRenderArray(
          $section['summary_cards'],
          (string) ($labels['section_summary_cards_title'] ?? 'Section KPI summary')
        );
      }

      if (!empty($section['chart_explanations'])) {
        $sectionBuild['chart_explanations'] = [
          '#theme' => 'item_list',
          '#title' => (string) ($labels['section_chart_explanations_title'] ?? 'Chart explanations'),
          '#items' => $section['chart_explanations'],
        ];
      }

      if (!empty($section['interpretation'])) {
        $sectionBuild['interpretation'] = [
          '#theme' => 'item_list',
          '#title' => (string) ($labels['section_interpretation_title'] ?? 'Metric interpretation guide'),
          '#items' => $section['interpretation'],
        ];
      }

      if (!empty($section['charts'])) {
        foreach ($section['charts'] as $delta => $chart) {
          $sectionBuild['chart_' . $delta] = $this->buildChartPreviewRenderArray($chart, $labels);
        }
      }

      if (!empty($section['tables'])) {
        $sectionBuild['tables_title'] = [
          '#markup' => '<h3 class="no-report-preview__subheading">' . htmlspecialchars((string) ($labels['section_tables_title'] ?? 'Supporting evidence tables')) . '</h3>',
        ];

        foreach ($section['tables'] as $delta => $table) {
          $sectionBuild['table_' . $delta] = $this->buildTablePreviewRenderArray($table, $labels);
        }
      }

      $sections[$sectionKey] = $sectionBuild;
    }

    $build = [
      '#type' => 'details',
      '#title' => (string) ($labels['preview_title'] ?? 'Report preview'),
      '#open' => TRUE,
      '#attached' => [
        'library' => [
          'native_observability_report/preview',
        ],
        'drupalSettings' => [
          'nativeObservabilityReport' => [
            'previewCharts' => $preview['chart_groups'] ?? [],
          ],
        ],
      ],
      'meta' => [
        '#type' => 'item',
        '#title' => (string) ($labels['preview_metadata_title'] ?? 'Preview metadata'),
        '#markup' => $this->replaceTokens(
          (string) ($labels['preview_metadata_template'] ?? 'Environment: @environment | Detail level: @detail | Narrative file: @path'),
          [
            '@environment' => (string) ($preview['environment'] ?? ''),
            '@detail' => (string) ($preview['detail_level'] ?? ''),
            '@path' => (string) ($preview['narrative_meta']['path'] ?? 'default'),
          ],
        ),
      ],
    ];

    if (!empty($preview['generation_feedback'])) {
      $build['generation_feedback'] = [
        '#theme' => 'item_list',
        '#title' => (string) ($labels['generation_feedback_title'] ?? 'Generation feedback'),
        '#items' => $preview['generation_feedback'],
        '#attributes' => ['class' => ['no-report-preview__feedback']],
      ];
    }

    if (!empty($preview['privacy_summary'])) {
      $build['privacy_summary'] = $this->buildSummaryCardsRenderArray(
        $preview['privacy_summary'],
        (string) ($labels['privacy_summary_title'] ?? 'Privacy and scope summary')
      );
    }

    if (!empty($preview['executive_kpis'])) {
      $build['executive_kpis'] = $this->buildSummaryCardsRenderArray(
        $preview['executive_kpis'],
        (string) ($labels['executive_kpis_title'] ?? 'Executive KPI snapshot')
      );
    }

    if (!empty($preview['executive_summary'])) {
      $build['executive_summary'] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['no-report-preview__executive-summary']],
        'title' => [
          '#markup' => '<h3 class="no-report-preview__subheading">' . htmlspecialchars((string) ($labels['executive_summary_title'] ?? 'Executive summary')) . '</h3>',
        ],
        'lead' => [
          '#markup' => '<p class="no-report-preview__lead">' . htmlspecialchars((string) ($labels['executive_summary_lead'] ?? 'This condensed summary highlights the dominant operational signals detected in the selected time range.')) . '</p>',
        ],
        'items' => [
          '#theme' => 'item_list',
          '#items' => $preview['executive_summary'],
        ],
      ];
    }

    if (!empty($preview['cross_section_insights'])) {
      $build['cross_section_insights'] = $this->buildNarrativeCardsRenderArray(
        $preview['cross_section_insights'],
        (string) ($labels['cross_section_insights_title'] ?? 'Cross-section insights'),
        (string) ($labels['cross_section_insights_lead'] ?? 'These insights correlate the strongest signals across overview, cache, and database evidence.')
      );
    }

    if (!empty($preview['priority_actions'])) {
      $build['priority_actions'] = $this->buildNarrativeCardsRenderArray(
        $preview['priority_actions'],
        (string) ($labels['priority_actions_title'] ?? 'Priority actions'),
        (string) ($labels['priority_actions_lead'] ?? 'This block helps prioritise what to review first when the report shows multiple attention points.')
      );
    }

    if (!empty($preview['report_readiness'])) {
      $build['report_readiness'] = $this->buildSummaryCardsRenderArray(
        $preview['report_readiness'],
        (string) ($labels['report_readiness_title'] ?? 'Report readiness')
      );
    }

    $build['toc'] = [
      '#theme' => 'item_list',
      '#title' => (string) ($labels['table_of_contents_title'] ?? 'Provisional table of contents'),
      '#items' => $preview['structured_toc'] ?: array_map(
        static fn(array $section): string => (string) ($section['title'] ?? ''),
        array_values($preview['sections'] ?? []),
      ),
    ];

    $build['sections'] = $sections;

    if (!empty($preview['appendix_cards'])) {
      $build['appendix_cards'] = $this->buildNarrativeCardsRenderArray(
        $preview['appendix_cards'],
        (string) ($labels['appendix_title'] ?? 'Report appendix snapshot'),
        (string) ($labels['appendix_lead'] ?? 'This appendix-oriented block captures the structural context that will feed the final document layout.')
      );
    }

    if (!empty($preview['impact'])) {
      $build['impact'] = [
        '#type' => 'details',
        '#title' => (string) ($labels['overhead_title'] ?? 'Observability overhead'),
        '#open' => TRUE,
        'summary' => [
          '#markup' => $this->replaceTokens(
            (string) ($labels['overhead_summary_template'] ?? 'Estimated overhead: @percent% | Impact level: @level'),
            [
              '@percent' => (string) ($preview['impact']['estimated_overhead_percent'] ?? 0),
              '@level' => (string) ($preview['impact']['impact_level'] ?? ''),
            ],
          ),
        ],
        'contributors' => [
          '#theme' => 'item_list',
          '#title' => (string) ($labels['overhead_contributors_title'] ?? 'Main contributors'),
          '#items' => $preview['impact']['contributors'] ?? [],
        ],
      ];
    }

    return $build;
  }

  /**
   * Builds a summary card list render array.
   */
  private function buildSummaryCardsRenderArray(array $cards, string $title): array {
    $items = [];

    foreach ($cards as $card) {
      $items[] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['no-report-preview__summary-card']],
        'title' => [
          '#markup' => '<div class="no-report-preview__summary-card-title">' . htmlspecialchars((string) ($card['title'] ?? '')) . '</div>',
        ],
        'value' => [
          '#markup' => '<div class="no-report-preview__summary-card-value">' . htmlspecialchars((string) ($card['value'] ?? '')) . '</div>',
        ],
        'description' => [
          '#markup' => '<div class="no-report-preview__summary-card-description">' . htmlspecialchars((string) ($card['description'] ?? '')) . '</div>',
        ],
      ];
    }

    return [
      '#type' => 'container',
      '#attributes' => ['class' => ['no-report-preview__summary-wrapper']],
      'title' => [
        '#markup' => '<h3 class="no-report-preview__subheading">' . htmlspecialchars($title) . '</h3>',
      ],
      'cards' => [
        '#type' => 'container',
        '#attributes' => ['class' => ['no-report-preview__summary-grid']],
        'items' => $items,
      ],
    ];
  }

  /**
   * Builds a narrative card list render array.
   */
  private function buildNarrativeCardsRenderArray(array $items, string $title, string $lead): array {
    $cards = [];

    foreach ($items as $item) {
      $cards[] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['no-report-preview__narrative-card']],
        'title' => [
          '#markup' => '<div class="no-report-preview__summary-card-title">' . htmlspecialchars((string) ($item['title'] ?? '')) . '</div>',
        ],
        'value' => [
          '#markup' => '<div class="no-report-preview__narrative-card-message">' . htmlspecialchars((string) ($item['message'] ?? '')) . '</div>',
        ],
        'description' => [
          '#markup' => '<div class="no-report-preview__summary-card-description">' . htmlspecialchars((string) ($item['description'] ?? '')) . '</div>',
        ],
      ];
    }

    return [
      '#type' => 'container',
      '#attributes' => ['class' => ['no-report-preview__summary-wrapper']],
      'title' => [
        '#markup' => '<h3 class="no-report-preview__subheading">' . htmlspecialchars($title) . '</h3>',
      ],
      'lead' => [
        '#markup' => '<p class="no-report-preview__lead">' . htmlspecialchars($lead) . '</p>',
      ],
      'cards' => [
        '#type' => 'container',
        '#attributes' => ['class' => ['no-report-preview__summary-grid']],
        'items' => $cards,
      ],
    ];
  }

  /**
   * Builds a chart render array for the preview section.
   */
  private function buildChartPreviewRenderArray(array $chart, array $labels): array {
    $readingGuideItems = [];
    foreach (($chart['reading_guide'] ?? []) as $item) {
      $readingGuideItems[] = ['#markup' => htmlspecialchars((string) $item)];
    }

    $interpretationItems = [];
    foreach (($chart['interpretation'] ?? []) as $item) {
      $interpretationItems[] = ['#markup' => htmlspecialchars((string) $item)];
    }

    return [
      '#type' => 'container',
      '#attributes' => [
        'class' => ['no-report-preview__chart-card'],
      ],
      'header' => [
        '#markup' => '<div class="no-report-preview__chart-header"><h4 class="no-report-preview__chart-title">' . htmlspecialchars((string) ($chart['title'] ?? '')) . '</h4><p class="no-report-preview__chart-description">' . htmlspecialchars((string) ($chart['description'] ?? '')) . '</p></div>',
      ],
      'surface' => [
        '#markup' => '<div class="no-report-preview__chart-surface" data-no-report-chart-id="' . htmlspecialchars((string) ($chart['chart_id'] ?? '')) . '"></div>',
      ],
      'meta' => [
        '#type' => 'container',
        '#attributes' => [
          'class' => ['no-report-preview__chart-meta'],
        ],
        'reading_guide' => [
          '#type' => 'container',
          '#attributes' => [
            'class' => ['no-report-preview__chart-meta-block'],
          ],
          'title' => [
            '#markup' => '<h5>' . htmlspecialchars((string) ($labels['chart_reading_guide_title'] ?? 'What this chart shows')) . '</h5>',
          ],
          'items' => [
            '#theme' => 'item_list',
            '#items' => $readingGuideItems,
          ],
        ],
        'interpretation' => [
          '#type' => 'container',
          '#attributes' => [
            'class' => ['no-report-preview__chart-meta-block'],
          ],
          'title' => [
            '#markup' => '<h5>' . htmlspecialchars((string) ($labels['chart_interpretation_title'] ?? 'How to interpret it')) . '</h5>',
          ],
          'items' => [
            '#theme' => 'item_list',
            '#items' => $interpretationItems,
          ],
        ],
      ],
    ];
  }

  /**
   * Builds a preview table render array.
   */
  private function buildTablePreviewRenderArray(array $table, array $labels): array {
    $headers = array_map(static fn(array $header): string => (string) ($header['label'] ?? ''), $table['headers'] ?? []);
    $rows = [];

    foreach ($table['rows'] ?? [] as $row) {
      $cells = [];
      foreach ($table['headers'] ?? [] as $header) {
        $key = (string) ($header['key'] ?? '');
        $cells[] = htmlspecialchars((string) ($row[$key] ?? '—'));
      }
      $rows[] = $cells;
    }

    return [
      '#type' => 'container',
      '#attributes' => ['class' => ['no-report-preview__table-card']],
      'title' => [
        '#markup' => '<h4 class="no-report-preview__table-title">' . htmlspecialchars((string) ($table['title'] ?? '')) . '</h4>',
      ],
      'description' => [
        '#markup' => '<p class="no-report-preview__table-description">' . htmlspecialchars((string) ($table['description'] ?? '')) . '</p>',
      ],
      'table' => !empty($rows) ? [
        '#theme' => 'table',
        '#header' => $headers,
        '#rows' => $rows,
        '#attributes' => ['class' => ['no-report-preview__table']],
      ] : [
        '#markup' => '<div class="no-report-preview__table-empty">' . htmlspecialchars((string) ($table['empty_message'] ?? ($labels['table_empty_message'] ?? 'No rows are currently available for this dataset.'))) . '</div>',
      ],
    ];
  }

  /**
   * Builds overview section summary cards.
   */
  private function buildOverviewSummaryCards(array $payload): array {
    $cards = [];
    $primaryCards = $payload['sections']['primary_kpis']['cards'] ?? [];
    $requestCards = $payload['sections']['request_timeline']['summary_cards'] ?? [];
    $ajaxCards = $payload['sections']['ajax_activity']['summary_cards'] ?? [];

    if (!empty($primaryCards[0]['primary'])) {
      $cards[] = [
        'title' => (string) $this->t('Traffic Overview'),
        'value' => (string) ($primaryCards[0]['primary']['value'] ?? '0'),
        'description' => (string) $this->t('Captured requests available inside the selected analysis range.'),
      ];
    }

    foreach ($requestCards as $card) {
      $cards[] = [
        'title' => (string) ($card['label'] ?? $this->t('Metric')),
        'value' => (string) ($card['value'] ?? '0'),
        'description' => $this->resolveOverviewCardDescription((string) ($card['label'] ?? '')),
      ];
    }

    if (!empty($ajaxCards[0])) {
      $cards[] = [
        'title' => (string) ($ajaxCards[0]['label'] ?? $this->t('AJAX Requests')),
        'value' => (string) ($ajaxCards[0]['value'] ?? '0'),
        'description' => (string) $this->t('Asynchronous requests captured separately from page navigation.'),
      ];
    }

    return $cards;
  }

  /**
   * Builds cache section summary cards.
   */
  private function buildCacheSummaryCards(array $payload): array {
    $section = $payload['sections']['cache_observability'] ?? [];
    $behavior = $section['behavior']['overall'] ?? [];
    $cacheability = $section['cacheability']['summary'] ?? [];
    $invalidations = $section['invalidations']['summary'] ?? [];
    $efficiency = $section['efficiency_score'] ?? [];

    return [
      [
        'title' => (string) $this->t('Cache Efficiency Score'),
        'value' => (string) (($efficiency['value'] ?? 0) . '/100'),
        'description' => (string) $this->t('Synthetic cache health score derived from the currently observed cache signals.'),
      ],
      [
        'title' => (string) $this->t('Dynamic Page Cache Hit Rate'),
        'value' => number_format((float) (($section['behavior']['layers']['dynamic_page_cache']['hit_rate'] ?? 0.0)), 1) . '%',
        'description' => (string) $this->t('How often reusable page variants are served from dynamic page cache.'),
      ],
      [
        'title' => (string) $this->t('Cacheable Ratio'),
        'value' => number_format((float) ($cacheability['cacheable_ratio'] ?? 0.0), 1) . '%',
        'description' => (string) $this->t('Share of observed responses that appear cacheable in the selected range.'),
      ],
      [
        'title' => (string) $this->t('Invalidation Rate / Hour'),
        'value' => number_format((float) ($invalidations['rate_per_hour'] ?? 0.0), 1),
        'description' => (string) $this->t('Frequency of cache invalidation events normalised across one hour.'),
      ],
      [
        'title' => (string) $this->t('Observed Hits'),
        'value' => (string) ($behavior['hits'] ?? 0),
        'description' => (string) $this->t('Requests that reused cache instead of rebuilding the response from scratch.'),
      ],
    ];
  }

  /**
   * Builds database section summary cards.
   */
  private function buildDatabaseSummaryCards(array $payload): array {
    $cards = [];
    foreach (($payload['sections']['database_observability']['overview_cards'] ?? []) as $card) {
      $cards[] = [
        'title' => (string) ($card['title'] ?? $this->t('Metric')),
        'value' => (string) ($card['value'] ?? '0'),
        'description' => (string) ($card['description'] ?? ''),
      ];
    }
    return $cards;
  }

  /**
   * Builds overview evidence tables.
   */
  private function buildOverviewTables(array $payload): array {
    $datasets = $payload['sections']['technical_insights']['datasets'] ?? [];
    $tables = [];

    foreach (['slow_endpoints', 'ajax_endpoints', 'external_requests'] as $key) {
      if (!empty($datasets[$key])) {
        $tables[] = $this->normalizeDatasetTable($datasets[$key], 8);
      }
    }

    return $tables;
  }

  /**
   * Builds cache evidence tables.
   */
  private function buildCacheTables(array $payload): array {
    $section = $payload['sections']['cache_observability'] ?? [];
    $tables = [];

    $layerRows = [];
    foreach (($section['behavior']['layers'] ?? []) as $layerKey => $layer) {
      $layerRows[] = [
        'layer' => (string) $this->t('@layer', ['@layer' => ucwords(str_replace('_', ' ', (string) $layerKey))]),
        'status' => (string) ($layer['status'] ?? $this->t('n/a')),
        'hit_rate' => number_format((float) ($layer['hit_rate'] ?? 0.0), 1) . '%',
        'hits' => (string) ($layer['hits'] ?? 0),
        'misses' => (string) ($layer['misses'] ?? 0),
      ];
    }

    $tables[] = [
      'title' => (string) $this->t('Cache Layer Overview'),
      'description' => (string) $this->t('Layer-level cache effectiveness snapshot used to explain which cache layer is carrying most of the reuse benefit.'),
      'headers' => [
        ['key' => 'layer', 'label' => (string) $this->t('Layer')],
        ['key' => 'status', 'label' => (string) $this->t('Status')],
        ['key' => 'hit_rate', 'label' => (string) $this->t('Hit Rate')],
        ['key' => 'hits', 'label' => (string) $this->t('Hits')],
        ['key' => 'misses', 'label' => (string) $this->t('Misses')],
      ],
      'rows' => $layerRows,
      'empty_message' => (string) $this->t('No layer-level cache records are available yet.'),
    ];

    if (!empty($section['runtime_records'])) {
      $tables[] = $this->normalizeRuntimeRecordsTable($section['runtime_records'], 8);
    }

    return $tables;
  }

  /**
   * Builds database evidence tables.
   */
  private function buildDatabaseTables(array $payload): array {
    $section = $payload['sections']['database_observability'] ?? [];
    $tables = [];

    if (!empty($section['route_table'])) {
      $tables[] = $this->normalizeRouteTable($section['route_table'], 8);
    }

    if (!empty($section['latest_records'])) {
      $tables[] = $this->normalizeLatestRecordsTable($section['latest_records'], 8);
    }

    return $tables;
  }

  /**
   * Builds the executive KPI cards.
   */
  private function buildExecutiveKpiCards(array $overviewPayload, array $cachePayload, array $databasePayload): array {
    $cards = [];
    $requestCards = $overviewPayload['sections']['request_timeline']['summary_cards'] ?? [];
    $ajaxCards = $overviewPayload['sections']['ajax_activity']['summary_cards'] ?? [];
    $cacheSection = $cachePayload['sections']['cache_observability'] ?? [];
    $databaseCards = $databasePayload['sections']['database_observability']['overview_cards'] ?? [];

    $cards[] = [
      'title' => (string) $this->t('Captured Requests'),
      'value' => $this->extractCardValue($requestCards, 'Captured Requests', '0'),
      'description' => (string) $this->t('Total amount of execution evidence available for the currently selected report range.'),
    ];
    $cards[] = [
      'title' => (string) $this->t('P95 Latency'),
      'value' => $this->extractCardValue($requestCards, 'P95 Latency', '0 ms'),
      'description' => (string) $this->t('User-experience tail latency is often more representative than the average alone.'),
    ];
    $cards[] = [
      'title' => (string) $this->t('AJAX Requests'),
      'value' => $this->extractCardValue($ajaxCards, 'AJAX Requests', '0'),
      'description' => (string) $this->t('Frontend asynchronous activity isolated from full-page navigation.'),
    ];
    $cards[] = [
      'title' => (string) $this->t('Cache Efficiency Score'),
      'value' => (string) (($cacheSection['efficiency_score']['value'] ?? 0) . '/100'),
      'description' => (string) $this->t('Synthetic cache health indicator used to decide whether cache tuning should be prioritised.'),
    ];

    foreach ($databaseCards as $card) {
      if ((string) ($card['title'] ?? '') === 'Average DB time / request') {
        $cards[] = [
          'title' => (string) $this->t('Average DB Time / Request'),
          'value' => (string) ($card['value'] ?? '0 ms'),
          'description' => (string) $this->t('Average share of request lifecycle spent executing database work.'),
        ];
        break;
      }
    }

    return $cards;
  }

  /**
   * Builds executive summary bullets.
   */
  private function buildExecutiveSummaryItems(array $preview, array $narrative): array {
    $items = [];
    $overview = $preview['sections']['overview'] ?? [];
    $cache = $preview['sections']['cache'] ?? [];
    $database = $preview['sections']['database'] ?? [];
    $kpis = $preview['executive_kpis'] ?? [];

    $p95 = $this->findExecutiveKpiValue($kpis, 'P95 Latency');
    $cacheScore = $this->findExecutiveKpiValue($kpis, 'Cache Efficiency Score');
    $dbTime = $this->findExecutiveKpiValue($kpis, 'Average DB Time / Request');

    if ($overview !== []) {
      $items[] = $this->replaceTokens(
        (string) $this->narrativeLoader->getValue($narrative, 'document.executive_summary.overview', 'Overview status is @status with a score of @score/100. Tail latency is currently @p95.'),
        [
          '@status' => (string) ($overview['status_label'] ?? strtoupper((string) ($overview['status'] ?? 'unknown'))),
          '@score' => (string) ($overview['score'] ?? 0),
          '@p95' => $p95,
        ],
      );
    }

    if ($cache !== []) {
      $items[] = $this->replaceTokens(
        (string) $this->narrativeLoader->getValue($narrative, 'document.executive_summary.cache', 'Cache status is @status with a score of @score/100. Current efficiency score is @cache_score.'),
        [
          '@status' => (string) ($cache['status_label'] ?? strtoupper((string) ($cache['status'] ?? 'unknown'))),
          '@score' => (string) ($cache['score'] ?? 0),
          '@cache_score' => $cacheScore,
        ],
      );
    }

    if ($database !== []) {
      $items[] = $this->replaceTokens(
        (string) $this->narrativeLoader->getValue($narrative, 'document.executive_summary.database', 'Database status is @status with a score of @score/100. Average DB time per request is @db_time.'),
        [
          '@status' => (string) ($database['status_label'] ?? strtoupper((string) ($database['status'] ?? 'unknown'))),
          '@score' => (string) ($database['score'] ?? 0),
          '@db_time' => $dbTime,
        ],
      );
    }

    $items[] = $this->replaceTokens(
      (string) $this->narrativeLoader->getValue($narrative, 'document.executive_summary.overall', 'The report currently combines @section_count sections, @chart_count charts, and @kpi_count executive KPIs into a single guided preview.'),
      [
        '@section_count' => (string) count($preview['sections'] ?? []),
        '@chart_count' => (string) count($preview['chart_groups'] ?? []),
        '@kpi_count' => (string) count($preview['executive_kpis'] ?? []),
      ],
    );

    return $items;
  }

  /**
   * Builds a structured table of contents list.
   */
  private function buildStructuredToc(array $preview): array {
    $items = [];

    if (!empty($preview['privacy_summary'])) {
      $items[] = 'Privacy and scope summary — ' . count($preview['privacy_summary']) . ' cards';
    }

    if (!empty($preview['executive_kpis'])) {
      $items[] = 'Executive KPI snapshot — ' . count($preview['executive_kpis']) . ' cards';
    }

    if (!empty($preview['executive_summary'])) {
      $items[] = 'Executive summary — ' . count($preview['executive_summary']) . ' summary statements';
    }

    if (!empty($preview['cross_section_insights'])) {
      $items[] = 'Cross-section insights — ' . count($preview['cross_section_insights']) . ' correlated findings';
    }

    if (!empty($preview['priority_actions'])) {
      $items[] = 'Priority actions — ' . count($preview['priority_actions']) . ' next-step recommendations';
    }

    if (!empty($preview['report_readiness'])) {
      $items[] = 'Report readiness — ' . count($preview['report_readiness']) . ' delivery checks';
    }

    foreach (($preview['sections'] ?? []) as $section) {
      $items[] = sprintf(
        '%s — %d KPI cards, %d charts, %d tables',
        (string) ($section['title'] ?? 'Section'),
        count($section['summary_cards'] ?? []),
        count($section['charts'] ?? []),
        count($section['tables'] ?? [])
      );
    }

    if (!empty($preview['appendix_cards'])) {
      $items[] = 'Report appendix snapshot — ' . count($preview['appendix_cards']) . ' structural notes';
    }

    if (!empty($preview['impact'])) {
      $items[] = 'Observability overhead — ' . count($preview['impact']['contributors'] ?? []) . ' contributors';
    }

    return $items;
  }

  /**
   * Builds privacy and scope summary cards.
   */
  private function buildPrivacySummaryCards(array $preview): array {
    $privacy = $preview['privacy'] ?? [];

    return [
      [
        'title' => 'User identifiers',
        'value' => !empty($privacy['mask_user_identifiers']) ? 'Masked' : 'Visible',
        'description' => 'Controls whether user-related identifiers are obfuscated inside the generated report preview.',
      ],
      [
        'title' => 'IP addresses',
        'value' => !empty($privacy['hide_ip_addresses']) ? 'Hidden' : 'Visible',
        'description' => 'Defines whether network-origin details are removed from report-oriented evidence blocks.',
      ],
      [
        'title' => 'Route parameters',
        'value' => !empty($privacy['hide_route_parameters']) ? 'Hidden' : 'Visible',
        'description' => 'Helps avoid exposing sensitive route fragments when the report is shared beyond engineering teams.',
      ],
      [
        'title' => 'Narrative scope',
        'value' => (string) ($preview['range_label'] ?? 'Unknown range'),
        'description' => 'The current document scope is aligned to the selected reporting window and language configuration.',
      ],
    ];
  }

  /**
   * Builds report readiness cards.
   */
  private function buildReportReadinessCards(array $preview): array {
    $sectionCount = count($preview['sections'] ?? []);
    $chartCount = count($preview['chart_groups'] ?? []);
    $tableCount = 0;
    $kpiCount = count($preview['executive_kpis'] ?? []);

    foreach (($preview['sections'] ?? []) as $section) {
      $tableCount += count($section['tables'] ?? []);
      $kpiCount += count($section['summary_cards'] ?? []);
    }

    return [
      [
        'title' => 'Coverage status',
        'value' => $sectionCount . ' sections',
        'description' => 'Preview composition currently includes the enabled functional domains that will feed the final document.',
      ],
      [
        'title' => 'Visual evidence',
        'value' => $chartCount . ' charts',
        'description' => 'Chart rendering is active, which means the preview is already close to the visual structure of the final report.',
      ],
      [
        'title' => 'Tabular evidence',
        'value' => $tableCount . ' tables',
        'description' => 'Supporting tables are available for technical drill-down and appendix-oriented evidence selection.',
      ],
      [
        'title' => 'Narrative density',
        'value' => $kpiCount . ' KPI cards',
        'description' => 'The report already includes decision-oriented KPI summaries that can be reused in the export workflow.',
      ],
    ];
  }

  /**
   * Builds appendix-oriented context cards.
   */
  private function buildAppendixCards(array $preview): array {
    $enabledSections = array_map(static fn(array $section): string => (string) ($section['title'] ?? 'Section'), array_values($preview['sections'] ?? []));
    $totalCharts = count($preview['chart_groups'] ?? []);
    $totalTables = 0;

    foreach (($preview['sections'] ?? []) as $section) {
      $totalTables += count($section['tables'] ?? []);
    }

    return [
      [
        'title' => 'Document scope',
        'message' => sprintf('Report title is "%s" for environment %s and range %s.', (string) ($preview['report_title'] ?? 'Native Observability Report'), (string) ($preview['environment'] ?? 'unknown'), (string) ($preview['range_label'] ?? 'unknown')),
        'description' => 'This card captures the base context that will later map well to the cover page and report metadata blocks.',
      ],
      [
        'title' => 'Enabled sections',
        'message' => $enabledSections ? implode(', ', $enabledSections) : 'No sections enabled.',
        'description' => 'Useful for the final appendix and index because it makes the exported report scope explicit.',
      ],
      [
        'title' => 'Evidence inventory',
        'message' => sprintf('%d charts, %d tables, %d cross-section insights, %d priority actions.', $totalCharts, $totalTables, count($preview['cross_section_insights'] ?? []), count($preview['priority_actions'] ?? [])),
        'description' => 'This helps verify that the report contains both visual and technical evidence before moving to final document generation.',
      ],
      [
        'title' => 'Privacy posture',
        'message' => sprintf('User identifiers: %s | IP addresses: %s | Route parameters: %s', !empty(($preview['privacy']['mask_user_identifiers'] ?? FALSE)) ? 'masked' : 'visible', !empty(($preview['privacy']['hide_ip_addresses'] ?? FALSE)) ? 'hidden' : 'visible', !empty(($preview['privacy']['hide_route_parameters'] ?? FALSE)) ? 'hidden' : 'visible'),
        'description' => 'This is especially useful when the report needs to be shared with non-technical or cross-team stakeholders.',
      ],
    ];
  }

  /**
   * Builds cross-section insights using correlated runtime signals.
   */
  private function buildCrossSectionInsights(array $overviewPayload, array $cachePayload, array $databasePayload, array $preview): array {
    $insights = [];

    $requestCards = $overviewPayload['sections']['request_timeline']['summary_cards'] ?? [];
    $ajaxCards = $overviewPayload['sections']['ajax_activity']['summary_cards'] ?? [];
    $cacheSection = $cachePayload['sections']['cache_observability'] ?? [];
    $databaseCards = $databasePayload['sections']['database_observability']['overview_cards'] ?? [];

    $avgLatency = $this->toFloat($this->extractCardValue($requestCards, 'Average Latency', '0'));
    $p95Latency = $this->toFloat($this->extractCardValue($requestCards, 'P95 Latency', '0'));
    $ajaxRequests = $this->toFloat($this->extractCardValue($ajaxCards, 'AJAX Requests', '0'));
    $ajaxErrorRate = $this->toFloat($this->extractCardValue($ajaxCards, 'Error Rate', '0'));
    $cacheEfficiency = (float) ($cacheSection['efficiency_score']['value'] ?? 0);
    $cacheableRatio = (float) ($cacheSection['cacheability']['summary']['cacheable_ratio'] ?? 0);
    $invalidationsPerHour = (float) ($cacheSection['invalidations']['summary']['rate_per_hour'] ?? 0);
    $dynamicHitRate = (float) ($cacheSection['behavior']['layers']['dynamic_page_cache']['hit_rate'] ?? 0);
    $avgDbTime = $this->extractOverviewMetricFloat($databaseCards, 'Average DB time / request');
    $avgQueries = $this->extractOverviewMetricFloat($databaseCards, 'Average queries / request');
    $slowIncidence = $this->extractOverviewMetricFloat($databaseCards, 'Slow query incidence');

    if ($p95Latency > 0 && $avgLatency > 0 && $p95Latency >= ($avgLatency * 2.0)) {
      $insights[] = [
        'title' => 'Latency tail is materially wider than the average',
        'message' => 'P95 latency is diverging from average latency, which suggests that a subset of requests is degrading far more than the general request baseline.',
        'description' => 'This usually points to route-specific bottlenecks, uneven cache reuse, or isolated downstream cost rather than broad platform slowness.',
      ];
    }

    if ($avgDbTime >= 100.0 && $dynamicHitRate < 50.0) {
      $insights[] = [
        'title' => 'Database-heavy traffic is not being shielded enough by cache',
        'message' => 'Average database time per request is elevated while dynamic page cache hit rate remains limited.',
        'description' => 'This combination usually means the backend is paying repeated database cost for traffic that could benefit more from response reuse or improved cacheability.',
      ];
    }

    if ($cacheEfficiency > 0 && $cacheEfficiency < 70.0 && $invalidationsPerHour >= 25.0) {
      $insights[] = [
        'title' => 'Cache reuse is likely being reduced by invalidation churn',
        'message' => 'Cache efficiency is below the healthy range while invalidation activity is frequent enough to erode response reuse.',
        'description' => 'This is often a sign that tag invalidations are too broad, too frequent, or attached to content that changes in large batches.',
      ];
    }

    if ($cacheableRatio > 0 && $cacheableRatio < 60.0 && $avgQueries >= 20.0) {
      $insights[] = [
        'title' => 'A significant share of database work may be happening on weakly cacheable traffic',
        'message' => 'Observed cacheable ratio is limited while average query density per request is elevated.',
        'description' => 'This combination often amplifies infrastructure cost because expensive execution is being repeated on traffic that is not sufficiently reusable.',
      ];
    }

    if ($ajaxRequests > 0 && $ajaxErrorRate >= 1.0) {
      $insights[] = [
        'title' => 'Asynchronous traffic deserves direct inspection',
        'message' => 'AJAX activity is present and the asynchronous error rate is non-trivial.',
        'description' => 'Even when full-page routes look acceptable, unstable AJAX callbacks can still degrade user experience, dashboards, and dynamic interfaces.',
      ];
    }

    if ($slowIncidence >= 10.0 && $avgDbTime >= 80.0) {
      $insights[] = [
        'title' => 'Slow-query evidence is aligned with broader database pressure',
        'message' => 'Slow query incidence is meaningful and average database time per request is also elevated.',
        'description' => 'This is stronger evidence than isolated spikes because it suggests the problem is recurring enough to influence overall request cost.',
      ];
    }

    if ($insights === []) {
      $insights[] = [
        'title' => 'Cross-section signals are currently aligned',
        'message' => 'The current preview does not expose a dominant contradiction between overview, cache, and database sections.',
        'description' => 'This does not guarantee the absence of issues, but it suggests there is no immediately obvious cross-layer conflict in the selected range.',
      ];
    }

    return $insights;
  }

  /**
   * Builds a priority action list ordered by section severity.
   */
  private function buildPriorityActions(array $preview): array {
    $actions = [];
    $sections = array_values($preview['sections'] ?? []);

    usort($sections, static function (array $left, array $right): int {
      return ((int) ($left['score'] ?? 100)) <=> ((int) ($right['score'] ?? 100));
    });

    foreach (array_slice($sections, 0, 3) as $index => $section) {
      $actions[] = [
        'title' => sprintf('Priority %d — %s', $index + 1, (string) ($section['title'] ?? 'Section')),
        'message' => (string) ($section['recommendation'] ?? 'Review this section first.'),
        'description' => sprintf(
          'Current status is %s with %d KPI cards, %d charts, and %d supporting tables available for investigation.',
          (string) ($section['status_label'] ?? strtoupper((string) ($section['status'] ?? 'unknown'))),
          count($section['summary_cards'] ?? []),
          count($section['charts'] ?? []),
          count($section['tables'] ?? [])
        ),
      ];
    }

    return $actions;
  }

  /**
   * Extracts a float metric from overview cards.
   */
  private function extractOverviewMetricFloat(array $cards, string $title): float {
    foreach ($cards as $card) {
      if ((string) ($card['title'] ?? '') === $title) {
        return $this->toFloat($card['value'] ?? 0);
      }
    }
    return 0.0;
  }

  /**
   * Normalizes a metric value to float.
   */
  private function toFloat(mixed $value): float {
    if (is_numeric($value)) {
      return (float) $value;
    }

    $normalized = preg_replace('/[^0-9.,-]/', '', (string) $value);
    $normalized = str_replace(',', '', (string) $normalized);

    return is_numeric($normalized) ? (float) $normalized : 0.0;
  }

  /**
   * Builds generation feedback rows.
   */
  private function buildGenerationFeedback(array $preview): array {
    $feedback = [];

    foreach (($preview['sections'] ?? []) as $section) {
      $feedback[] = $this->t(
        '@title loaded with @cards KPI cards, @charts charts, and @tables evidence tables.',
        [
          '@title' => (string) ($section['title'] ?? 'Section'),
          '@cards' => count($section['summary_cards'] ?? []),
          '@charts' => count($section['charts'] ?? []),
          '@tables' => count($section['tables'] ?? []),
        ],
      )->render();
    }

    $feedback[] = $this->t(
      'Preview metadata resolved for environment @environment, detail level @detail, and report range @range.',
      [
        '@environment' => (string) ($preview['environment'] ?? 'unknown'),
        '@detail' => (string) ($preview['detail_level'] ?? 'unknown'),
        '@range' => (string) ($preview['range_label'] ?? 'unknown'),
      ],
    )->render();

    if (!empty($preview['cross_section_insights'])) {
      $feedback[] = $this->t(
        'Cross-section insight engine produced @count correlated findings.',
        [
          '@count' => count($preview['cross_section_insights'] ?? []),
        ],
      )->render();
    }

    if (!empty($preview['priority_actions'])) {
      $feedback[] = $this->t(
        'Priority action planner generated @count ranked actions.',
        [
          '@count' => count($preview['priority_actions'] ?? []),
        ],
      )->render();
    }

    if (!empty($preview['report_readiness'])) {
      $feedback[] = $this->t(
        'Report readiness block generated with @count delivery checks.',
        [
          '@count' => count($preview['report_readiness'] ?? []),
        ],
      )->render();
    }

    if (!empty($preview['appendix_cards'])) {
      $feedback[] = $this->t(
        'Appendix snapshot prepared with @count structural notes.',
        [
          '@count' => count($preview['appendix_cards'] ?? []),
        ],
      )->render();
    }

    if (!empty($preview['impact'])) {
      $feedback[] = $this->t(
        'Observability overhead estimation included with @contributors contributors.',
        [
          '@contributors' => count($preview['impact']['contributors'] ?? []),
        ],
      )->render();
    }

    return $feedback;
  }

  /**
   * Normalizes a dashboard dataset table.
   */
  private function normalizeDatasetTable(array $dataset, int $limit): array {
    $headers = [];
    foreach (($dataset['columns'] ?? []) as $column) {
      $headers[] = [
        'key' => (string) ($column['key'] ?? ''),
        'label' => (string) ($column['label'] ?? ''),
      ];
    }

    $rows = [];
    foreach (array_slice($dataset['rows'] ?? [], 0, $limit) as $row) {
      $normalizedRow = [];
      foreach ($headers as $header) {
        $normalizedRow[$header['key']] = (string) ($row[$header['key']] ?? '—');
      }
      $rows[] = $normalizedRow;
    }

    return [
      'title' => (string) ($dataset['label'] ?? 'Dataset'),
      'description' => (string) ($dataset['description'] ?? ''),
      'headers' => $headers,
      'rows' => $rows,
      'empty_message' => (string) ($dataset['empty_message'] ?? 'No rows are currently available for this dataset.'),
    ];
  }

  /**
   * Normalizes the cache runtime records table.
   */
  private function normalizeRuntimeRecordsTable(array $section, int $limit): array {
    $headers = [
      ['key' => 'time', 'label' => 'Time'],
      ['key' => 'event_type', 'label' => 'Event Type'],
      ['key' => 'cache_layer', 'label' => 'Layer'],
      ['key' => 'method', 'label' => 'Method'],
      ['key' => 'status', 'label' => (string) $this->t('Status')],
    ];

    $rows = [];
    foreach (array_slice($section['rows'] ?? [], 0, $limit) as $row) {
      $rows[] = [
        'time' => (string) ($row['time'] ?? '—'),
        'event_type' => (string) ($row['event_type'] ?? '—'),
        'cache_layer' => (string) ($row['cache_layer'] ?? '—'),
        'method' => (string) ($row['method'] ?? '—'),
        'status' => (string) ($row['status'] ?? '—'),
      ];
    }

    return [
      'title' => (string) ($section['title'] ?? 'Cache Runtime Records'),
      'description' => (string) ($section['description'] ?? ''),
      'headers' => $headers,
      'rows' => $rows,
      'empty_message' => (string) ($section['empty_message'] ?? 'No cache runtime records are available for the selected range.'),
    ];
  }

  /**
   * Normalizes the database route table.
   */
  private function normalizeRouteTable(array $section, int $limit): array {
    $headers = [
      ['key' => 'route_path', 'label' => 'Route / Path'],
      ['key' => 'requests', 'label' => 'Requests'],
      ['key' => 'avg_db_time', 'label' => 'Avg DB Time'],
      ['key' => 'p95_db_time', 'label' => 'P95 DB Time'],
      ['key' => 'avg_queries', 'label' => 'Avg Queries'],
      ['key' => 'slow_rate', 'label' => 'Slow Req Rate'],
    ];

    $rows = [];
    foreach (array_slice($section['rows'] ?? [], 0, $limit) as $row) {
      $rows[] = [
        'route_path' => trim(((string) ($row['route'] ?? '—')) . ' | ' . ((string) ($row['path'] ?? '—'))),
        'requests' => (string) ($row['requests'] ?? '0'),
        'avg_db_time' => (string) ($row['avg_db_time'] ?? '0 ms'),
        'p95_db_time' => (string) ($row['p95_db_time'] ?? '0 ms'),
        'avg_queries' => (string) ($row['avg_queries'] ?? '0'),
        'slow_rate' => (string) ($row['slow_rate'] ?? '0%'),
      ];
    }

    return [
      'title' => (string) ($section['title'] ?? 'Most DB-Expensive Routes'),
      'description' => 'Route-level database pressure evidence used to identify the most expensive execution paths.',
      'headers' => $headers,
      'rows' => $rows,
      'empty_message' => 'No database-intensive route rows are available for the selected range.',
    ];
  }

  /**
   * Normalizes the latest slow query records table.
   */
  private function normalizeLatestRecordsTable(array $section, int $limit): array {
    $headers = [
      ['key' => 'time', 'label' => 'Time'],
      ['key' => 'route_name', 'label' => 'Route'],
      ['key' => 'duration_ms', 'label' => 'Duration'],
      ['key' => 'query_hash', 'label' => 'Query Hash'],
    ];

    $rows = [];
    foreach (array_slice($section['rows'] ?? [], 0, $limit) as $row) {
      $rows[] = [
        'time' => (string) ($row['recorded_at'] ?? $row['time'] ?? '—'),
        'route_name' => (string) ($row['route_name'] ?? '—'),
        'duration_ms' => (string) ($row['duration_ms'] ?? '—'),
        'query_hash' => (string) ($row['query_hash'] ?? '—'),
      ];
    }

    return [
      'title' => (string) ($section['title'] ?? 'Latest Slow Query Records'),
      'description' => 'Recent slow-query evidence used to move from high-level diagnosis to concrete investigation.',
      'headers' => $headers,
      'rows' => $rows,
      'empty_message' => (string) ($section['empty_message'] ?? 'No slow query records are available for the selected time range.'),
    ];
  }

  /**
   * Extracts a value from a flat card list.
   */
  private function extractCardValue(array $cards, string $label, string $default = '0'): string {
    foreach ($cards as $card) {
      if ((string) ($card['label'] ?? '') === $label) {
        return (string) ($card['value'] ?? $default);
      }
    }
    return $default;
  }

  /**
   * Finds the value of an executive KPI card.
   */
  private function findExecutiveKpiValue(array $cards, string $title): string {
    foreach ($cards as $card) {
      if ((string) ($card['title'] ?? '') === $title) {
        return (string) ($card['value'] ?? '0');
      }
    }
    return '0';
  }

  /**
   * Resolves overview card descriptions.
   */
  private function resolveOverviewCardDescription(string $label): string {
    return match ($label) {
      'Captured Requests' => (string) $this->t('Total request traces captured for the selected report window.'),
      'Peak Requests/sec' => (string) $this->t('Highest short-lived throughput bucket identified across the selected timeline.'),
      'Average Latency' => (string) $this->t('Mean request duration across the currently observed traffic sample.'),
      'P95 Latency' => (string) $this->t('Slow tail latency that highlights the most degraded user experiences.'),
      default => (string) $this->t('Overview metric extracted from the main request timeline section.'),
    };
  }

  /**
   * Builds overview charts for the preview.
   */
  private function buildOverviewPreviewCharts(array $payload, array $narrative): array {
    $charts = [];
    $requestTimeline = $payload['sections']['request_timeline'] ?? [];
    if (!empty($requestTimeline['enabled']) && !empty($requestTimeline['series'])) {
      $chartId = 'report-preview-request-timeline';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.request_timeline.title', 'Request Activity'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.request_timeline.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.request_timeline.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.request_timeline.interpretation', [])),
        'settings' => $this->normalizeOverviewTimelineChart($chartId, $requestTimeline['series']),
      ];
    }

    $ajaxActivity = $payload['sections']['ajax_activity'] ?? [];
    if (!empty($ajaxActivity['enabled']) && !empty($ajaxActivity['series'])) {
      $chartId = 'report-preview-ajax-activity';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.ajax_activity.title', 'AJAX Activity'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.ajax_activity.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.ajax_activity.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.ajax_activity.interpretation', [])),
        'settings' => $this->normalizeOverviewTimelineChart($chartId, $ajaxActivity['series']),
      ];
    }

    $technicalInsights = $payload['sections']['technical_insights']['datasets'] ?? [];

    $slowEndpointsRows = $technicalInsights['slow_endpoints']['rows'] ?? [];
    if ($slowEndpointsRows !== []) {
      $chartId = 'report-preview-slow-endpoints';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.slow_endpoints.title', 'Slow Endpoints by Average Duration'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.slow_endpoints.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.slow_endpoints.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.slow_endpoints.interpretation', [])),
        'settings' => $this->normalizeSlowEndpointsChart($chartId, $slowEndpointsRows),
      ];
    }

    $externalRequestRows = $technicalInsights['external_requests']['rows'] ?? [];
    if ($externalRequestRows !== []) {
      $chartId = 'report-preview-external-requests';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.external_requests.title', 'External Request Share'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.external_requests.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.external_requests.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.overview.preview_charts.external_requests.interpretation', [])),
        'settings' => $this->normalizeExternalRequestsPieChart($chartId, $externalRequestRows),
      ];
    }

    return $charts;
  }

  /**
   * Builds cache charts for the preview.
   */
  private function buildCachePreviewCharts(array $payload, array $narrative): array {
    $charts = [];
    $section = $payload['sections']['cache_observability'] ?? [];
    $trend = $section['activity_trend'] ?? [];

    if (!empty($trend['enabled']) && !empty($trend['series'])) {
      $chartId = 'report-preview-cache-activity-trend';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.activity_trend.title', 'Cache Activity Trend'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.activity_trend.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.activity_trend.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.activity_trend.interpretation', [])),
        'settings' => $this->normalizeCacheTrendChart($chartId, $trend),
      ];
    }

    if (!empty($section['efficiency_score'])) {
      $chartId = 'report-preview-cache-efficiency-gauge';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.efficiency_gauge.title', 'Cache Efficiency Gauge'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.efficiency_gauge.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.efficiency_gauge.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.efficiency_gauge.interpretation', [])),
        'settings' => $this->normalizeEfficiencyGaugeChart($chartId, $section['efficiency_score']),
      ];
    }

    if (!empty($section['behavior']['overall'])) {
      $chartId = 'report-preview-cache-behavior-share';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.behavior_share.title', 'Cache Behaviour Share'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.behavior_share.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.behavior_share.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.behavior_share.interpretation', [])),
        'settings' => $this->normalizeCacheBehaviorPieChart($chartId, $section['behavior']['overall']),
      ];
    }

    if (!empty($section['behavior']['layers'])) {
      $chartId = 'report-preview-cache-layer-rates';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.layer_rates.title', 'Cache Layer Hit and Miss Rates'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.layer_rates.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.layer_rates.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.layer_rates.interpretation', [])),
        'settings' => $this->normalizeCacheLayerRatesChart($chartId, $section['behavior']['layers']),
      ];
    }

    if (!empty($section['cacheability'])) {
      $chartId = 'report-preview-cacheability-radar';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.cacheability_radar.title', 'Cacheability Profile Radar'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.cacheability_radar.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.cacheability_radar.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.cache.preview_charts.cacheability_radar.interpretation', [])),
        'settings' => $this->normalizeCacheabilityRadarChart($chartId, $section['cacheability']),
      ];
    }

    return $charts;
  }

  /**
   * Builds database charts for the preview.
   */
  private function buildDatabasePreviewCharts(array $payload, array $narrative): array {
    $charts = [];
    $section = $payload['sections']['database_observability'] ?? [];

    $pressureTrend = $section['pressure_trend'] ?? [];
    if (!empty($pressureTrend['enabled'])) {
      $chartId = 'report-preview-database-pressure';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.pressure_trend.title', 'Database Pressure Trend'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.pressure_trend.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.pressure_trend.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.pressure_trend.interpretation', [])),
        'settings' => $this->normalizeDatabasePressureChart($chartId, $pressureTrend),
      ];
    }

    $requestComparison = $section['request_comparison'] ?? [];
    if (!empty($requestComparison['enabled'])) {
      $chartId = 'report-preview-db-vs-request-time';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.request_comparison.title', 'DB vs Request Time'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.request_comparison.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.request_comparison.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.request_comparison.interpretation', [])),
        'settings' => $this->normalizeDatabaseScatterChart($chartId, $requestComparison),
      ];
    }

    if (!empty($section['route_table']['rows'])) {
      $chartId = 'report-preview-db-route-cost';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.route_cost.title', 'Most DB-Expensive Routes'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.route_cost.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.route_cost.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.route_cost.interpretation', [])),
        'settings' => $this->normalizeDatabaseRouteCostChart($chartId, $section['route_table']['rows']),
      ];
    }

    if (!empty($section['hotspots']['query_hashes'])) {
      $chartId = 'report-preview-db-hotspot-hashes';
      $charts[] = [
        'chart_id' => $chartId,
        'title' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.hotspot_hashes.title', 'Slow Query Hash Hotspots'),
        'description' => (string) $this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.hotspot_hashes.description', ''),
        'reading_guide' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.hotspot_hashes.reading_guide', [])),
        'interpretation' => $this->normalizeTextList($this->narrativeLoader->getValue($narrative, 'sections.database.preview_charts.hotspot_hashes.interpretation', [])),
        'settings' => $this->normalizeDatabaseHotspotHashChart($chartId, $section['hotspots']['query_hashes']),
      ];
    }

    return $charts;
  }

  /**
   * Normalizes an overview timeline chart.
   */
  private function normalizeOverviewTimelineChart(string $chartId, array $series): array {
    $labels = array_map(
      static fn(array $item): string => (string) ($item['label'] ?? ''),
      $series['requests_per_second'] ?? []
    );

    return [
      'chart_id' => $chartId,
      'renderer' => 'timeseries',
      'labels' => $labels,
      'y_axes' => [
        ['type' => 'value', 'name' => 'Req/s', 'position' => 'left'],
        ['type' => 'value', 'name' => 'Latency (ms)', 'position' => 'right'],
        ['type' => 'value', 'name' => 'Error %', 'position' => 'right', 'offset' => 64],
      ],
      'series' => [
        [
          'name' => 'Requests/sec',
          'type' => 'line',
          'y_axis_index' => 0,
          'area' => TRUE,
          'data' => array_map(static fn(array $item): float => (float) ($item['value'] ?? 0), $series['requests_per_second'] ?? []),
        ],
        [
          'name' => 'Average Duration',
          'type' => 'line',
          'y_axis_index' => 1,
          'data' => array_map(static fn(array $item): float => (float) ($item['value'] ?? 0), $series['average_duration'] ?? []),
        ],
        [
          'name' => 'P95 Latency',
          'type' => 'line',
          'y_axis_index' => 1,
          'data' => array_map(static fn(array $item): float => (float) ($item['value'] ?? 0), $series['p95_duration'] ?? []),
        ],
        [
          'name' => 'Error Rate',
          'type' => 'line',
          'y_axis_index' => 2,
          'data' => array_map(static fn(array $item): float => (float) ($item['value'] ?? 0), $series['error_rate'] ?? []),
        ],
      ],
    ];
  }

  /**
   * Normalizes slow endpoint evidence into a horizontal bar chart.
   */
  private function normalizeSlowEndpointsChart(string $chartId, array $rows): array {
    usort($rows, static fn(array $a, array $b): int => (int) round(((float) ($b['avg_duration'] ?? 0)) * 1000) <=> (int) round(((float) ($a['avg_duration'] ?? 0)) * 1000));
    $rows = array_slice($rows, 0, 6);

    return [
      'chart_id' => $chartId,
      'renderer' => 'horizontal_bar',
      'categories' => array_map(fn(array $row): string => $this->truncateLabel((string) ($row['path'] ?? $row['route_name'] ?? 'unknown')), $rows),
      'series' => [
        [
          'name' => 'Average Duration (ms)',
          'data' => array_map(static fn(array $row): float => (float) ($row['avg_duration'] ?? 0), $rows),
        ],
      ],
    ];
  }

  /**
   * Normalizes external requests into a pie chart.
   */
  private function normalizeExternalRequestsPieChart(string $chartId, array $rows): array {
    usort($rows, static fn(array $a, array $b): int => ((int) ($b['request_count'] ?? 0)) <=> ((int) ($a['request_count'] ?? 0)));
    $rows = array_slice($rows, 0, 6);

    return [
      'chart_id' => $chartId,
      'renderer' => 'pie',
      'series_name' => 'External request share',
      'data' => array_map(fn(array $row): array => [
        'name' => $this->truncateLabel((string) ($row['target'] ?? 'unknown')),
        'value' => (int) ($row['request_count'] ?? 0),
      ], $rows),
    ];
  }

  /**
   * Normalizes a cache trend chart.
   */
  private function normalizeCacheTrendChart(string $chartId, array $trend): array {
    $labels = array_map(
      static fn(array $item): string => (string) ($item['label'] ?? ''),
      $trend['series']['response_cache_events'] ?? []
    );

    return [
      'chart_id' => $chartId,
      'renderer' => 'timeseries',
      'labels' => $labels,
      'y_axes' => [
        ['type' => 'value', 'name' => 'Events', 'position' => 'left'],
      ],
      'series' => [
        [
          'name' => 'Response Cache Events',
          'type' => 'line',
          'y_axis_index' => 0,
          'data' => array_map(static fn(array $item): float => (float) ($item['value'] ?? 0), $trend['series']['response_cache_events'] ?? []),
        ],
        [
          'name' => 'Cacheability Events',
          'type' => 'line',
          'y_axis_index' => 0,
          'data' => array_map(static fn(array $item): float => (float) ($item['value'] ?? 0), $trend['series']['cacheability_events'] ?? []),
        ],
        [
          'name' => 'Invalidation Events',
          'type' => 'bar',
          'y_axis_index' => 0,
          'data' => array_map(static fn(array $item): float => (float) ($item['value'] ?? 0), $trend['series']['invalidation_events'] ?? []),
        ],
      ],
    ];
  }

  /**
   * Normalizes the cache efficiency score into a gauge chart.
   */
  private function normalizeEfficiencyGaugeChart(string $chartId, array $score): array {
    return [
      'chart_id' => $chartId,
      'renderer' => 'gauge',
      'series_name' => 'Efficiency score',
      'value' => (float) ($score['value'] ?? 0),
      'max' => (float) ($score['max'] ?? 100),
    ];
  }

  /**
   * Normalizes overall cache behaviour into a pie chart.
   */
  private function normalizeCacheBehaviorPieChart(string $chartId, array $overall): array {
    $data = [];
    foreach ([
      'hits' => 'Hits',
      'misses' => 'Misses',
      'uncacheable' => 'Uncacheable',
      'bypass' => 'Bypass',
      'other' => 'Other',
    ] as $key => $label) {
      $value = (int) ($overall[$key] ?? 0);
      if ($value > 0) {
        $data[] = ['name' => $label, 'value' => $value];
      }
    }

    return [
      'chart_id' => $chartId,
      'renderer' => 'pie',
      'series_name' => 'Cache behaviour share',
      'data' => $data,
    ];
  }

  /**
   * Normalizes layer hit and miss rates into a category bar chart.
   */
  private function normalizeCacheLayerRatesChart(string $chartId, array $layers): array {
    $ordered = [];
    foreach (['page_cache', 'dynamic_page_cache'] as $key) {
      if (!empty($layers[$key])) {
        $ordered[] = $layers[$key];
      }
    }

    return [
      'chart_id' => $chartId,
      'renderer' => 'category_bar',
      'categories' => array_map(static fn(array $layer): string => (string) ($layer['label'] ?? 'Layer'), $ordered),
      'series' => [
        [
          'name' => 'Hit rate %',
          'data' => array_map(static fn(array $layer): float => (float) ($layer['hit_rate'] ?? 0), $ordered),
        ],
        [
          'name' => 'Miss rate %',
          'data' => array_map(static fn(array $layer): float => (float) ($layer['miss_rate'] ?? 0), $ordered),
        ],
      ],
    ];
  }

  /**
   * Normalizes cacheability summary values into a radar chart.
   */
  private function normalizeCacheabilityRadarChart(string $chartId, array $cacheability): array {
    $summary = $cacheability['summary'] ?? [];

    return [
      'chart_id' => $chartId,
      'renderer' => 'radar',
      'indicators' => [
        ['name' => 'Cacheable ratio', 'max' => 100],
        ['name' => 'Uncacheable ratio', 'max' => 100],
        ['name' => 'Avg tags', 'max' => max(10, (int) ceil((float) ($summary['avg_tags'] ?? 0) + 2))],
        ['name' => 'Avg contexts', 'max' => max(10, (int) ceil((float) ($summary['avg_contexts'] ?? 0) + 2))],
        ['name' => 'Avg max-age', 'max' => max(3600, (int) ceil((float) ($summary['avg_max_age'] ?? 0) + 300))],
      ],
      'series_name' => 'Cacheability profile',
      'values' => [
        round((float) ($summary['cacheable_ratio'] ?? 0), 2),
        round((float) ($summary['uncacheable_ratio'] ?? 0), 2),
        round((float) ($summary['avg_tags'] ?? 0), 2),
        round((float) ($summary['avg_contexts'] ?? 0), 2),
        round((float) ($summary['avg_max_age'] ?? 0), 2),
      ],
    ];
  }

  /**
   * Normalizes the database pressure chart.
   */
  private function normalizeDatabasePressureChart(string $chartId, array $chart): array {
    return [
      'chart_id' => $chartId,
      'renderer' => 'timeseries',
      'labels' => $chart['labels'] ?? [],
      'y_axes' => [
        ['type' => 'value', 'name' => 'Average', 'position' => 'left'],
        ['type' => 'value', 'name' => 'Slow Events', 'position' => 'right'],
      ],
      'series' => [
        [
          'name' => 'Average DB Time',
          'type' => 'line',
          'y_axis_index' => 0,
          'data' => $chart['series'][0]['data'] ?? [],
        ],
        [
          'name' => 'Average Query Count',
          'type' => 'line',
          'y_axis_index' => 0,
          'data' => $chart['series'][1]['data'] ?? [],
        ],
        [
          'name' => 'Slow Query Events',
          'type' => 'bar',
          'y_axis_index' => 1,
          'data' => $chart['series'][2]['data'] ?? [],
        ],
      ],
    ];
  }

  /**
   * Normalizes the database scatter chart.
   */
  private function normalizeDatabaseScatterChart(string $chartId, array $chart): array {
    return [
      'chart_id' => $chartId,
      'renderer' => 'scatter',
      'points' => $chart['points'] ?? [],
    ];
  }

  /**
   * Normalizes route cost evidence into a horizontal bar chart.
   */
  private function normalizeDatabaseRouteCostChart(string $chartId, array $rows): array {
    usort($rows, fn(array $a, array $b): int => (int) round($this->toFloat($b['avg_db_time'] ?? 0) * 1000) <=> (int) round($this->toFloat($a['avg_db_time'] ?? 0) * 1000));
    $rows = array_slice($rows, 0, 6);

    return [
      'chart_id' => $chartId,
      'renderer' => 'horizontal_bar',
      'categories' => array_map(fn(array $row): string => $this->truncateLabel((string) ($row['route'] ?? 'unknown')), $rows),
      'series' => [
        [
          'name' => 'Average DB Time (ms)',
          'data' => array_map(fn(array $row): float => $this->toFloat($row['avg_db_time'] ?? 0), $rows),
        ],
      ],
    ];
  }

  /**
   * Normalizes hotspot hashes into a category bar chart.
   */
  private function normalizeDatabaseHotspotHashChart(string $chartId, array $hashes): array {
    $hashes = array_slice($hashes, 0, 6);

    return [
      'chart_id' => $chartId,
      'renderer' => 'category_bar',
      'categories' => array_map(fn(array $row): string => $this->truncateLabel((string) ($row['hash'] ?? 'unknown'), 18), $hashes),
      'series' => [
        [
          'name' => 'Occurrences',
          'data' => array_map(static fn(array $row): int => (int) ($row['count'] ?? 0), $hashes),
        ],
        [
          'name' => 'Max duration (ms)',
          'data' => array_map(static fn(array $row): float => (float) ($row['max'] ?? 0), $hashes),
        ],
      ],
    ];
  }

  /**
   * Truncates long chart labels for compact preview rendering.
   */
  private function truncateLabel(string $label, int $limit = 28): string {
    $label = trim($label);
    if ($label === '') {
      return 'Unknown';
    }

    return mb_strlen($label) > $limit ? mb_substr($label, 0, $limit - 1) . '…' : $label;
  }

  /**
   * Builds a fixed time range value object from the selected key.
   */
  private function buildTimeRange(string $rangeKey): TimeRange {
    $seconds = match ($rangeKey) {
      '15m' => 900,
      '1h' => 3600,
      '6h' => 21600,
      '24h' => 86400,
      '7d' => 604800,
      default => 1800,
    };

    $label = match ($rangeKey) {
      '15m' => 'Last 15 minutes',
      '1h' => 'Last 1 hour',
      '6h' => 'Last 6 hours',
      '24h' => 'Last 24 hours',
      '7d' => 'Last 7 days',
      default => 'Last 30 minutes',
    };

    $endTimestamp = time();
    $startTimestamp = $endTimestamp - $seconds;

    return new TimeRange($startTimestamp, $endTimestamp, $label, $rangeKey);
  }

  /**
   * Normalizes a narrative list into a plain list of strings.
   */
  private function normalizeTextList(mixed $items): array {
    if (!is_array($items)) {
      return [];
    }

    $normalized = [];
    foreach ($items as $item) {
      $value = trim((string) $item);
      if ($value !== '') {
        $normalized[] = $value;
      }
    }

    return $normalized;
  }

  /**
   * Replaces placeholder tokens in a narrative string.
   */
  private function replaceTokens(string $template, array $tokens): string {
    return strtr($template, $tokens);
  }

}
