<?php
/***********************************************************************************
 * The contents of this file are subject to the Extension License Agreement
 * ("Agreement") which can be viewed at
 * https://www.espocrm.com/extension-license-agreement/.
 * By copying, installing downloading, or using this file, You have unconditionally
 * agreed to the terms and conditions of the Agreement, and You may not use this
 * file except in compliance with the Agreement. Under the terms of the Agreement,
 * You shall not license, sublicense, sell, resell, rent, lease, lend, distribute,
 * redistribute, market, publish, commercialize, or otherwise transfer rights or
 * usage to the software or any modified version or derivative work of the software
 * created by or for you.
 *
 * Copyright (C) 2024-2025 Letrium Ltd.
 *
 * License ID: f27e70ce6801a13265271f5669c8bc5c
 ************************************************************************************/

namespace Espo\Modules\Project\Tools\Board;

use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\Core\Record\Collection;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Core\Utils\Metadata;
use Espo\Modules\Project\Entities\Project;
use Espo\Modules\Project\Entities\ProjectColumn;
use Espo\Modules\Project\Entities\ProjectGroup;
use Espo\Modules\Project\Entities\ProjectTask;
use Espo\Modules\Project\Tools\ProjectTask\FromTasksLoader;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\Query\Select;
use Espo\ORM\Query\SelectBuilder;
use Espo\Tools\Kanban\GroupItem;
use Espo\Tools\Kanban\Result;
use RuntimeException;

class Service
{
    public function __construct(
        private EntityManager $entityManager,
        private SelectBuilderFactory $selectBuilderFactory,
        private Metadata $metadata,
        private FromTasksLoader $fromTasksLoader,
    ) {}

    /**
     * @throws BadRequest
     * @throws Forbidden
     */
    public function get(ProjectGroup $group, SearchParams $params): Result
    {
        $query = $this->selectBuilderFactory
            ->create()
            ->from(ProjectTask::ENTITY_TYPE)
            ->withSearchParams($params)
            ->withStrictAccessControl()
            ->buildQueryBuilder()
            ->order([])
            ->order('boardOrder')
            ->order('order')
            ->where(['groupId' => $group->getId()])
            ->build();

        $boardGroups = [];

        foreach ($this->getColumns($group) as $column) {
            $boardGroups[] = $this->getBoardGroup($column, $query, $params);
        }

        $total = $this->entityManager
            ->getRDBRepositoryByClass(ProjectTask::class)
            ->clone($query)
            ->count();

        return new Result($boardGroups, $total);
    }

    private function getBoardGroup(ProjectColumn $column, Select $baseQuery, SearchParams $params): GroupItem
    {
        $query = SelectBuilder::create()->clone($baseQuery)
            ->where(['columnId' => $column->getId()])
            ->build();

        $builder = $this->entityManager->getRDBRepositoryByClass(ProjectTask::class)->clone($query);

        $collection = $builder->find();
        $total = $builder->count();

        if (in_array('fromTasksIds', $params->getSelect() ?? [])) {
            $this->fromTasksLoader->load($collection, $query);
        }

        $style = $this->metadata->get("entityDefs.ProjectTask.fields.status.style.{$column->getMappedStatus()}");

        return new GroupItem(
            name: $column->getId(),
            collection: new Collection($collection, $total),
            label: $column->getName(),
            style: $style,
        );
    }

    /**
     * @return iterable<ProjectColumn>
     */
    private function getColumns(ProjectGroup $group): iterable
    {
        if (!$group->getProjectId()) {
            throw new RuntimeException("No project ID.");
        }

        $project = $this->entityManager->getRDBRepositoryByClass(Project::class)->getById($group->getProjectId());

        if (!$project) {
            throw new RuntimeException("No project.");
        }

        return $this->entityManager
            ->getRDBRepositoryByClass(ProjectColumn::class)
            ->where(['boardId' => $project->getBoardId()])
            ->order('order')
            ->find();
    }

    /**
     * @param string[] $ids
     */
    public function order(ProjectGroup $group, string $columnId, array $ids): void
    {
        $step = 10;

        $first = $this->entityManager
            ->getRDBRepositoryByClass(ProjectTask::class)
            ->select('boardOrder')
            ->where([
                'groupId' => $group->getId(),
                'parentTaskId' => null,
                'columnId' => $columnId,
                'id!=' => $ids,
            ])
            ->order('boardOrder', Order::ASC)
            ->findOne();

        $order = 0;

        if ($first) {
            $order = $first->getBoardOrder() - count($ids) * $step;
        }

        $taskCollection = $this->entityManager
            ->getRDBRepositoryByClass(ProjectTask::class)
            ->where([
                'groupId' => $group->getId(),
                'parentTaskId' => null,
                'columnId' => $columnId,
                'id' => $ids,
            ])
            ->order('boardOrder', Order::ASC)
            ->find();

        /** @var ProjectTask[] $tasks */
        $tasks = iterator_to_array($taskCollection);

        usort($tasks, function (ProjectTask $a, ProjectTask $b) use ($ids) {
            $aIndex = array_search($a->getId(), $ids);
            $bIndex = array_search($b->getId(), $ids);

            if (!is_int($aIndex) || !is_int($bIndex)) {
                return 0;
            }

            return $aIndex - $bIndex;
        });

        foreach ($tasks as $task) {
            $task->setBoardOrder($order);
            $this->entityManager->saveEntity($task, [SaveOption::SILENT => true]);

            $order += $step;
        }
    }
}
