<?php

namespace App\Branch\Service;

use App\Branch\Entity\BranchWorkingHours;
use App\Branch\Repository\BranchWorkingHoursRepository;
use App\Doctor\Entity\DoctorAddress;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Doctrine\ORM\EntityManagerInterface;

/**
 * ساعت کاری هفتگی شعبه — اعتبارسنجی و ذخیره.
 *
 * قرارداد نوشتن PUT است نه PATCH: بدنه تمام حقیقتِ هفت روز است و آرایهٔ خالی یعنی
 * «شعبه کاملاً بسته». دلیل: ساعت کاری یک شکل واحد است، و merge تفاضلی روی هفت روز و
 * چند بازه در هر روز، دو کلاینت هم‌زمان را به وضعیت‌های ناسازگار می‌رساند.
 *
 * «شعبهٔ بدون هیچ ساعت کاری» = تعریف‌نشده، نه همیشه‌باز. تسک ۰۳ در آن حالت به رفتار
 * فعلی برمی‌گردد (برنامهٔ پزشک تنها مرجع) تا دادهٔ موجود دقیقاً مثل امروز کار کند.
 */
final class WorkingHoursService
{
    public const DAYS = [0, 1, 2, 3, 4, 5, 6];

    public function __construct(
        private readonly BranchWorkingHoursRepository $hours,
        private readonly EntityManagerInterface       $em,
    ) {}

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

        foreach ($this->hours->findForAddress($address) as $row) {
            $result[$row->getDayOfWeek()][] = $row->toArray();
        }

        return $result;
    }

    public function isDefined(DoctorAddress $address): bool
    {
        return $this->hours->findForAddress($address) !== [];
    }

    /**
     * جایگزینی کامل هفت روز.
     *
     * @param array<int|string, mixed> $days نگاشت روز => فهرست بازه‌ها
     * @return array<int, list<array<string, mixed>>>
     * @throws AppException روی هر ورودی نامعتبر — پیش از هر تغییری در دیتابیس
     */
    public function replace(DoctorAddress $address, array $days): array
    {
        $normalized = $this->validate($days);

        // اعتبارسنجی کاملِ هر هفت روز قبل از DELETE: بازهٔ نامعتبر در روز ششم نباید
        // شش روز درستِ قبلی را هم پاک کند و بعد ۴۲۲ برگرداند.
        $this->hours->deleteForAddress($address);

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

        $this->em->flush();

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

    /**
     * @param array<int|string, mixed> $days
     * @return array<int, list<array{start_minute: int, end_minute: int}>> مرتب‌شده، بدون هم‌پوشانی
     * @throws AppException
     */
    private function validate(array $days): array
    {
        $normalized = array_fill_keys(self::DAYS, []);

        foreach ($days as $rawDay => $ranges) {
            $day = $this->assertDay($rawDay);

            if (!is_array($ranges)) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf('بازه‌های روز %d باید یک آرایه باشد', $day),
                    422,
                    (string) $rawDay,
                );
            }

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

        return $normalized;
    }

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

        return (int) $rawDay;
    }

    /**
     * @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,
                    sprintf('بازهٔ روز %d ساختار درستی ندارد', $day),
                    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']);

        // sequence از همین ترتیب مشتق می‌شود، پس تشخیص هم‌پوشانی فقط مقایسهٔ همسایه‌هاست.
        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 > BranchWorkingHours::MINUTES_IN_DAY) {
            throw new AppException(
                ErrorCodes::ERR_VALIDATION_001,
                sprintf('در روز %d مقدار %s باید بین ۰ و ۱۴۴۰ باشد', $day, $field),
                422,
                $field,
            );
        }

        return $minute;
    }
}
