<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Form;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\ffmpeg_media\FFmpegCodecPluginManager;
use Drupal\ffmpeg_media\FFmpegOutputPluginManager;
use Drupal\ffmpeg_media\FFmpegProcessorPluginManager;
use Drupal\ffmpeg_media\Service\HardwareDetectorInterface;
use Drupal\ffmpeg_media\Service\ProfileFormValidator;
use Drupal\ffmpeg_media\Service\ProfileFormValidatorInterface;
use Drupal\ffmpeg_media\Service\ProfileManagerInterface;
use Drupal\ffmpeg_media\Service\ProfileOutputValidatorServiceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Form for managing conversion profiles.
 */
final class ProfileForm extends ConfigFormBase {

  /**
   * Constructs a ProfileForm object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
   *   The typed config manager.
   * @param \Drupal\ffmpeg_media\FFmpegCodecPluginManager $codecManager
   *   Codec plugin manager.
   * @param \Drupal\ffmpeg_media\FFmpegOutputPluginManager $outputManager
   *   Output plugin manager.
   * @param \Drupal\ffmpeg_media\FFmpegProcessorPluginManager $processorManager
   *   Processor plugin manager.
   * @param \Drupal\ffmpeg_media\Service\ProfileManagerInterface $profileManager
   *   Profile manager for cache invalidation on save.
   * @param \Drupal\ffmpeg_media\Service\HardwareDetectorInterface $hardwareDetector
   *   Hardware detector for GPU compatibility validation.
   * @param \Drupal\ffmpeg_media\Service\ProfileOutputValidatorServiceInterface $outputValidator
   *   Output configuration validator service.
   * @param \Drupal\ffmpeg_media\Service\ProfileFormValidatorInterface $profileFormValidator
   *   Form validator service for output/codec validation.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory,
    TypedConfigManagerInterface $typedConfigManager,
    protected readonly FFmpegCodecPluginManager $codecManager,
    protected readonly FFmpegOutputPluginManager $outputManager,
    protected readonly FFmpegProcessorPluginManager $processorManager,
    protected readonly ProfileManagerInterface $profileManager,
    protected readonly HardwareDetectorInterface $hardwareDetector,
    protected readonly ProfileOutputValidatorServiceInterface $outputValidator,
    protected readonly ProfileFormValidatorInterface $profileFormValidator,
  ) {
    parent::__construct($config_factory, $typedConfigManager);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('config.factory'),
      $container->get('config.typed'),
      $container->get(FFmpegCodecPluginManager::class),
      $container->get(FFmpegOutputPluginManager::class),
      $container->get(FFmpegProcessorPluginManager::class),
      $container->get(ProfileManagerInterface::class),
      $container->get(HardwareDetectorInterface::class),
      $container->get(ProfileOutputValidatorServiceInterface::class),
      $container->get(ProfileFormValidatorInterface::class),
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function getFormId(): string {
    return 'ffmpeg_media_profile';
  }

  /**
   * {@inheritdoc}
   *
   * @return array<int, string>
   *   Editable config names.
   */
  #[\Override]
  protected function getEditableConfigNames(): array {
    return [];
  }

