<?php

declare(strict_types=1);

namespace Drupal\languagewire_translation_provider\Adapter\Database;

use Drupal\Core\Database\Connection;
use Drupal\languagewire_translation_provider\Database\DocumentRepositoryInterface;
use Drupal\languagewire_translation_provider\Database\Mapper\ProjectMapper;
use Drupal\languagewire_translation_provider\Database\ProjectRepositoryInterface;
use Drupal\languagewire_translation_provider\Database\TablesInterface;
use Drupal\languagewire_translation_provider\Domain\Project;
use Drupal\languagewire_translation_provider\Domain\ProjectStatus;

/**
 * Drupal Project Repository.
* */
final class ProjectRepository implements ProjectRepositoryInterface {

  /**
   * Connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  private Connection $connection;

  /**
   * Mapper.
   *
   * @var \Drupal\languagewire_translation_provider\Database\Mapper\ProjectMapper
   */
  private ProjectMapper $mapper;

  /**
   * Document repository.
   *
   * @var \Drupal\languagewire_translation_provider\Database\DocumentRepositoryInterface
   */
  private DocumentRepositoryInterface $documentRepository;

  /**
   * Constructs a new ProjectRepository object.
   *
   * @param \Drupal\Core\Database\Connection $connection
   *   Connection.
   * @param \Drupal\languagewire_translation_provider\Database\Mapper\ProjectMapper $mapper
   *   Mapper.
   * @param \Drupal\languagewire_translation_provider\Database\DocumentRepositoryInterface $documentRepository
   *   Document repository.
   */
  public function __construct(Connection $connection, ProjectMapper $mapper, DocumentRepositoryInterface $documentRepository) {
    $this->connection = $connection;
    $this->mapper = $mapper;
    $this->documentRepository = $documentRepository;
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Exception
   */
  public function getById(int $projectId): ?Project {
    $dbItem = $this->connection->select(TablesInterface::PROJECTS, 'projects')->fields('projects')->condition('pid', $projectId)->execute()->fetchAssoc();
    return $dbItem ? $this->mapper->toProject($dbItem) : NULL;
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Exception
   */
  public function getAllByStatus(ProjectStatus $projectStatus): array {
    return $this->getAllByCondition('status', $projectStatus->value());
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Exception
   */
  public function getAllByBundlingId(string $bundlingId): array {
    return $this->getAllByCondition('bundle_id', $bundlingId);
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Exception
   */
  public function getAllByTmgmtJobId(int $tmgmtJobId): array {
    return $this->getAllByCondition('tmgmt_job_id', $tmgmtJobId);
  }

  /**
   * Get project statuses by tmgmt job id.
   *
   * @throws \Exception
   */
  public function getProjectStatusesByTmgmtJobId(int $tmgmtJobId): array {
    return array_map(function (Project $project): ProjectStatus {
      return $project->getStatus();
    }, $this->getAllByTmgmtJobId($tmgmtJobId));
  }

  /**
   * {@inheritDoc}
   *
   * @throws \Exception
   */
  public function save(Project $project): Project {
    // Drupal offers the upsert and merge queries
    // which would do insert/update for us but neither of them will.
    // Return the last insert ID. There's no _clean_ way of receiving it,
    // so we might as well do it ourselves.
    // Should we discover that we don't need to return anything from
    // those save methods we can optimize it easily.
    $documents = $project->getDocuments();
    $this->documentRepository->saveMany($documents);
    $projectId = $project->getId() ? $this->update($project) : $this->insert($project);
    return $this->getById($projectId);
  }

  /**
   * Insert project.
   *
   * @throws \Exception
   */
  private function insert(Project $project): int {
    $dbEntity = $this->mapper->toDatabaseEntity($project);
    return (int) $this->connection->insert(TablesInterface::PROJECTS)->fields(array_keys($dbEntity))->values(array_values($dbEntity))->execute();
  }

  /**
   * Update project.
   *
   * @throws \Exception
   */
  private function update(Project $project): int {
    $this->connection->update(TablesInterface::PROJECTS)->condition('pid', $project->getId())->fields($this->mapper->toDatabaseEntity($project))->execute();
    return $project->getId();
  }

  /**
   * Get all by condition.
   *
   * @throws \Exception
   */
  private function getAllByCondition(string $columnName, $value): array {
    $dbItems = $this->connection->select(TablesInterface::PROJECTS, 'projects')->fields('projects')->condition($columnName, $value)->execute()->fetchAll(\PDO::FETCH_ASSOC);
    $projects = array_map(function (array $dbItem): ?Project {
      return $this->mapper->toProject($dbItem);
    }, $dbItems);
    // @todo return [$dbItem, $result] in $projects and then cleanup invalid dbItems
    return array_filter($projects, function (?Project $result): bool {
      return $result != NULL;
    });
  }

}
