<?php

namespace App\Resource\Service;

use App\Resource\Entity\ClinicResource;
use App\Resource\Entity\ResourceCalendar;
use App\Resource\Entity\ResourceException;
use App\Resource\Repository\ResourceCalendarRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Doctrine\ORM\EntityManagerInterface;

final class ResourceCalendarService
{
    public const DAYS = [0, 1, 2, 3, 4, 5, 6];

    public function __construct(
        private readonly ResourceCalendarRepository $calendars,
        private readonly EntityManagerInterface     $em,
    ) {}

    /** @return array<int, list<array<string, mixed>>> کلیدهای ۰..۶ همیشه هر هفت روز */
    public function read(ClinicResource $resource): array
    {
        $result = array_fill_keys(self::DAYS, []);

        foreach ($this->calendars->findForResource($resource) as $shift) {
            $result[$shift->getDayOfWeek()][] = $shift->toArray();
        }

        return $result;
    }

    public function isDefined(ClinicResource $resource): bool
    {
        return $this->calendars->findForResource($resource) !== [];
    }

    /**
     * جایگزینی کامل هفت روز — همان قرارداد ساعت کاری شعبه، و به همان دلیل:
     * merge تفاضلی روی هفت روز و چند بازه، دو کلاینت هم‌زمان را ناسازگار می‌کند.
     *
     * @param array<int|string, mixed> $days
     * @return array<int, list<array<string, mixed>>>
     */
    public function replace(ClinicResource $resource, array $days): array
    {
        $normalized = $this->validate($days);

        // اعتبارسنجی کامل پیش از هر حذفی — بازهٔ نامعتبر در روز ششم نباید شش روز
        // درستِ قبلی را پاک کند و بعد ۴۲۲ برگرداند.
        $this->calendars->deleteForResource($resource);

        foreach ($normalized as $dayOfWeek => $ranges) {
            foreach ($ranges as $sequence => $range) {
                $this->em->persist(new ResourceCalendar(
                    $resource,
                    $dayOfWeek,
                    $range['start_minute'],
                    $range['end_minute'],
                    $sequence,
                ));
            }
        }

        $this->em->flush();

        return $this->read($resource);
    }

    /**
     * @param array<int|string, mixed> $days
     * @return array<int, list<array{start_minute: int, end_minute: int}>>
     */
    private function validate(array $days): array
    {
        $normalized = array_fill_keys(self::DAYS, []);

        foreach ($days as $rawDay => $ranges) {
            if (!is_numeric($rawDay) || !in_array((int) $rawDay, self::DAYS, true)) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    'روز هفته باید عددی بین ۰ (شنبه) و ۶ (جمعه) باشد',
                    422,
                    'day_of_week',
                );
            }

            if (!is_array($ranges)) {
                throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'بازه‌های هر روز باید آرایه باشد', 422, 'shifts');
            }

            $normalized[(int) $rawDay] = $this->assertRanges((int) $rawDay, $ranges);
        }

        return $normalized;
    }

    /**
     * @param array<int|string, mixed> $ranges
     * @return list<array{start_minute: int, end_minute: int}>
     */
    private function assertRanges(int $day, array $ranges): array
    {
        $parsed = [];

        foreach ($ranges as $range) {
            if (!is_array($range)) {
                throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'ساختار بازه درست نیست', 422, 'start_minute');
            }

            $start = $this->assertMinute($range['start_minute'] ?? null, $day, 'start_minute');
            $end   = $this->assertMinute($range['end_minute'] ?? null, $day, 'end_minute');

            if ($end <= $start) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf('در روز %d، پایان شیفت باید بعد از شروع آن باشد', $day),
                    422,
                    'end_minute',
                );
            }

            $parsed[] = ['start_minute' => $start, 'end_minute' => $end];
        }

        usort($parsed, static fn (array $a, array $b): int => $a['start_minute'] <=> $b['start_minute']);

        foreach ($parsed as $i => $range) {
            if ($i > 0 && $range['start_minute'] < $parsed[$i - 1]['end_minute']) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf('شیفت‌های روز %d با هم هم‌پوشانی دارند', $day),
                    422,
                    'start_minute',
                );
            }
        }

        return $parsed;
    }

    private function assertMinute(mixed $value, int $day, string $field): int
    {
        if (!is_numeric($value)) {
            throw new AppException(
                ErrorCodes::ERR_VALIDATION_002,
                sprintf('در روز %d مقدار %s الزامی است', $day, $field),
                422,
                $field,
            );
        }

        $minute = (int) $value;

        if ($minute < 0 || $minute > ResourceCalendar::MINUTES_IN_DAY) {
            throw new AppException(
                ErrorCodes::ERR_VALIDATION_001,
                sprintf('در روز %d مقدار %s باید بین ۰ و ۱۴۴۰ باشد', $day, $field),
                422,
                $field,
            );
        }

        return $minute;
    }

    /** @param array<string, mixed> $data */
    public function createException(ClinicResource $resource, array $data): ResourceException
    {
        $type  = is_string($data['type'] ?? null) ? $data['type'] : '';
        $start = $data['starts_at'] ?? null;
        $end   = $data['ends_at'] ?? null;

        if (!array_key_exists($type, ResourceException::TYPES)) {
            throw new AppException(
                ErrorCodes::ERR_VALIDATION_001,
                'نوع استثنا باید یکی از leave/absence/maintenance/closure باشد',
                422,
                'type',
            );
        }

        if (!is_numeric($start) || !is_numeric($end)) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_002, 'starts_at و ends_at الزامی‌اند', 422, 'starts_at');
        }

        try {
            $exception = new ResourceException($resource, $type, (int) $start, (int) $end);
        } catch (\InvalidArgumentException) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'پایان استثنا باید بعد از شروع آن باشد', 422, 'ends_at');
        }

        if (isset($data['reason']) && is_string($data['reason'])) {
            $exception->setReason(trim($data['reason']) === '' ? null : trim($data['reason']));
        }

        $this->em->persist($exception);
        $this->em->flush();

        return $exception;
    }

    /** @param array<string, mixed> $data */
    public function updateException(ResourceException $exception, array $data): ResourceException
    {
        $start = $data['starts_at'] ?? $exception->getStartsAt();
        $end   = $data['ends_at'] ?? $exception->getEndsAt();

        if (!is_numeric($start) || !is_numeric($end)) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'starts_at و ends_at باید عدد باشند', 422, 'starts_at');
        }

        try {
            $exception->reschedule((int) $start, (int) $end);
        } catch (\InvalidArgumentException) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'پایان استثنا باید بعد از شروع آن باشد', 422, 'ends_at');
        }

        if (array_key_exists('reason', $data)) {
            $reason = is_string($data['reason']) ? trim($data['reason']) : '';
            $exception->setReason($reason === '' ? null : $reason);
        }

        $this->em->flush();

        return $exception;
    }

    public function deleteException(ResourceException $exception): void
    {
        $this->em->remove($exception);
        $this->em->flush();
    }
}
