<?php

declare(strict_types=1);

namespace Drupal\native_observability\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Native Observability settings form.
 *
 * This form allows site administrators to configure which data is captured
 * and persisted, following a privacy-first, opt-in approach.
 */
final class NativeObservabilitySettingsForm extends ConfigFormBase {

  public const CONFIG_NAME = 'native_observability.settings';

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

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames(): array {
    return [self::CONFIG_NAME];
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array {
    $config = $this->config(self::CONFIG_NAME);

    $form['intro'] = [
      '#type' => 'item',
      '#markup' => $this->t('Configure which request/response data is captured and stored. Defaults are privacy-friendly. Enable more data only when needed for debugging.'),
    ];

    $form['capture'] = [
      '#type' => 'details',
      '#title' => $this->t('Capture settings'),
      '#open' => TRUE,
    ];

    $form['capture']['enabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enable capture'),
      '#default_value' => (bool) $config->get('capture.enabled'),
      '#description' => $this->t('When disabled, no traces will be recorded.'),
    ];

    $form['capture']['exclude_own_routes'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Exclude module-own routes from tracing'),
      // The config key may be absent on installations predating this option;
      // default to TRUE so the first form load shows the recommended state.
      '#default_value' => (bool) ($config->get('capture.exclude_own_routes') ?? TRUE),
      '#description' => $this->t('When enabled, requests to any route belonging to the Native Observability module itself (dashboards, Prometheus metrics endpoint, admin configuration forms, reports) are silently skipped and never stored in the trace table or forwarded to the OTLP collector. <br><strong>Recommended: enabled.</strong> Disabling this option is useful only when actively debugging the observability module itself. With this option off, every time you open the trace list, view a dashboard, or Prometheus scrapes the metrics endpoint, a new trace row is written — creating background noise that inflates request counters and skews latency percentiles.'),
    ];

    // Core technical fields (method/path are required for storage indexing).
    $form['capture']['core_fields'] = [
      '#type' => 'details',
      '#title' => $this->t('Core technical fields'),
      '#open' => TRUE,
    ];

    $form['capture']['core_fields']['request_id'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Store request ID'),
      '#default_value' => (bool) $config->get('capture.request_id'),
    ];

    $form['capture']['core_fields']['method'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Store HTTP method'),
      '#default_value' => TRUE,
      '#disabled' => TRUE,
      '#description' => $this->t('Required by the storage schema for indexing.'),
    ];

    $form['capture']['core_fields']['path'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Store request path'),
      '#default_value' => TRUE,
      '#disabled' => TRUE,
      '#description' => $this->t('Required by the storage schema for indexing.'),
    ];

    $form['capture']['core_fields']['route_name'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Store route name'),
      '#default_value' => (bool) $config->get('capture.route_name'),
    ];

    $form['capture']['core_fields']['status_code'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Store response status code'),
      '#default_value' => (bool) $config->get('capture.status_code'),
    ];

    $form['capture']['core_fields']['duration_ms'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Store duration (ms)'),
      '#default_value' => (bool) $config->get('capture.duration_ms'),
    ];

    $form['capture']['core_fields']['exception'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Store exception metadata'),
      '#default_value' => (bool) $config->get('capture.exception'),
      '#description' => $this->t('Stores exception class/message/code (best-effort).'),
    ];

    // Identity fields.
    $form['capture']['identity'] = [
      '#type' => 'details',
      '#title' => $this->t('Identity-related fields'),
      '#open' => FALSE,
    ];

    $form['capture']['identity']['store_uid'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Store Drupal user ID'),
      '#default_value' => (bool) $config->get('capture.store_uid'),
      '#description' => $this->t('May be considered personal data. Disable if not needed.'),
    ];

    // Client IP.
    $form['capture']['client_ip'] = [
      '#type' => 'details',
      '#title' => $this->t('Client IP address'),
      '#open' => FALSE,
    ];

    $form['capture']['client_ip']['client_ip_mode'] = [
      '#type' => 'select',
      '#title' => $this->t('IP capture mode'),
      '#default_value' => (string) ($config->get('capture.client_ip_mode') ?? 'anonymized'),
      '#options' => [
        'off' => $this->t('Off'),
        'anonymized' => $this->t('Anonymized'),
        'hashed' => $this->t('Hashed'),
        'full' => $this->t('Full (not recommended)'),
      ],
      '#description' => $this->t('Anonymized masks the address. Hashed stores a one-way hash using the salt below.'),
    ];

    $form['capture']['client_ip']['client_ip_hash_salt'] = [
      '#type' => 'textfield',
      '#title' => $this->t('IP hash salt'),
      '#default_value' => (string) ($config->get('capture.client_ip_hash_salt') ?? ''),
      '#description' => $this->t('Used only when IP capture mode is "Hashed". Keep it secret.'),
      '#states' => [
        'visible' => [
          ':input[name="client_ip_mode"]' => ['value' => 'hashed'],
        ],
      ],
    ];

    // User Agent.
    $form['capture']['user_agent'] = [
      '#type' => 'details',
      '#title' => $this->t('User Agent'),
      '#open' => FALSE,
    ];

    $form['capture']['user_agent']['user_agent_mode'] = [
      '#type' => 'select',
      '#title' => $this->t('User Agent capture mode'),
      '#default_value' => (string) ($config->get('capture.user_agent_mode') ?? 'family'),
      '#options' => [
        'off' => $this->t('Off'),
        'family' => $this->t('Family (browser/OS summary)'),
        'full' => $this->t('Full string (higher fingerprinting risk)'),
      ],
    ];

    // Headers.
    $form['capture']['headers'] = [
      '#type' => 'details',
      '#title' => $this->t('Request headers'),
      '#open' => FALSE,
    ];

    $form['capture']['headers']['headers_mode'] = [
      '#type' => 'select',
      '#title' => $this->t('Headers capture mode'),
      '#default_value' => (string) ($config->get('capture.headers_mode') ?? 'off'),
      '#options' => [
        'off' => $this->t('Off'),
        'allowlist' => $this->t('Allowlist only'),
      ],
      '#description' => $this->t('Sensitive headers (Authorization, Cookie, Set-Cookie, etc.) are never stored.'),
    ];

    $form['capture']['headers']['headers_allowlist_text'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Allowed header names'),
      '#default_value' => implode("\n", (array) ($config->get('capture.headers_allowlist') ?? [])),
      '#description' => $this->t('One header name per line (case-insensitive). Example: Accept, Content-Type, Referer'),
      '#states' => [
        'visible' => [
          ':input[name="headers_mode"]' => ['value' => 'allowlist'],
        ],
      ],
    ];

    // Query string.
    $form['capture']['query'] = [
      '#type' => 'details',
      '#title' => $this->t('Query string parameters'),
      '#open' => FALSE,
    ];

    $form['capture']['query']['query_mode'] = [
      '#type' => 'select',
      '#title' => $this->t('Query capture mode'),
      '#default_value' => (string) ($config->get('capture.query_mode') ?? 'off'),
      '#options' => [
        'off' => $this->t('Off'),
        'keys_only' => $this->t('Keys only'),
        'allowlist' => $this->t('Allowlist (store values)'),
        'full' => $this->t('Full (stores all values, redacts sensitive keys)'),
      ],
    ];

    $form['capture']['query']['query_allowlist_text'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Allowed query keys'),
      '#default_value' => implode("\n", (array) ($config->get('capture.query_allowlist') ?? [])),
      '#description' => $this->t('One key per line. Only used when mode is "Allowlist".'),
      '#states' => [
        'visible' => [
          ':input[name="query_mode"]' => ['value' => 'allowlist'],
        ],
      ],
    ];

    // Body.
    $form['capture']['body'] = [
      '#type' => 'details',
      '#title' => $this->t('Request body'),
      '#open' => FALSE,
    ];

    $form['capture']['body']['body_mode'] = [
      '#type' => 'select',
      '#title' => $this->t('Body capture mode'),
      '#default_value' => (string) ($config->get('capture.body_mode') ?? 'off'),
      '#options' => [
        'off' => $this->t('Off'),
        'keys_only' => $this->t('Keys only'),
        'allowlist' => $this->t('Allowlist (store values)'),
        'full' => $this->t('Full (stores all values, redacts sensitive keys)'),
      ],
      '#description' => $this->t('By default only parsed request parameters are captured (not raw streams). Use with care.'),
    ];

    $form['capture']['body']['body_allowlist_text'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Allowed body keys'),
      '#default_value' => implode("\n", (array) ($config->get('capture.body_allowlist') ?? [])),
      '#description' => $this->t('One key per line. Only used when mode is "Allowlist".'),
      '#states' => [
        'visible' => [
          ':input[name="body_mode"]' => ['value' => 'allowlist'],
        ],
      ],
    ];

    // Retention.
    $form['retention'] = [
      '#type' => 'details',
      '#title' => $this->t('Retention policy'),
      '#open' => FALSE,
    ];

    $form['retention']['days'] = [
      '#type' => 'number',
      '#title' => $this->t('Retention days'),
      '#default_value' => (int) ($config->get('retention.days') ?? 14),
      '#min' => 0,
      '#description' => $this->t('Delete traces older than this number of days. Set to 0 to disable time-based purging.'),
    ];

    $form['retention']['max_rows'] = [
      '#type' => 'number',
      '#title' => $this->t('Maximum stored rows'),
      '#default_value' => (int) ($config->get('retention.max_rows') ?? 50000),
      '#min' => 0,
      '#description' => $this->t('Optional safety cap. Set to 0 to disable size-based purging.'),
    ];

    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    $days = (int) $form_state->getValue('days');
    $max_rows = (int) $form_state->getValue('max_rows');
    if ($days < 0) {
      $form_state->setErrorByName('days', $this->t('Retention days must be >= 0.'));
    }
    if ($max_rows < 0) {
      $form_state->setErrorByName('max_rows', $this->t('Maximum rows must be >= 0.'));
    }

    parent::validateForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $config = $this->configFactory->getEditable(self::CONFIG_NAME);

    $headers_allowlist = $this->parseLines((string) $form_state->getValue('headers_allowlist_text'));
    $query_allowlist = $this->parseLines((string) $form_state->getValue('query_allowlist_text'));
    $body_allowlist = $this->parseLines((string) $form_state->getValue('body_allowlist_text'));

    $config
      ->set('capture.enabled', (bool) $form_state->getValue('enabled'))
      ->set('capture.exclude_own_routes', (bool) $form_state->getValue('exclude_own_routes'))
    // Keep request correlation always on.
      ->set('capture.request_id', TRUE)
    // Required by storage schema.
      ->set('capture.method', TRUE)
    // Required by storage schema.
      ->set('capture.path', TRUE)
      ->set('capture.route_name', (bool) $form_state->getValue('route_name'))
      ->set('capture.status_code', (bool) $form_state->getValue('status_code'))
      ->set('capture.duration_ms', (bool) $form_state->getValue('duration_ms'))
      ->set('capture.exception', (bool) $form_state->getValue('exception'))
      ->set('capture.store_uid', (bool) $form_state->getValue('store_uid'))
      ->set('capture.client_ip_mode', (string) $form_state->getValue('client_ip_mode'))
      ->set('capture.client_ip_hash_salt', (string) $form_state->getValue('client_ip_hash_salt'))
      ->set('capture.user_agent_mode', (string) $form_state->getValue('user_agent_mode'))
      ->set('capture.headers_mode', (string) $form_state->getValue('headers_mode'))
      ->set('capture.headers_allowlist', $headers_allowlist)
      ->set('capture.query_mode', (string) $form_state->getValue('query_mode'))
      ->set('capture.query_allowlist', $query_allowlist)
      ->set('capture.body_mode', (string) $form_state->getValue('body_mode'))
      ->set('capture.body_allowlist', $body_allowlist)
      ->set('retention.days', (int) $form_state->getValue('days'))
      ->set('retention.max_rows', (int) $form_state->getValue('max_rows'))
      ->save();

    parent::submitForm($form, $form_state);
  }

  /**
   * Parses textarea lines into a normalized array (trimmed, unique).
   */
  private function parseLines(string $text): array {
    $lines = preg_split('/\R/u', $text) ?: [];
    $out = [];
    foreach ($lines as $line) {
      $line = trim($line);
      if ($line === '') {
        continue;
      }
      $out[] = $line;
    }
    $out = array_values(array_unique($out));
    return $out;
  }

}
