<?php

declare(strict_types=1);

namespace Drupal\conductor\Repository;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\conductor\Exception\UnableToAssignDraftToEntityException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;

final readonly class DraftRepository implements DraftRepositoryInterface {

  public const string TABLE_NAME = 'conductor_draft_map';
  public const string DEFAULT_DRAFT_TITLE = "Untitled Canvas draft:";

  public const int DEFAULT_MAX_DRAFT = 15;

  public function __construct(
    private Connection $connection,
    private TimeInterface $time,
    private EntityTypeManagerInterface $entityTypeManager,
    private ConfigFactoryInterface $configFactory,
  ) {}

  public function getDrafts(?int $year = NULL): array {
    $year = $year ?? (int) date('Y');
    $result = $this->connection->select(self::TABLE_NAME)
      ->fields(self::TABLE_NAME)
      ->condition('created', [
        strtotime("first day of january $year midnight"),
        strtotime("last day of december $year 23:59:59"),
      ], 'BETWEEN')
      ->orderBy('created', 'DESC')
      ->execute();
    if (!$result instanceof StatementInterface) {
      return [];
    }
    return $result->fetchAll(\PDO::FETCH_ASSOC);
  }

  public function getDraft(string $entity_type, int $entity_id): array {
    $result = $this->connection->select(self::TABLE_NAME)
      ->fields(self::TABLE_NAME)
      ->condition('entity_type', $entity_type)
      ->condition('entity_id', $entity_id)
      ->execute();
    if (!$result instanceof StatementInterface) {
      return [];
    }
    $draft = $result->fetchAssoc();
    if (!is_array($draft) || empty($draft)) {
      return [];
    }
    return $draft;
  }

  /**
   * @throws \Drupal\conductor\Exception\UnableToAssignDraftToEntityException
   */
  public function upsertDraft(string $entity_type, int $entity_id, string $draft_id): void {
    $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id);
    if (NULL === $entity) {
      throw new UnableToAssignDraftToEntityException("$entity_type $entity_id not found");
    }

    $time = $this->time->getCurrentTime();
    $this->connection->merge(self::TABLE_NAME)
      ->keys([
        'entity_id' => $entity_id,
        'entity_type' => $entity_type,
      ])
      ->insertFields([
        'entity_id' => $entity_id,
        'entity_type' => $entity_type,
        'draft_id' => $draft_id,
        'created' => $time,
        'changed' => $time,
      ])
      ->updateFields([
        'draft_id' => $draft_id,
        'changed' => $time,
      ])
      ->execute();
  }

  /**
   * Insert a new draft-entity_id association into the database.
   *
   * @throws \Drupal\conductor\Exception\UnableToAssignDraftToEntityException
   */
  public function createDraft(string $entity_type, int $entity_id, string $draft_id): void {
    if (!$this->entityTypeManager->getStorage($entity_type)->load($entity_id)) {
      throw new UnableToAssignDraftToEntityException("$entity_type $entity_id not found");
    }

    // Idempotency: if the exact same mapping already exists, treat as success.
    $existing = $this->getDraft($entity_type, $entity_id);
    if (!empty($existing) && isset($existing['draft_id']) && $existing['draft_id'] === $draft_id) {
      // No-op success.
      return;
    }

    if (
      $this->isEntityLinkedToADraft($entity_type, $entity_id) ||
      $this->isDraftLinkedToAnEntity($draft_id)
    ) {
      throw new UnableToAssignDraftToEntityException("Draft $draft_id already linked to another canvas page.");
    }
    if ($this->getUsedDrafts() >= $this->getMaxDrafts()) {
      throw new UnableToAssignDraftToEntityException("Maximum number of drafts reached for this year.");
    }

    $this->upsertDraft($entity_type, $entity_id, $draft_id);
  }

  /**
   * @throws \Drupal\conductor\Exception\UnableToAssignDraftToEntityException
   */
  public function updateDraft(string $entity_type, int $entity_id, string $draft_id): void {
    $this->upsertDraft($entity_type, $entity_id, $draft_id);
  }

  public function deleteDraft(string $entity_type, int $entity_id): bool {
    return (bool) $this->connection->update(self::TABLE_NAME)
      ->condition('entity_type', $entity_type)
      ->condition('entity_id', $entity_id)
      ->fields(['deleted' => 1])
      ->execute();
  }

  private function isEntityLinkedToADraft(string $entity_type, int $entity_id): bool {
    $result = $this->connection->select(self::TABLE_NAME)
      ->fields(self::TABLE_NAME)
      ->condition('entity_type', $entity_type)
      ->condition('entity_id', $entity_id)
      ->execute();
    if (!$result instanceof StatementInterface) {
      return FALSE;
    }
    return (bool) $result->fetchField();
  }

  public function isDraftLinkedToAnEntity(string $draft_id): bool {
    $result = $this->connection->select(self::TABLE_NAME)
      ->fields(self::TABLE_NAME)
      ->condition('draft_id', $draft_id)
      ->execute();
    if (!$result instanceof StatementInterface) {
      return FALSE;
    }
    return (bool) $result->fetchField();
  }

  /**
   * Retrieves the maximum number of drafts allowed from the configuration settings.
   *
   * @return int
   *   The maximum number of drafts.
   */
  public function getMaxDrafts(): int {
    return $this->configFactory->get('conductor.settings')->get('max_drafts') ?? self::DEFAULT_MAX_DRAFT;
  }

  /**
   * Calculates and retrieves the number of drafts currently in use.
   *
   * @return int
   *   The number of drafts that are currently in use.
   */
  public function getUsedDrafts(?int $year = NULL): int {
    return count($this->getDrafts($year));
  }

}
