<?php

declare(strict_types=1);

namespace Drupal\display_builder\Controller;

use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormAjaxException;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\BareHtmlPageRenderer;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\HtmlResponseAttachmentsProcessor;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\display_builder\DisplayBuilderInterface;
use Drupal\display_builder\Event\DisplayBuilderEvent;
use Drupal\display_builder\Event\DisplayBuilderEvents;
use Drupal\display_builder\IslandPluginManagerInterface;
use Drupal\display_builder\Plugin\display_builder\Island\InstanceFormPanel;
use Drupal\display_builder\RenderableBuilderTrait;
use Drupal\display_builder\StateManager\StateManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * Returns responses for Display builder routes.
 */
class ApiController extends ControllerBase implements ApiControllerInterface, ContainerInjectionInterface {

  use RenderableBuilderTrait;

  /**
   * The bare html page renderer.
   */
  private BareHtmlPageRenderer $bareHtmlPageRenderer;

  /**
   * The lazy loaded display builder.
   */
  private ?DisplayBuilderInterface $displayBuilder = NULL;

  public function __construct(
    private IslandPluginManagerInterface $islandPluginManager,
    private StateManagerInterface $stateManager,
    private EventDispatcherInterface $eventDispatcher,
    #[Autowire(service: 'html_response.attachments_processor')]
    private HtmlResponseAttachmentsProcessor $htmlResponseAttachmentsProcessor,
    private RendererInterface $renderer,
    private MemoryCacheInterface $memoryCache,
  ) {
    $this->bareHtmlPageRenderer = new BareHtmlPageRenderer($this->renderer, $this->htmlResponseAttachmentsProcessor);
  }