  /**
   * {@inheritdoc}
   *
   * @param array<string, mixed> $form
   *   Form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   * @param string|null $profile_id
   *   Profile ID.
   * @param bool $clone
   *   Whether to clone the profile.
   *
   * @return array<string, mixed>
   *   Form elements.
   */
  #[\Override]
  public function buildForm(array $form, FormStateInterface $form_state, ?string $profile_id = NULL, bool $clone = FALSE): array {
    $isEdit = $profile_id !== NULL && !$clone;
    $configName = $profile_id !== NULL ? "ffmpeg_media.profile.{$profile_id}" : '';
    $config = $profile_id !== NULL ? $this->configFactory->get($configName) : NULL;

    $defaultLabel = $config?->get('label') ?? '';
    if ($clone && $defaultLabel !== '') {
      $defaultLabel = (string) $this->t('Copy of @label', ['@label' => $defaultLabel]);
    }

    $form['label'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Label'),
      '#default_value' => $defaultLabel,
      '#required' => TRUE,
    ];

    $form['profile_id'] = [
      '#type' => 'machine_name',
      '#title' => $this->t('Profile ID'),
      '#default_value' => $clone ? '' : ($profile_id ?? ''),
      '#machine_name' => [
        'exists' => [$this, 'profileExists'],
        'source' => ['label'],
      ],
      '#disabled' => $isEdit,
    ];

    $form['description'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Description'),
      '#default_value' => $config?->get('description') ?? '',
      '#rows' => 2,
    ];

    // Hardware acceleration requirement.
    $form['hardware_acceleration'] = [
      '#type' => 'select',
      '#title' => $this->t('Hardware acceleration'),
      '#description' => $this->t('When set to <em>Required</em>, only codecs with a GPU encoder on the detected hardware will be accepted. Outputs with incompatible codecs will be rejected at save time.'),
      '#options' => [
        '' => $this->t('Auto (use GPU if available, fall back to software)'),
        'required' => $this->t('Required (GPU only, no software fallback)'),
      ],
      '#default_value' => $config?->get('hardware_acceleration') ?? '',
    ];

    // Output configurations with AJAX support.
    $form['outputs'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Output Formats'),
      '#description' => $this->t('Configure output formats for this profile. You can add up to @max outputs.', [
        '@max' => ProfileFormValidator::MAX_OUTPUTS,
      ]),
      '#tree' => TRUE,
      '#prefix' => '<div id="outputs-wrapper" tabindex="-1">',
      '#suffix' => '</div>',
    ];

    $outputs = $form_state->get('outputs');
    if ($outputs === NULL) {
      $outputs = $config?->get('outputs') ?? $this->getDefaultOutputs();
      $form_state->set('outputs', $outputs);
    }

    foreach ($outputs as $index => $output) {
      $form['outputs'][$index] = $this->buildOutputForm($output, $index);
    }

    $form['outputs']['add_output'] = [
      '#type' => 'submit',
      '#value' => $this->t('Add output'),
      '#submit' => ['::addOutputSubmit'],
      '#ajax' => [
        'callback' => '::outputsAjaxCallback',
        'wrapper' => 'outputs-wrapper',
      ],
      '#limit_validation_errors' => [],
    ];

    // Thumbnail settings.
    $form['thumbnail'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Thumbnail Generation'),
    ];

    $form['thumbnail']['generate_thumbnail'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Generate thumbnail'),
      '#default_value' => $config?->get('generate_thumbnail') ?? TRUE,
    ];

    $form['thumbnail']['thumbnail_timestamp'] = [
      '#type' => 'number',
      '#title' => $this->t('Thumbnail timestamp'),
      '#description' => $this->t('Seconds into video to capture thumbnail.'),
      '#default_value' => $config?->get('thumbnail_timestamp') ?? 1.0,
      '#min' => 0,
      '#step' => 0.1,
      '#states' => [
        'visible' => [
          ':input[name="generate_thumbnail"]' => ['checked' => TRUE],
        ],
      ],
    ];

    // Processor selection per profile.
    $form['processors'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Processors'),
      '#description' => $this->t('Select which processors run for this profile. Leave empty to use the global processor settings.'),
    ];

    $processorOptions = [];
    foreach ($this->processorManager->getDefinitions() as $id => $definition) {
      $label = $definition['label'] ?? $id;
      $processorOptions[$id] = $label instanceof TranslatableMarkup ? $label : $this->t('@label', ['@label' => $label]);
    }

    $form['processors']['enabled_processors'] = [
      '#type' => 'checkboxes',
      '#title' => $this->t('Enabled processors'),
      '#options' => $processorOptions,
      '#default_value' => $config?->get('enabled_processors') ?? [],
    ];

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

  /**
   * AJAX callback for the outputs fieldset.
   *
   * @param array<string, mixed> $form
   *   The form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   AJAX response with replace and focus commands.
   */
  public function outputsAjaxCallback(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand('#outputs-wrapper', $form['outputs']));
    // Restore focus to the outputs wrapper for keyboard/screen reader users.
    $response->addCommand(new InvokeCommand('#outputs-wrapper', 'focus'));
    return $response;
  }

