<?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\Project;

use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\FieldProcessing\ListLoadProcessor;
use Espo\Core\FieldProcessing\Loader\Params as LoaderParams;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Modules\Project\Entities\Project;
use Espo\Modules\Project\Entities\ProjectGroup;
use Espo\Modules\Project\Entities\ProjectTask;
use Espo\Modules\Project\Tools\ProjectTask\FromTasksLoader;
use Espo\ORM\Collection;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Expression as Expr;
use Espo\ORM\Query\Part\Order;
use Espo\ORM\Query\Part\Selection;
use PDO;
use RuntimeException;

class PlanService
{
    public function __construct(
        private SelectBuilderFactory $selectBuilderFactory,
        private ServiceContainer $serviceContainer,
        private EntityManager $entityManager,
        private ListLoadProcessor $listLoadProcessor,
        private FromTasksLoader $fromTasksLoader,
    ) {}

    /**
     * @param string[] $collapsedGroupIds
     */
    public function get(Project $project, SearchParams $params, array $collapsedGroupIds = []): PlanData
    {
        $list = [];

        $totals = $this->getTotals($project, $params);

        foreach ($this->getGroups($project) as $group) {
            $this->serviceContainer->getByClass(ProjectGroup::class)->prepareEntityForOutput($group);

            $tasks = !in_array($group->getId(), $collapsedGroupIds) ?
                $this->getTasks($group, $params) :
                null;

            $total = $totals[$group->getId()] ?? null;

            $list[] = [$group, $tasks, $total];
        }

        return new PlanData(
            groups: $list,
            total: $this->getTotal($project, $params),
        );
    }

    /**
     * @return iterable<ProjectGroup>
     */
    private function getGroups(Project $project): iterable
    {
        /** @var Collection<ProjectGroup> */
        return $this->entityManager
            ->getRelation($project, 'groups')
            ->order('order')
            ->find();
    }

    /**
     * @return Collection<ProjectTask>
     */
    private function getTasks(ProjectGroup $group, SearchParams $params): Collection
    {
        try {
            $builder = $this->selectBuilderFactory
                ->create()
                ->from(ProjectTask::ENTITY_TYPE)
                ->withSearchParams($params)
                ->withStrictAccessControl();

            $query = $builder->buildQueryBuilder()
                ->order([])
                ->order('order', Order::ASC)
                ->where([
                    'projectId' => $group->getProjectId(),
                    'groupId' => $group->getId(),
                    'parentTaskId' => null,
                ])
                ->build();
        }
        catch (BadRequest|Forbidden $e) {
            throw new RuntimeException($e->getMessage(), 0, $e);
        }

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

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

        $service = $this->serviceContainer->getByClass(ProjectTask::class);

        foreach ($tasks as $task) {
            $service->prepareEntityForOutput($task);

            $this->listLoadProcessor->process($task, LoaderParams::create()->withSelect($params->getSelect()));
        }

        return $tasks;
    }

    private function getTotal(Project $project, SearchParams $params): int
    {
        try {
            $builder = $this->selectBuilderFactory
                ->create()
                ->from(ProjectTask::ENTITY_TYPE)
                ->withSearchParams($params)
                ->withStrictAccessControl();

            $query = $builder->buildQueryBuilder()
                ->order([])
                ->where([
                    'projectId' => $project->getId(),
                    'parentTaskId' => null,
                ])
                ->build();
        }
        catch (BadRequest|Forbidden $e) {
            throw new RuntimeException($e->getMessage(), 0, $e);
        }

        return $this->entityManager->getRDBRepositoryByClass(ProjectTask::class)->clone($query)->count();
    }

    /**
     * @return array<string, int>
     */
    private function getTotals(Project $project, SearchParams $params): array
    {
        try {
            $builder = $this->selectBuilderFactory
                ->create()
                ->from(ProjectTask::ENTITY_TYPE)
                ->withSearchParams($params)
                ->withStrictAccessControl()
                ->buildQueryBuilder();
        } catch (BadRequest|Forbidden $e) {
            throw new RuntimeException($e->getMessage(), 0, $e);
        }

        $query = $builder
            ->limit()
            ->select([
                Selection::fromString('groupId'),
                Selection::create(Expr::count(Expr::column('id')))->withAlias('count'),
            ])
            ->order([])
            ->group('groupId')
            ->having(['groupId!=' => null])
            ->where([
                'projectId' => $project->getId(),
                'parentTaskId' => null,
            ])
            ->build();

        $sth = $this->entityManager->getQueryExecutor()->execute($query);

        $totals = [];

        while ($row = $sth->fetch(PDO::FETCH_ASSOC)) {
            $id = $row['groupId'];

            $totals[$id] = $row['count'];
        }

        return $totals;
    }
}
