<?php

namespace App\Domain\Shared\Infrastructure\Persistence\Repository;

use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
use App\Domain\Shared\Domain\Exception\JobNotFoundException;
use App\Domain\Shared\Domain\Model\Job;
use App\Domain\Shared\Domain\Model\JobStatus;
use App\Domain\Shared\Infrastructure\Persistence\Entity\JobEntity;
use App\Domain\Shared\Infrastructure\Persistence\Mapper\JobMapper;
use Doctrine\ORM\EntityManagerInterface;

readonly class DoctrineJobRepository implements JobRepositoryInterface
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private JobMapper $mapper
    ) {
    }

    public function save(Job $job): void
    {
        /** @var JobEntity|null $existingJobEntity */
        $existingJobEntity = $this->entityManager->find(JobEntity::class, $job->id);

        if ($existingJobEntity) {
            $existingJobEntity->setStatus($job->status->value);
            $existingJobEntity->setStartedAt($job->startedAt);
            $existingJobEntity->setCompletedAt($job->completedAt);
            $existingJobEntity->setFailureReason($job->failureReason);
            $existingJobEntity->setAttempts($job->attempts);
            $existingJobEntity->setContext($job->context);
            $this->entityManager->persist($existingJobEntity);
        } else {
            $entity = $this->mapper->toEntity($job);
            $this->entityManager->persist($entity);
        }
        $this->entityManager->flush();
    }

    public function get(string $id): Job
    {
        $job = $this->findById($id);

        if (null === $job) {
            throw JobNotFoundException::withId($id);
        }

        return $job;
    }

    public function findById(string $id): ?Job
    {
        $entity = $this->entityManager->find(JobEntity::class, $id);

        if (null === $entity) {
            return null;
        }

        return $this->mapper->toDomain($entity);
    }

    public function findByStatus(JobStatus $status): array
    {
        $entities = $this->entityManager->createQueryBuilder()
            ->select('j')
            ->from(JobEntity::class, 'j')
            ->where('j.status = :status')
            ->setParameter('status', $status->value)
            ->getQuery()
            ->getResult();

        return array_map(fn (JobEntity $entity) => $this->mapper->toDomain($entity), $entities);
    }

    public function findPendingJobs(): array
    {
        return $this->findByStatus(JobStatus::PENDING);
    }

    public function findInProgressJobs(): array
    {
        return $this->findByStatus(JobStatus::IN_PROGRESS);
    }

    public function findFailedJobs(): array
    {
        return $this->findByStatus(JobStatus::FAILED);
    }

    public function findByType(string $type): array
    {
        $entities = $this->entityManager->createQueryBuilder()
            ->select('j')
            ->from(JobEntity::class, 'j')
            ->where('j.type = :type')
            ->setParameter('type', $type)
            ->getQuery()
            ->getResult();

        return array_map(fn (JobEntity $entity) => $this->mapper->toDomain($entity), $entities);
    }

    public function findByCriteria(array $criteria): array
    {
        $qb = $this->entityManager->createQueryBuilder()
            ->select('j')
            ->from(JobEntity::class, 'j');

        if (isset($criteria['statuses']) && is_array($criteria['statuses']) && !empty($criteria['statuses'])) {
            $expr = $qb->expr()->orX();
            foreach ($criteria['statuses'] as $key => $status) {
                $paramName = 'status' . $key;
                $expr->add($qb->expr()->eq('j.status', ':' . $paramName));
                $qb->setParameter($paramName, $status->value);
            }
            $qb->andWhere($expr);
        } elseif (isset($criteria['status'])) {
            $qb->andWhere('j.status = :status')
               ->setParameter('status', $criteria['status']->value);
        }

        if (isset($criteria['type'])) {
            $qb->andWhere('j.type = :type')
               ->setParameter('type', $criteria['type']);
        }

        if (isset($criteria['createdAfter'])) {
            $qb->andWhere('j.createdAt >= :createdAfter')
               ->setParameter('createdAfter', $criteria['createdAfter']);
        }

        if (isset($criteria['createdBefore'])) {
            $qb->andWhere('j.createdAt <= :createdBefore')
               ->setParameter('createdBefore', $criteria['createdBefore']);
        }

        if (isset($criteria['sortBy'])) {
            $qb->orderBy('j.' . $criteria['sortBy'], $criteria['sortOrder'] ?? 'ASC');
        }

        if (isset($criteria['offset'])) {
            $qb->setFirstResult($criteria['offset']);
        }

        if (isset($criteria['limit'])) {
            $qb->setMaxResults($criteria['limit']);
        }

        $entities = $qb->getQuery()->getResult();

        return array_map(fn (JobEntity $entity) => $this->mapper->toDomain($entity), $entities);
    }

    public function countByCriteria(array $criteria): int
    {
        $qb = $this->entityManager->createQueryBuilder()
            ->select('COUNT(j.id)')
            ->from(JobEntity::class, 'j');

        if (isset($criteria['statuses']) && is_array($criteria['statuses']) && !empty($criteria['statuses'])) {
            $expr = $qb->expr()->orX();
            foreach ($criteria['statuses'] as $key => $status) {
                $paramName = 'status' . $key;
                $expr->add($qb->expr()->eq('j.status', ':' . $paramName));
                $qb->setParameter($paramName, $status->value);
            }
            $qb->andWhere($expr);
        } elseif (isset($criteria['status'])) {
            $qb->andWhere('j.status = :status')
               ->setParameter('status', $criteria['status']->value);
        }

        if (isset($criteria['type'])) {
            $qb->andWhere('j.type = :type')
               ->setParameter('type', $criteria['type']);
        }

        if (isset($criteria['createdAfter'])) {
            $qb->andWhere('j.createdAt >= :createdAfter')
               ->setParameter('createdAfter', $criteria['createdAfter']);
        }

        if (isset($criteria['createdBefore'])) {
            $qb->andWhere('j.createdAt <= :createdBefore')
               ->setParameter('createdBefore', $criteria['createdBefore']);
        }

        return (int) $qb->getQuery()->getSingleScalarResult();
    }

    public function delete(string $id): void
    {
        $entity = $this->entityManager->find(JobEntity::class, $id);

        if (null === $entity) {
            throw JobNotFoundException::withId($id);
        }

        $this->entityManager->remove($entity);
        $this->entityManager->flush();
    }

    public function deleteByCriteria(array $criteria): int
    {
        $qb = $this->entityManager->createQueryBuilder()
            ->delete(JobEntity::class, 'j');

        if (isset($criteria['statuses']) && is_array($criteria['statuses']) && !empty($criteria['statuses'])) {
            $expr = $qb->expr()->orX();
            foreach ($criteria['statuses'] as $key => $status) {
                $paramName = 'status' . $key;
                $expr->add($qb->expr()->eq('j.status', ':' . $paramName));
                $qb->setParameter($paramName, $status->value);
            }
            $qb->andWhere($expr);
        } elseif (isset($criteria['status'])) {
            $qb->andWhere('j.status = :status')
               ->setParameter('status', $criteria['status']->value);
        }

        if (isset($criteria['type'])) {
            $qb->andWhere('j.type = :type')
               ->setParameter('type', $criteria['type']);
        }

        if (isset($criteria['createdAfter'])) {
            $qb->andWhere('j.createdAt >= :createdAfter')
               ->setParameter('createdAfter', $criteria['createdAfter']);
        }

        if (isset($criteria['createdBefore'])) {
            $qb->andWhere('j.createdAt <= :createdBefore')
               ->setParameter('createdBefore', $criteria['createdBefore']);
        }

        return (int) $qb->getQuery()->execute();
    }
}