  /**
   * AJAX callback for realtime output validation.
   *
   * @param array<string, mixed> $form
   *   The form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array<string, mixed>
   *   The validation messages container render array.
   */
  public function validateOutputAjaxCallback(array &$form, FormStateInterface $form_state): array {
    $trigger = $form_state->getTriggeringElement();
    $parents = $trigger['#array_parents'] ?? [];

    // Find the output index from array parents (e.g. ['outputs', 0, 'codec']).
    $index = NULL;
    foreach ($parents as $i => $parent) {
      if ($parent === 'outputs' && isset($parents[$i + 1]) && is_numeric($parents[$i + 1])) {
        $index = (int) $parents[$i + 1];
        break;
      }
    }

    $container = [
      '#type' => 'container',
      '#attributes' => [
        'id' => 'output-validation-' . ($index ?? 0),
        'role' => 'alert',
      ],
    ];

    if ($index === NULL) {
      return $container;
    }

    $outputs = $form_state->getValue('outputs') ?? [];
    $output = $outputs[$index] ?? NULL;
    if (!is_array($output)) {
      return $container;
    }

    $messages = $this->getOutputValidationMessages($output, $index);

    foreach ($messages as $message) {
      $container['messages'][] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['messages', 'messages--warning']],
        'text' => ['#plain_text' => $message],
      ];
    }

    return $container;
  }

  /**
   * Gets validation messages for a single output configuration.
   *
   * @param array<string, mixed> $output
   *   Output form values.
   * @param int $index
   *   Output index.
   *
   * @return array<int, string>
   *   Validation warning messages.
   */
  #[\NoDiscard]
  protected function getOutputValidationMessages(array $output, int $index): array {
    return $this->outputValidator->validateOutputs([$output]);
  }

  /**
   * Submit handler for adding an output.
   *
   * @param array<string, mixed> $form
   *   The form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function addOutputSubmit(array &$form, FormStateInterface $form_state): void {
    $outputs = $form_state->get('outputs') ?? [];
    if (count($outputs) >= ProfileFormValidator::MAX_OUTPUTS) {
      $this->messenger()->addWarning($this->t('Maximum of @max outputs reached.', ['@max' => ProfileFormValidator::MAX_OUTPUTS]));
      $form_state->setRebuild();
      return;
    }

    $globalBitrate = $this->configFactory->get('ffmpeg_media.settings')
      ->get('audio.format.bitrate') ?? '128k';
    $outputs[] = [
      'container' => '',
      'codec' => '',
      'enabled' => TRUE,
      'options' => [
        'crf' => 23,
        'preset' => 6,
        'audio_bitrate' => $globalBitrate,
        'audio_codec' => 'libopus',
      ],
    ];
    $form_state->set('outputs', $outputs);
    $form_state->setRebuild();
  }

  /**
   * Submit handler for removing an output.
   *
   * @param array<string, mixed> $form
   *   The form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function removeOutputSubmit(array &$form, FormStateInterface $form_state): void {
    $trigger = $form_state->getTriggeringElement();
    $index = (int) str_replace('remove_output_', '', $trigger['#name'] ?? '');

    $outputs = $form_state->get('outputs') ?? [];
    unset($outputs[$index]);
    $outputs = array_values($outputs);

    $form_state->set('outputs', $outputs);
    $form_state->setRebuild();
  }

  /**
   * Builds form elements for a single output configuration.
   *
   * @param array<string, mixed> $output
   *   Output configuration.
   * @param int $index
   *   Array index.
   *
   * @return array<string, mixed>
   *   Form elements.
   */
  protected function buildOutputForm(array $output, int $index): array {
    $element = [
      '#type' => 'details',
      '#title' => $this->t('Output @index: @container', [
        '@index' => $index + 1,
        '@container' => strtoupper($output['container'] ?? 'NEW'),
      ]),
      '#open' => $output['enabled'] ?? TRUE,
    ];

    $ajaxValidation = [
      'callback' => '::validateOutputAjaxCallback',
      'wrapper' => 'output-validation-' . $index,
    ];

    $element['enabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enabled'),
      '#default_value' => $output['enabled'] ?? TRUE,
    ];

    try {
      $containerOptions = $this->outputManager->getOutputOptions();
    }
    catch (\Throwable) {
      $containerOptions = [];
    }
    $element['container'] = [
      '#type' => 'select',
      '#title' => $this->t('Container format'),
      '#options' => $containerOptions,
      '#default_value' => $output['container'] ?? '',
      '#required' => TRUE,
      '#empty_option' => $this->t('- Select -'),
      '#ajax' => $ajaxValidation,
    ];

    $element['codec'] = [
      '#type' => 'select',
      '#title' => $this->t('Codec'),
      '#options' => $this->profileFormValidator->getGroupedCodecOptions(),
      '#default_value' => $output['codec'] ?? '',
      '#required' => TRUE,
      '#empty_option' => $this->t('- Select -'),
      '#description' => $this->t('Codecs in the <em>Patent-Encumbered</em> group may require licensing depending on jurisdiction. The server operator is responsible for codec patent compliance. See <a href=":url">FFmpeg Legal</a>.', [
        ':url' => 'https://ffmpeg.org/legal.html',
      ]),
      '#ajax' => $ajaxValidation,
    ];

    $element['options'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Encoding Options'),
    ];

    $options = $output['options'] ?? [];

    $element['options']['crf'] = [
      '#type' => 'number',
      '#title' => $this->t('<abbr title="Constant Rate Factor">CRF</abbr> (Quality)'),
      '#description' => $this->t('Lower = better quality, larger file. Recommended ranges: H.264/HEVC 18–28, VP9 25–35, AV1 20–35.'),
      '#default_value' => $options['crf'] ?? 31,
      '#min' => 0,
      '#max' => 63,
    ];

    $element['options']['preset'] = [
      '#type' => 'number',
      '#title' => $this->t('Preset'),
      '#description' => $this->t('0 = slowest/best quality, 9 = fastest.'),
      '#default_value' => $options['preset'] ?? 6,
      '#min' => 0,
      '#max' => 9,
    ];

    $element['options']['audio_bitrate'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Audio bitrate'),
      '#default_value' => $options['audio_bitrate'] ?? '128k',
      '#size' => 10,
      '#states' => [
        'invisible' => [
          ":input[name=\"outputs[$index][container]\"]" => ['value' => 'jxl'],
        ],
      ],
    ];

    $element['options']['audio_codec'] = [
      '#type' => 'select',
      '#title' => $this->t('Audio codec'),
      '#options' => $this->profileFormValidator->getGroupedAudioCodecOptions(),
      '#default_value' => $options['audio_codec'] ?? 'libopus',
      '#description' => $this->t('Codecs in the <em>Patent-Encumbered</em> group may require licensing. The server operator is responsible for codec patent compliance.'),
      '#ajax' => $ajaxValidation,
      '#states' => [
        'invisible' => [
          ":input[name=\"outputs[$index][container]\"]" => ['value' => 'jxl'],
        ],
      ],
    ];

    $element['options']['max_width'] = [
      '#type' => 'number',
      '#title' => $this->t('Max width'),
      '#description' => $this->t('Leave empty for no limit.'),
      '#default_value' => $options['max_width'] ?? NULL,
      '#min' => 0,
    ];

    $element['options']['max_height'] = [
      '#type' => 'number',
      '#title' => $this->t('Max height'),
      '#description' => $this->t('Leave empty for no limit.'),
      '#default_value' => $options['max_height'] ?? NULL,
      '#min' => 0,
    ];

    $element['options']['two_pass'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Two-pass encoding'),
      '#description' => $this->t('Better quality but slower.'),
      '#default_value' => $options['two_pass'] ?? FALSE,
      '#ajax' => $ajaxValidation,
    ];

    $element['validation_messages'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'output-validation-' . $index],
    ];

    $element['remove'] = [
      '#type' => 'submit',
      '#value' => $this->t('Remove output'),
      '#name' => 'remove_output_' . $index,
      '#submit' => ['::removeOutputSubmit'],
      '#ajax' => [
        'callback' => '::outputsAjaxCallback',
        'wrapper' => 'outputs-wrapper',
      ],
      '#limit_validation_errors' => [],
    ];

    return $element;
  }

  /**
   * Checks if a profile ID exists.
   *
   * @param string $id
   *   Profile ID.
   *
   * @return bool
   *   TRUE if exists.
   */
  public function profileExists(string $id): bool {
    $config = $this->configFactory->get("ffmpeg_media.profile.{$id}");
    return $config->get('id') !== NULL;
  }

  /**
   * Gets default output configurations.
   *
   * @return array<int, array<string, mixed>>
   *   Default outputs.
   */
  protected function getDefaultOutputs(): array {
    $globalBitrate = $this->configFactory->get('ffmpeg_media.settings')
      ->get('audio.format.bitrate') ?? '128k';

    return [
      [
        'container' => 'webm',
        'codec' => 'av1',
        'enabled' => TRUE,
        'options' => [
          'crf' => 30,
          'preset' => 6,
          'audio_bitrate' => $globalBitrate,
          'audio_codec' => 'libopus',
        ],
      ],
      [
        'container' => 'mp4',
        'codec' => 'av1',
        'enabled' => TRUE,
        'options' => [
          'crf' => 30,
          'preset' => 6,
          'audio_bitrate' => $globalBitrate,
          'audio_codec' => 'libopus',
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @param array<string, mixed> $form
   *   Form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   */
  #[\Override]
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    parent::validateForm($form, $form_state);

    $hwAccel = $form_state->getValue('hardware_acceleration') ?? '';
    $this->profileFormValidator->validateOutputs($form_state, $hwAccel);
  }

  /**
   * {@inheritdoc}
   *
   * @param array<string, mixed> $form
   *   Form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   */
  #[\Override]
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    $profileId = $form_state->getValue('profile_id');
    $configName = "ffmpeg_media.profile.{$profileId}";

    $outputs = [];
    foreach ($form_state->getValue('outputs') as $key => $output) {
      if (!is_numeric($key)) {
        continue;
      }
      $outputs[] = [
        'container' => $output['container'],
        'codec' => $output['codec'],
        'enabled' => (bool) $output['enabled'],
        'options' => [
          'crf' => (int) $output['options']['crf'],
          'preset' => (int) $output['options']['preset'],
          'audio_bitrate' => $output['options']['audio_bitrate'],
          'audio_codec' => $output['options']['audio_codec'],
          'max_width' => $output['options']['max_width'] ? (int) $output['options']['max_width'] : NULL,
          'max_height' => $output['options']['max_height'] ? (int) $output['options']['max_height'] : NULL,
          'two_pass' => (bool) $output['options']['two_pass'],
        ],
      ];
    }

    $enabledProcessors = $form_state->getValue('enabled_processors')
      |> array_filter(...)
      |> array_values(...);

    $hwAccel = $form_state->getValue('hardware_acceleration');

    $this->configFactory->getEditable($configName)
      ->set('id', $profileId)
      ->set('label', $form_state->getValue('label'))
      ->set('description', $form_state->getValue('description'))
      ->set('outputs', $outputs)
      ->set('generate_thumbnail', (bool) $form_state->getValue('generate_thumbnail'))
      ->set('thumbnail_timestamp', (float) $form_state->getValue('thumbnail_timestamp'))
      ->set('enabled_processors', $enabledProcessors)
      ->set('hardware_acceleration', $hwAccel !== '' ? $hwAccel : NULL)
      ->save();

    // Invalidate the ProfileManager's in-memory cache so subsequent
    // requests reflect the saved profile configuration.
    $this->profileManager->resetCache();

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

    $this->messenger()->addStatus($this->t('Profile %label saved.', [
      '%label' => $form_state->getValue('label'),
    ]));

    $form_state->setRedirectUrl(Url::fromRoute('ffmpeg_media.profile_list'));
  }

}
