<?php

namespace App\Resource\Service;

use App\Doctor\Entity\DoctorAddress;
use App\Resource\Entity\ClinicResource;
use App\Resource\Entity\ResourceType;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Doctrine\ORM\EntityManagerInterface;

final class ResourceService
{
    /** کلیدهای شناخته‌شدهٔ `attributes` — قرارداد، نه اجبار. */
    public const KNOWN_ATTRIBUTES = ['gender', 'device_model', 'floor', 'brand'];

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

    /** @param array<string, mixed> $data */
    public function create(DoctorAddress $address, ResourceType $type, array $data): ClinicResource
    {
        $resource = new ClinicResource($address, $type, $this->assertName($data['name'] ?? null));
        $this->applyOptional($resource, $data);

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

        return $resource;
    }

    /** @param array<string, mixed> $data */
    public function update(ClinicResource $resource, array $data): ClinicResource
    {
        if (array_key_exists('name', $data)) {
            $resource->setName($this->assertName($data['name']));
        }

        $this->applyOptional($resource, $data);
        $this->em->flush();

        return $resource;
    }

    public function delete(ClinicResource $resource): void
    {
        $this->em->remove($resource);
        $this->em->flush();
    }

    /** @param array<string, mixed> $data */
    private function applyOptional(ClinicResource $resource, array $data): void
    {
        if (array_key_exists('capacity', $data)) {
            $this->guard('capacity', fn () => $resource->setCapacity($this->assertInt($data['capacity'], 'capacity')));
        }

        if (array_key_exists('setup_minutes', $data)) {
            $this->guard('setup_minutes', fn () => $resource->setSetupMinutes($this->assertInt($data['setup_minutes'], 'setup_minutes')));
        }

        if (array_key_exists('cleanup_minutes', $data)) {
            $this->guard('cleanup_minutes', fn () => $resource->setCleanupMinutes($this->assertInt($data['cleanup_minutes'], 'cleanup_minutes')));
        }

        if (array_key_exists('attributes', $data)) {
            $resource->setAttributes($this->normalizeAttributes($data['attributes']));
        }

        if (array_key_exists('active', $data)) {
            $resource->setActive((bool) $data['active']);
        }
    }

    /**
     * `attributes` آزاد است ولی نه بی‌قید: کلید `[a-z_]{1,40}`، مقدار فقط اسکالر،
     * حداکثر ۲۰ کلید.
     *
     * محدودیت اسکالر عمدی است: تسک ۰۵ قید `same_gender` و تسک ۰۹ شرط‌های منبع را با
     * مقایسهٔ ساده روی همین مقادیر می‌سنجند. آرایهٔ تودرتو یعنی مقایسهٔ دلخواه — همان
     * چیزی که بند ۸ مستند ممنوع کرده.
     *
     * @return array<string, scalar>
     */
    public function normalizeAttributes(mixed $raw): array
    {
        if ($raw === null || $raw === []) {
            return [];
        }

        if (!is_array($raw)) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'ویژگی‌ها باید یک شیء باشد', 422, 'attributes');
        }

        if (count($raw) > ClinicResource::MAX_ATTRIBUTES) {
            throw new AppException(
                ErrorCodes::ERR_VALIDATION_001,
                sprintf('حداکثر %d ویژگی مجاز است', ClinicResource::MAX_ATTRIBUTES),
                422,
                'attributes',
            );
        }

        $out = [];
        foreach ($raw as $key => $value) {
            if (!is_string($key) || preg_match('/^[a-z_]{1,40}$/', $key) !== 1) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    'نام ویژگی فقط حروف کوچک انگلیسی و زیرخط می‌پذیرد',
                    422,
                    'attributes',
                );
            }

            if ($value !== null && !is_scalar($value)) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf('مقدار ویژگی «%s» باید یک مقدار ساده باشد', $key),
                    422,
                    'attributes',
                );
            }

            if ($value !== null) {
                $out[$key] = $value;
            }
        }

        return $out;
    }

    /**
     * قواعد عددی در خودِ entity زندگی می‌کنند تا هیچ مسیری دورشان نزند؛ اینجا فقط
     * استثنای انگلیسیِ آن‌ها به خطای HTTP فارسی با **فیلد درست** ترجمه می‌شود.
     */
    private function guard(string $field, callable $apply): void
    {
        try {
            $apply();
        } catch (\InvalidArgumentException $e) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, $this->persianFor($e->getMessage()), 422, $field);
        }
    }

    private function assertName(mixed $value): string
    {
        $name = is_string($value) ? trim($value) : '';

        if ($name === '') {
            throw new AppException(ErrorCodes::ERR_VALIDATION_002, 'نام منبع الزامی است', 422, 'name');
        }

        if (mb_strlen($name) > 150) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'نام منبع حداکثر ۱۵۰ نویسه است', 422, 'name');
        }

        return $name;
    }

    private function assertInt(mixed $value, string $field): int
    {
        if (!is_numeric($value)) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, sprintf('%s باید عدد باشد', $field), 422, $field);
        }

        return (int) $value;
    }

    private function persianFor(string $englishMessage): string
    {
        return match (true) {
            str_contains($englishMessage, 'at least 1')      => 'ظرفیت منبع حداقل ۱ است',
            str_contains($englishMessage, 'more than one')   => 'منبعی که یک شخص است نمی‌تواند ظرفیت بیش از ۱ داشته باشد',
            str_contains($englishMessage, 'between 0 and')   => 'زمان آماده‌سازی/تمیزکاری باید بین ۰ تا ۴۸۰ دقیقه باشد',
            default                                          => 'ورودی نامعتبر است',
        };
    }
}
