<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\Employee;
use App\Entity\WorkHour;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<WorkHour>
 */
final class WorkHourRepository extends ServiceEntityRepository implements WorkHourReadRepositoryInterface
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, WorkHour::class);
    }

    /**
     * @param list<Employee> $employees
     *
     * @return array<int, WorkHour>
     */
    public function findByDateAndEmployeesIndexedByEmployeeId(DateTimeImmutable $workDate, array $employees): array
    {
        if ([] === $employees) {
            return [];
        }

        $qb = $this->createQueryBuilder('w')
            ->leftJoin('w.employee', 'e')
            ->addSelect('e')
            ->andWhere('w.workDate = :workDate')
            ->andWhere('w.employee IN (:employees)')
            ->setParameter('workDate', $workDate)
            ->setParameter('employees', $employees)
        ;

        /** @var list<WorkHour> $workHours */
        $workHours = $qb->getQuery()->getResult();

        $byEmployeeId = [];
        foreach ($workHours as $workHour) {
            $employeeId = $workHour->getEmployee()?->getId();
            if ($employeeId) {
                $byEmployeeId[$employeeId] = $workHour;
            }
        }

        return $byEmployeeId;
    }

    /**
     * @param list<Employee> $employees
     *
     * @return list<WorkHour>
     */
    public function findByDateRangeAndEmployees(DateTimeImmutable $from, DateTimeImmutable $to, array $employees): array
    {
        if ([] === $employees) {
            return [];
        }

        $qb = $this->createQueryBuilder('w')
            ->leftJoin('w.employee', 'e')
            ->addSelect('e')
            ->andWhere('w.workDate >= :from')
            ->andWhere('w.workDate <= :to')
            ->andWhere('w.employee IN (:employees)')
            ->setParameter('from', $from)
            ->setParameter('to', $to)
            ->setParameter('employees', $employees)
        ;

        // @var list<WorkHour> $workHours
        return $qb->getQuery()->getResult();
    }

    public function hasValidatedInRange(Employee $employee, DateTimeInterface $from, DateTimeInterface $to): bool
    {
        $fromDate = DateTimeImmutable::createFromInterface($from);
        $toDate   = DateTimeImmutable::createFromInterface($to);

        $qb = $this->createQueryBuilder('w')
            ->select('COUNT(w.id)')
            ->andWhere('w.employee = :employee')
            ->andWhere('w.workDate >= :from')
            ->andWhere('w.workDate <= :to')
            ->andWhere('w.isValid = :isValid')
            ->setParameter('employee', $employee)
            ->setParameter('from', $fromDate)
            ->setParameter('to', $toDate)
            ->setParameter('isValid', true)
        ;

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

    public function hasSiteValidatedInRange(Employee $employee, DateTimeInterface $from, DateTimeInterface $to): bool
    {
        $fromDate = DateTimeImmutable::createFromInterface($from);
        $toDate   = DateTimeImmutable::createFromInterface($to);

        $qb = $this->createQueryBuilder('w')
            ->select('COUNT(w.id)')
            ->andWhere('w.employee = :employee')
            ->andWhere('w.workDate >= :from')
            ->andWhere('w.workDate <= :to')
            ->andWhere('w.isSiteValid = :isSiteValid')
            ->setParameter('employee', $employee)
            ->setParameter('from', $fromDate)
            ->setParameter('to', $toDate)
            ->setParameter('isSiteValid', true)
        ;

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

    public function findOneByEmployeeAndDate(Employee $employee, DateTimeInterface $date): ?WorkHour
    {
        $workDate = DateTimeImmutable::createFromInterface($date);

        $qb = $this->createQueryBuilder('w')
            ->andWhere('w.employee = :employee')
            ->andWhere('w.workDate = :workDate')
            ->setParameter('employee', $employee)
            ->setParameter('workDate', $workDate)
            ->setMaxResults(1)
        ;

        // @var null|WorkHour $workHour
        return $qb->getQuery()->getOneOrNullResult();
    }

    /**
     * Count weekend worked days by month.
     * >= 5h total = 1.0 day, < 5h = 0.5 day.
     *
     * @return array<string, float> YYYY-MM => weekend worked day count
     */
    public function countWeekendWorkedDaysByMonth(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $sql = <<<'SQL'
            SELECT TO_CHAR(work_date, 'YYYY-MM') AS month,
                   SUM(
                       CASE
                           WHEN total_minutes >= 300 THEN 1.0
                           WHEN total_minutes > 0 THEN 0.5
                           ELSE 0
                       END
                   ) AS cnt
            FROM (
                SELECT work_date,
                       COALESCE(
                           EXTRACT(EPOCH FROM (morning_to::time - morning_from::time)) / 60, 0
                       )
                       + COALESCE(
                           EXTRACT(EPOCH FROM (afternoon_to::time - afternoon_from::time)) / 60, 0
                       )
                       + COALESCE(
                           EXTRACT(EPOCH FROM (evening_to::time - evening_from::time)) / 60, 0
                       ) AS total_minutes
                FROM work_hours
                WHERE employee_id = :employee
                  AND work_date >= :from
                  AND work_date <= :to
                  AND EXTRACT(ISODOW FROM work_date) IN (6, 7)
                  AND (morning_from IS NOT NULL OR afternoon_from IS NOT NULL OR evening_from IS NOT NULL)
            ) sub
            GROUP BY month
            SQL;

        $conn = $this->getEntityManager()->getConnection();
        $rows = $conn->fetchAllAssociative($sql, [
            'employee' => $employee->getId(),
            'from'     => $from->format('Y-m-d'),
            'to'       => $to->format('Y-m-d'),
        ]);

        $result = [];
        foreach ($rows as $row) {
            $result[(string) $row['month']] = (float) $row['cnt'];
        }

        return $result;
    }

    /**
     * Count weekend and public holiday worked days for forfait bonus leave (PRESENCE mode only).
     * Morning + afternoon = 1.0 day, one only = 0.5 day.
     *
     * @param list<string> $publicHolidayDates Y-m-d formatted weekday public holiday dates
     */
    public function countWeekendAndHolidayWorkedDays(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to, array $publicHolidayDates = []): float
    {
        $targetDates = [];

        // Collect weekend dates in range
        for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) {
            if ((int) $cursor->format('N') >= 6) {
                $targetDates[] = $cursor;
            }
        }

        // Add weekday public holidays
        foreach ($publicHolidayDates as $date) {
            $targetDates[] = new DateTimeImmutable($date);
        }

        if ([] === $targetDates) {
            return 0.0;
        }

        $dateStrings = array_map(static fn (DateTimeImmutable $d): string => $d->format('Y-m-d'), $targetDates);

        /** @var list<WorkHour> $rows */
        $rows = $this->createQueryBuilder('w')
            ->andWhere('w.employee = :employee')
            ->andWhere('w.workDate IN (:dates)')
            ->andWhere('w.isPresentMorning = true OR w.isPresentAfternoon = true')
            ->setParameter('employee', $employee)
            ->setParameter('dates', $dateStrings)
            ->getQuery()
            ->getResult()
        ;

        $total = 0.0;
        foreach ($rows as $row) {
            if ($row->isPresentMorning() && $row->isPresentAfternoon()) {
                $total += 1.0;
            } else {
                $total += 0.5;
            }
        }

        return $total;
    }

    /**
     * Return the set of Y-m-d dates where the employee has worked hours on the given dates.
     *
     * @param list<string> $dates Y-m-d formatted dates
     *
     * @return array<string, true> Y-m-d => true
     */
    public function findWorkedDatesAmong(Employee $employee, array $dates): array
    {
        if ([] === $dates) {
            return [];
        }

        $placeholders = [];
        $params       = ['employee' => $employee->getId()];
        foreach (array_values($dates) as $i => $date) {
            $key            = "d{$i}";
            $placeholders[] = ":{$key}";
            $params[$key]   = $date;
        }

        $sql = sprintf(
            'SELECT work_date FROM work_hours WHERE employee_id = :employee AND work_date IN (%s) AND (morning_from IS NOT NULL OR afternoon_from IS NOT NULL OR evening_from IS NOT NULL)',
            implode(', ', $placeholders)
        );

        $conn = $this->getEntityManager()->getConnection();
        $rows = $conn->fetchAllAssociative($sql, $params);

        $result = [];
        foreach ($rows as $row) {
            $result[(string) $row['work_date']] = true;
        }

        return $result;
    }

    public function isWeekFullyValidated(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool
    {
        // Count weekdays (Mon-Fri) in range
        $expectedWeekdays = 0;
        for ($d = $from; $d <= $to; $d = $d->modify('+1 day')) {
            if ((int) $d->format('N') <= 5) {
                ++$expectedWeekdays;
            }
        }

        if (0 === $expectedWeekdays) {
            return false;
        }

        // Every weekday must have a work_hour row
        $totalCount = (int) $this->createQueryBuilder('w')
            ->select('COUNT(w.id)')
            ->andWhere('w.employee = :employee')
            ->andWhere('w.workDate >= :from')
            ->andWhere('w.workDate <= :to')
            ->setParameter('employee', $employee)
            ->setParameter('from', $from)
            ->setParameter('to', $to)
            ->getQuery()
            ->getSingleScalarResult()
        ;

        if ($totalCount < $expectedWeekdays) {
            return false;
        }

        // All rows must be validated
        $nonValidatedCount = (int) $this->createQueryBuilder('w')
            ->select('COUNT(w.id)')
            ->andWhere('w.employee = :employee')
            ->andWhere('w.workDate >= :from')
            ->andWhere('w.workDate <= :to')
            ->andWhere('w.isValid = :isValid')
            ->setParameter('employee', $employee)
            ->setParameter('from', $from)
            ->setParameter('to', $to)
            ->setParameter('isValid', false)
            ->getQuery()
            ->getSingleScalarResult()
        ;

        return 0 === $nonValidatedCount;
    }

    public function hasPendingSiteValidationForSiteAndDate(int $siteId, DateTimeInterface $date): bool
    {
        $workDate = DateTimeImmutable::createFromInterface($date);

        $qb = $this->createQueryBuilder('w')
            ->select('COUNT(w.id)')
            ->leftJoin('w.employee', 'e')
            ->leftJoin('e.site', 's')
            ->andWhere('s.id = :siteId')
            ->andWhere('w.workDate = :workDate')
            ->andWhere('w.isSiteValid = :isSiteValid')
            ->setParameter('siteId', $siteId)
            ->setParameter('workDate', $workDate)
            ->setParameter('isSiteValid', false)
        ;

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