  /**
   * {@inheritdoc}
   */
  public function attachToRoot(Request $request, string $builder_id): HtmlResponse {
    $position = (int) $request->request->get('position', 0);
    $is_move = FALSE;

    if ($request->request->has('instance_id')) {
      $instance_id = (string) $request->request->get('instance_id');
      if (!$this->stateManager->moveToRoot($builder_id, $instance_id, $position)) {
        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
        return $this->responseMessageError($builder_id, $message, $request->request->all());
      }

      $is_move = TRUE;
    }
    elseif ($request->request->has('source_id')) {
      $source_id = (string) $request->request->get('source_id');
      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
      $instance_id = $this->stateManager->attachSourceToRoot($builder_id, $position, $source_id, $data);
    }
    elseif ($request->request->has('preset_id')) {
      $preset_id = (string) $request->request->get('preset_id');
      $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');

      /** @var \Drupal\display_builder\PatternPresetInterface $preset */
      $preset = $presetStorage->load($preset_id);
      $data = $preset->getSources();

      if (!isset($data['source_id']) || !isset($data['source'])) {
        $message = $this->t('[attachToRoot] Missing preset source_id data');

        return $this->responseMessageError($builder_id, $message, $data);
      }
      $instance_id = $this->stateManager->attachSourceToRoot($builder_id, $position, $data['source_id'], $data['source']);
    }
    else {
      $message = '[attachToRoot] Missing content (source_id, instance_id or preset_id)';

      return $this->responseMessageError($builder_id, $message, $request->request->all());
    }

    return $this->dispatchDisplayBuilderEvent(
      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
      $builder_id,
      NULL,
      $instance_id,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function attachToSlot(Request $request, string $builder_id, string $instance_id, string $slot): HtmlResponse {
    $parent_id = $instance_id;
    $position = (int) $request->request->get('position', 0);
    $is_move = FALSE;

    // First, we update the data state.
    if ($request->request->has('instance_id')) {
      $instance_id = (string) $request->request->get('instance_id');
      if (!$this->stateManager->moveToSlot($builder_id, $instance_id, $parent_id, $slot, $position)) {
        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
        return $this->responseMessageError($builder_id, $message, $request->request->all());
      }

      $is_move = TRUE;
    }
    elseif ($request->request->has('source_id')) {
      $source_id = (string) $request->request->get('source_id');
      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
      $instance_id = $this->stateManager->attachSourceToSlot($builder_id, $parent_id, $slot, $position, $source_id, $data);
    }
    elseif ($request->request->has('preset_id')) {
      $preset_id = (string) $request->request->get('preset_id');
      $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');

      /** @var \Drupal\display_builder\PatternPresetInterface $preset */
      $preset = $presetStorage->load($preset_id);
      $data = $preset->getSources();

      if (!isset($data['source_id']) || !isset($data['source'])) {
        $message = $this->t('[attachToSlot] Missing preset source_id data');

        return $this->responseMessageError($builder_id, $message, $data);
      }
      $instance_id = $this->stateManager->attachSourceToSlot($builder_id, $parent_id, $slot, $position, $data['source_id'], $data['source']);
    }
    else {
      $message = $this->t('[attachToSlot] Missing content (component_id, block_id or instance_id)');
      $debug = [
        'instance_id' => $instance_id,
        'slot' => $slot,
        'request' => $request->request->all(),
      ];

      return $this->responseMessageError($builder_id, $message, $debug);
    }

    return $this->dispatchDisplayBuilderEvent(
      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
      $builder_id,
      NULL,
      $instance_id,
      $parent_id,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getInstance(Request $request, string $builder_id, string $instance_id): array {
    $data = $this->stateManager->get($builder_id, $instance_id);

    return $this->dispatchDisplayBuilderEventWithRenderApi(
      DisplayBuilderEvents::ON_ACTIVE,
      $builder_id,
      $data
    );
  }

  /**
   * {@inheritdoc}
   *
   * @todo factorize with thirdPartySettingsUpdate.
   */
  public function updateInstance(Request $request, string $builder_id, string $instance_id): array {
    $body = $request->getPayload()->all();

    if (!isset($body['form_id'])) {
      // @todo log an error.
      return [];
    }

    // Load the instance to properly alter the form data into config data.
    $instance = $this->stateManager->get($builder_id, $instance_id);

    if (isset($body['source']['form_build_id'])) {
      unset($body['source']['form_build_id'], $body['source']['form_token'], $body['source']['form_id']);
    }

    if (isset($body['form_build_id'])) {
      unset($body['form_build_id'], $body['form_token'], $body['form_id']);
    }
    $form_state = new FormState();
    // Default values are the existing values from the state.
    $form_state->addBuildInfo('args', [
      [
        'island_id' => 'instance_form',
        'builder_id' => $builder_id,
        'instance' => $instance,
      ],
      $this->stateManager->getContexts($builder_id),
    ]);
    // The body received corresponds to raw form values.
    // We need to set them in the form state to properly
    // take them into account.
    $form_state->setValues($body);

    $formClass = InstanceFormPanel::getFormClass();
    $values = $this->validateIslandForm($formClass, $form_state);
    $data = [
      'source' => $values,
    ];

    if (isset($instance['source']['component']['slots'], $data['source']['component'])
      && ($data['source']['component']['component_id'] === $instance['source']['component']['component_id'])) {
      // We keep the slots.
      $data['source']['component']['slots'] = $instance['source']['component']['slots'];
    }

    $this->stateManager->setSource($builder_id, $instance_id, $instance['source_id'], $data['source']);

    return $this->dispatchDisplayBuilderEventWithRenderApi(
      DisplayBuilderEvents::ON_UPDATE,
      $builder_id,
      NULL,
      $instance_id,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function thirdPartySettingsUpdate(Request $request, string $builder_id, string $instance_id, string $island_id): HtmlResponse {
    $body = $request->getPayload()->all();

    if (!isset($body['form_id'])) {
      $message = $this->t('[thirdPartySettingsUpdate] Missing payload!');

      return $this->responseMessageError($builder_id, $message, $body);
    }

    $islandDefinition = $this->islandPluginManager->getDefinition($island_id);
    // Load the instance to properly alter the form data into config data.
    $instance = $this->stateManager->get($builder_id, $instance_id);
    unset($body['form_build_id'], $body['form_token'], $body['form_id']);

    $form_state = new FormState();
    // Default values are the existing values from the state.
    $form_state->addBuildInfo('args', [
      [
        'island_id' => $island_id,
        'builder_id' => $builder_id,
        'instance' => $instance,
      ],
      [],
    ]);
    // The body received corresponds to raw form values.
    // We need to set them in the form state to properly
    // take them into account.
    $form_state->setValues($body);

    $formClass = ($islandDefinition['class'])::getFormClass();
    $values = $this->validateIslandForm($formClass, $form_state);
    // We update the state with the new data.
    $this->stateManager->setThirdPartySettings($builder_id, $instance_id, $island_id, $values);

    return $this->dispatchDisplayBuilderEvent(
      DisplayBuilderEvents::ON_UPDATE,
      $builder_id,
      NULL,
      $instance_id,
      NULL,
      $island_id
    );
  }

  /**
   * {@inheritdoc}
   */
  public function pasteInstance(Request $request, string $builder_id, string $instance_id, string $parent_id, string $slot_id, string $slot_position): HtmlResponse {
    $dataToCopy = $this->stateManager->get($builder_id, $instance_id);

    // Keep flag for move or attach to root.
    $is_paste_root = FALSE;

    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
      $source_id = $dataToCopy['source_id'];
      $data = $dataToCopy['source'];

      self::recursiveRefreshInstanceId($data);

      // If no parent we are on root.
      // @todo for duplicate and not parent root seems not detected and copy is inside the slot.
      if ($parent_id === '__root__') {
        $is_paste_root = TRUE;
        $this->stateManager->attachSourceToRoot($builder_id, 0, $source_id, $data, $dataToCopy['_third_party_settings'] ?? NULL);
      }
      else {
        $this->stateManager->attachSourceToSlot($builder_id, $parent_id, $slot_id, (int) $slot_position, $source_id, $data, $dataToCopy['_third_party_settings'] ?? NULL);
      }
    }

    return $this->dispatchDisplayBuilderEvent(
      $is_paste_root ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
      $builder_id,
      NULL,
      $parent_id,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function deleteInstance(Request $request, string $builder_id, string $instance_id): HtmlResponse {
    $current = $this->stateManager->getCurrentState($builder_id);
    $parent_id = $this->stateManager->getParentId($builder_id, $current, $instance_id);
    $this->stateManager->remove($builder_id, $instance_id);

    return $this->dispatchDisplayBuilderEvent(
      DisplayBuilderEvents::ON_DELETE,
      $builder_id,
      NULL,
      $instance_id,
      $parent_id
    );
  }

  /**
   * {@inheritdoc}
   */
  public function saveInstanceAsPreset(Request $request, string $builder_id, string $instance_id): HtmlResponse {
    $label = $this->t('New preset');

    foreach ($request->headers as $key => $value) {
      if ($key === 'hx-prompt' && !empty($value[0])) {
        $label = $value[0];

        break;
      }
    }
    // phpcs:ignore
    $theme = \Drupal::configFactory()->get('system.theme')->get('default');
    $data = $this->stateManager->get($builder_id, $instance_id);
    self::cleanInstanceId($data);

    $preset_storage = $this->entityTypeManager()->getStorage('pattern_preset');
    $preset = $preset_storage->create([
      'id' => \uniqid(),
      'label' => (string) $label,
      'theme' => $theme,
      'status' => TRUE,
      'group' => '',
      'description' => '',
      'sources' => Yaml::encode($data),
    ]);
    $preset->save();

    return $this->dispatchDisplayBuilderEvent(
      DisplayBuilderEvents::ON_PRESET_SAVE,
      $builder_id,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function save(Request $request, string $builder_id): HtmlResponse {
    $this->stateManager->setSave($builder_id, $this->stateManager->getCurrentState($builder_id));

    return $this->dispatchDisplayBuilderEvent(
      DisplayBuilderEvents::ON_SAVE,
      $builder_id,
      $this->stateManager->getContexts($builder_id)
    );
  }

  /**
   * {@inheritdoc}
   */
  public function restore(Request $request, string $builder_id): HtmlResponse {
    $this->stateManager->restore($builder_id);

    // @todo on history change is closest to a data change that we need here
    // without any instance id. Perhaps we need a new event?
    return $this->dispatchDisplayBuilderEvent(
      DisplayBuilderEvents::ON_HISTORY_CHANGE,
      $builder_id
    );
  }

  /**
   * {@inheritdoc}
   */
  public function undo(Request $request, string $builder_id): HtmlResponse {
    $this->stateManager->undo($builder_id);

    return $this->dispatchDisplayBuilderEvent(
      DisplayBuilderEvents::ON_HISTORY_CHANGE,
      $builder_id,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function redo(Request $request, string $builder_id): HtmlResponse {
    $this->stateManager->redo($builder_id);

    return $this->dispatchDisplayBuilderEvent(
      DisplayBuilderEvents::ON_HISTORY_CHANGE,
      $builder_id,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function clear(Request $request, string $builder_id): HtmlResponse {
    $this->stateManager->clear($builder_id);

    return $this->dispatchDisplayBuilderEvent(
      DisplayBuilderEvents::ON_HISTORY_CHANGE,
      $builder_id,
    );
  }

  /**
   * Dispatches a display builder event.
   *
   * @param string $event_id
   *   The event ID.
   * @param string $builder_id
   *   The builder ID.
   * @param array|null $data
   *   The data.
   * @param string|null $instance_id
   *   Optional instance ID.
   * @param string|null $parent_id
   *   Optional parent ID.
   * @param string|null $current_island_id
   *   Optional current island ID which trigger action.
   *
   * @return \Drupal\Core\Render\HtmlResponse
   *   The HTML response.
   */
  protected function dispatchDisplayBuilderEvent(
    string $event_id,
    string $builder_id,
    ?array $data = NULL,
    ?string $instance_id = NULL,
    ?string $parent_id = NULL,
    ?string $current_island_id = NULL,
  ): HtmlResponse {
    $result = $this->createEventWithEnabledIsland($event_id, $builder_id, $data, $instance_id, $parent_id, $current_island_id);

    return $this->bareHtmlPageRenderer->renderBarePage($result, '', 'markup');
  }

  /**
   * Dispatches a display builder event with render API.
   *
   * @param string $event_id
   *   The event ID.
   * @param string $builder_id
   *   The builder ID.
   * @param array|null $data
   *   The data.
   * @param string|null $instance_id
   *   Optional instance ID.
   * @param string|null $parent_id
   *   Optional parent ID.
   * @param string|null $current_island_id
   *   Optional current island ID which trigger action.
   *
   * @return array
   *   The render array result of the event.
   */
  protected function dispatchDisplayBuilderEventWithRenderApi(
    string $event_id,
    string $builder_id,
    ?array $data = NULL,
    ?string $instance_id = NULL,
    ?string $parent_id = NULL,
    ?string $current_island_id = NULL,
  ): array {
    return $this->createEventWithEnabledIsland($event_id, $builder_id, $data, $instance_id, $parent_id, $current_island_id);
  }

  /**
   * Validates an island form.
   *
   * @param string $formClass
   *   The form class.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The validated values.
   */
  protected function validateIslandForm(string $formClass, FormStateInterface $form_state): array {
    $formBuilder = $this->formBuilder();

    try {
      /** @var \Drupal\Core\Form\FormBuilder $formBuilder */
      $form = $formBuilder->buildForm($formClass, $form_state);
      $formBuilder->validateForm($formClass, $form, $form_state);
    }
    catch (FormAjaxException $e) {
      throw $e;
    }
    // Those values are the validated values, produced by the form.
    // with all Form API processing.
    $values = $form_state->getValues();

    // We clean the values from form API keys.
    if (isset($values['form_build_id'])) {
      unset($values['form_build_id'], $values['form_token'], $values['form_id']);
    }

    return $values;
  }

  /**
   * Returns the display builder by builder ID.
   */
  protected function getDisplayBuilder(string $builder_id): DisplayBuilderInterface {
    if ($this->displayBuilder !== NULL) {
      return $this->displayBuilder;
    }
    $builder_config_id = $this->stateManager->getEntityConfigId($builder_id);
    $display_builder = $this->entityTypeManager()->getStorage('display_builder')->load($builder_config_id);
    \assert($display_builder instanceof DisplayBuilderInterface);
    $this->displayBuilder = $display_builder;

    return $this->displayBuilder;
  }

  /**
   * Render an error message in the display builder.
   *
   * @param string $builder_id
   *   The builder ID.
   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
   *   The error message.
   * @param array $debug
   *   The debug code.
   *
   * @return \Drupal\Core\Render\HtmlResponse
   *   The response with the component.
   */
  private function responseMessageError(
    string $builder_id,
    string|TranslatableMarkup $message,
    array $debug,
  ): HtmlResponse {
    $build = $this->buildError($builder_id, $message, \print_r($debug, TRUE), NULL, TRUE);

    $html = $this->renderer->renderInIsolation($build);
    $response = new HtmlResponse();
    $response->setContent($html);

    return $response;
  }

  /**
   * Creates a display builder event with enabled islands only.
   *
   * Use a cache to avoid loading all the builder configuration.
   *
   * @param string $event_id
   *   The event ID.
   * @param string $builder_id
   *   The builder ID.
   * @param array|null $data
   *   The data.
   * @param string|null $instance_id
   *   Optional instance ID.
   * @param string|null $parent_id
   *   Optional parent ID.
   * @param string|null $current_island_id
   *   Current island ID which trigger action.
   *
   * @return array
   *   The event result.
   */
  private function createEventWithEnabledIsland($event_id, $builder_id, $data, $instance_id, $parent_id, $current_island_id): array {
    $key = \sprintf('db_%s_island_enable', $builder_id);
    $island_configuration_key = \sprintf('db_%s_island_configuration', $builder_id);
    $island_enabled = $this->memoryCache->get($key);
    $island_configuration = $this->memoryCache->get($island_configuration_key);

    if ($island_configuration === FALSE) {
      $island_configuration = $this->getDisplayBuilder($builder_id)->getIslandConfigurations();
      $this->memoryCache->set($island_configuration_key, $island_configuration);
    }
    else {
      $island_configuration = $island_configuration->data;
    }

    if ($island_enabled === FALSE) {
      $island_enabled = $this->getDisplayBuilder($builder_id)->getIslandEnabled();
      $this->memoryCache->set($key, $island_enabled);
    }
    else {
      $island_enabled = $island_enabled->data;
    }

    $event = new DisplayBuilderEvent($builder_id, $island_enabled, $island_configuration, $data, $instance_id, $parent_id, $current_island_id);
    $this->eventDispatcher->dispatch($event, $event_id);

    return $event->getResult();
  }

  /**
   * Recursively regenerate the _instance_id key.
   *
   * @param array $array
   *   The array reference.
   */
  private static function recursiveRefreshInstanceId(array &$array): void {
    if (isset($array['_instance_id'])) {
      $array['_instance_id'] = \uniqid();
    }

    foreach ($array as &$value) {
      if (\is_array($value)) {
        self::recursiveRefreshInstanceId($value);
      }
    }
  }

  /**
   * Recursively regenerate the _instance_id key.
   *
   * @param array $array
   *   The array reference.
   *
   * @todo set as utils because clone in ExportForm.php?
   */
  private static function cleanInstanceId(array &$array): void {
    unset($array['_instance_id']);

    foreach ($array as $key => &$value) {
      if (\is_array($value)) {
        self::cleanInstanceId($value);

        if (isset($value['source_id'], $value['source']['value']) && empty($value['source']['value'])) {
          unset($array[$key]);
        }
      }
    }
  }

}
