<?php

namespace App\Doctor\Service;

use App\Auth\Entity\User;
use App\Doctor\Entity\Doctor;
use App\Doctor\Entity\DoctorClaimRequest;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use App\Shared\Service\ApiIrService;
use App\Shared\Util\PersianText;
use App\Sms\Entity\SmsLog;
use App\Sms\Service\SmsService;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;

/**
 * تصاحب پروفایل پزشک ایمپورت‌شده (unclaimed) توسط پزشک واقعی.
 *
 * دو مسیر: self-claim (احراز هویت API.ir: شاهکار + PersonInfo + تطبیق نام) و
 * انتقال دستی ادمین. نهایی‌سازی هر دو مسیر یکی است: اتصال کاربر واقعی، نقش
 * ROLE_DOCTOR، حذف امن کاربر جانشین، و ثبت رکورد ممیزی DoctorClaimRequest.
 *
 * ضد race: تغییر وضعیت unclaimed→pending_transfer زیر قفل PESSIMISTIC_WRITE
 * انجام می‌شود؛ درخواست هم‌زمان دوم 409 می‌گیرد. فراخوانی خارجی هرگز داخل قفل نیست.
 */
class DoctorClaimService
{
    public const METHOD_APIIR         = 'apiir_personinfo';
    public const METHOD_APIIR_SHAHKAR = 'apiir_personinfo+shahkar';
    public const METHOD_ADMIN         = 'admin_manual';

    public function __construct(
        private readonly EntityManagerInterface $em,
        private readonly ApiIrService           $apiIr,
        private readonly SmsService             $smsService,
        private readonly LoggerInterface        $logger,
    ) {}

    public function claim(Doctor $doctor, User $user, string $nationalCode, string $birthDateJalali, string $firstName, string $lastName): DoctorClaimRequest
    {
        $method = $this->apiIr->isConfigured() ? self::METHOD_APIIR_SHAHKAR : self::METHOD_APIIR;

        // مرحلهٔ ۱ — رزرو اتمیک پروفایل زیر قفل (تراکنش کوتاه، بدون فراخوان خارجی)
        $claim = $this->em->wrapInTransaction(function () use ($doctor, $user, $nationalCode, $method): DoctorClaimRequest {
            $locked = $this->em->find(Doctor::class, $doctor->getId(), LockMode::PESSIMISTIC_WRITE);

            if ($locked->getOwnerStatus() !== 'unclaimed') {
                throw new AppException(ErrorCodes::ERR_CONFLICT_001, 'این پروفایل قابل تصاحب نیست یا درخواست دیگری در جریان است', 409);
            }
            $existing = $this->em->getRepository(Doctor::class)->findOneBy(['user' => $user]);
            if ($existing !== null) {
                throw new AppException(ErrorCodes::ERR_CONFLICT_001, 'شما از قبل یک پروفایل پزشک دارید', 409);
            }
            $codeOwner = $this->em->getRepository(User::class)->findOneBy(['nationalCode' => $nationalCode]);
            if ($codeOwner !== null && $codeOwner->getId() !== $user->getId()) {
                throw new AppException(ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN, null, 409);
            }

            $locked->setOwnerStatus('pending_transfer');
            $claim = new DoctorClaimRequest($locked, $user, $nationalCode, $user->getMobileNumber(), $method);
            $this->em->persist($claim);

            return $claim;
        });

        // مرحلهٔ ۲ — احراز هویت (خارج از قفل)
        try {
            $this->verifyIdentity($doctor, $user, $nationalCode, $birthDateJalali, $firstName, $lastName);
        } catch (AppException $e) {
            $this->revert($doctor, $claim, $e->getMessage());
            throw $e;
        }

        // مرحلهٔ ۳ — نهایی‌سازی اتمیک
        $this->finalize($doctor, $user, $claim, $nationalCode);

        // مرحلهٔ ۴ — پیامک خوش‌آمد (غیر بحرانی؛ شکستش claim را باطل نمی‌کند)
        $this->smsService->dispatchTemplate(SmsLog::TAG_WELCOME, $user->getMobileNumber(), [
            'name' => PersianText::stripDoctorTitle($doctor->getName()),
            'site' => 'نوبت۷۲۴',
        ]);

        return $claim;
    }

    /** انتقال دستی توسط ادمین (پشتیبانی) — بدون استعلام هویت؛ کاربر هدف با موبایل پیدا/ساخته می‌شود. */
    public function transferByAdmin(Doctor $doctor, string $mobile): DoctorClaimRequest
    {
        $claim = $this->em->wrapInTransaction(function () use ($doctor, $mobile): DoctorClaimRequest {
            $locked = $this->em->find(Doctor::class, $doctor->getId(), LockMode::PESSIMISTIC_WRITE);

            if ($locked->getOwnerStatus() === 'claimed') {
                throw new AppException(ErrorCodes::ERR_CONFLICT_001, 'این پروفایل قبلاً تصاحب شده است', 409);
            }

            $userRepo = $this->em->getRepository(User::class);
            $target   = $userRepo->findOneBy(['mobileNumber' => $mobile]);
            if ($target === null) {
                $target = new User($mobile);
                $target->setRealName(PersianText::stripDoctorTitle($locked->getName()));
                $target->setStatus(1);
                $this->em->persist($target);
            }

            $existing = $this->em->getRepository(Doctor::class)->findOneBy(['user' => $target]);
            if ($existing !== null && $existing->getId() !== $locked->getId()) {
                throw new AppException(ErrorCodes::ERR_CONFLICT_001, 'این کاربر قبلاً پروفایل پزشک دیگری دارد', 409);
            }

            $locked->setOwnerStatus('pending_transfer');
            $claim = new DoctorClaimRequest($locked, $target, '', $mobile, self::METHOD_ADMIN);
            $this->em->persist($claim);

            return $claim;
        });

        $this->finalize($doctor, $claim->getUser(), $claim, null);

        return $claim;
    }

    private function verifyIdentity(Doctor $doctor, User $user, string $nationalCode, string $birthDateJalali, string $firstName, string $lastName): void
    {
        // تطبیق موبایل ↔ کد ملی (شاهکار). بدون پیکربندی api.ir استعلام ممکن نیست →
        // موبایلِ OTP-تأییدشدهٔ کاربر لاگین‌شده مبنا می‌ماند و فقط PersonInfo چک می‌شود.
        if ($this->apiIr->isConfigured() && !$this->apiIr->shahkarMatch($nationalCode, $user->getMobileNumber())) {
            throw new AppException(ErrorCodes::ERR_NATIONAL_CODE_MISMATCH, null, 422);
        }

        $person = $this->apiIr->personInfo($nationalCode, $birthDateJalali);
        if ($person === null || !$person['alive']) {
            throw new AppException(ErrorCodes::ERR_NATIONAL_CODE_MISMATCH, 'اطلاعات هویتی با سامانهٔ ثبت احوال مطابقت ندارد', 422);
        }

        // ورودی کاربر ↔ هویت تأییدشده
        if (!PersianText::sameName($firstName . ' ' . $lastName, $person['firstName'] . ' ' . $person['lastName'])) {
            throw new AppException(ErrorCodes::ERR_NATIONAL_CODE_MISMATCH, 'نام واردشده با اطلاعات هویتی مطابقت ندارد', 422);
        }

        // هویت تأییدشده ↔ نام پروفایل ایمپورت‌شده از نظام پزشکی
        $profileName  = PersianText::stripDoctorTitle($doctor->getName());
        $verifiedName = PersianText::normalize($person['firstName'] . ' ' . $person['lastName']);
        if ($profileName !== $verifiedName) {
            throw new AppException(ErrorCodes::ERR_NATIONAL_CODE_MISMATCH, 'نام شما با نام این پروفایل پزشک مطابقت ندارد', 422);
        }
    }

    private function finalize(Doctor $doctor, User $target, DoctorClaimRequest $claim, ?string $nationalCode): void
    {
        $surrogate = $this->em->wrapInTransaction(function () use ($doctor, $target, $claim, $nationalCode): ?User {
            $locked = $this->em->find(Doctor::class, $doctor->getId(), LockMode::PESSIMISTIC_WRITE);

            if ($locked->getOwnerStatus() !== 'pending_transfer') {
                throw new AppException(ErrorCodes::ERR_CONFLICT_001, 'وضعیت پروفایل در این میان تغییر کرده است', 409);
            }

            $surrogate = $locked->getUser();

            if ($nationalCode !== null && $nationalCode !== '') {
                $target->setNationalCode($nationalCode);
                $target->setNationalCodeVerified(true);
            }
            $target->addRole('ROLE_DOCTOR');
            $locked->transferOwnershipTo($target);
            $claim->markCompleted();

            return $surrogate;
        });

        // حذف امن جانشین — پس از flush انتقال، تا شمارش پزشکانِ متصل قطعی باشد
        if ($surrogate !== null
            && $surrogate->getId() !== $target->getId()
            && $surrogate->hasRole(DoctorImportService::ROLE_UNCLAIMED_DOCTOR)
            && $this->em->getRepository(Doctor::class)->count(['user' => $surrogate]) === 0) {
            $this->em->remove($surrogate);
            $this->em->flush();
        }

        $this->logger->info('doctor profile claimed', [
            'claim_uuid'  => $claim->getUuid(),
            'doctor_uuid' => $doctor->getUuid(),
            'user_id'     => $target->getId(),
            'method'      => $claim->getVerificationMethod(),
        ]);
    }

    private function revert(Doctor $doctor, DoctorClaimRequest $claim, string $reason): void
    {
        $this->em->wrapInTransaction(function () use ($doctor, $claim, $reason): void {
            $locked = $this->em->find(Doctor::class, $doctor->getId(), LockMode::PESSIMISTIC_WRITE);
            if ($locked->getOwnerStatus() === 'pending_transfer') {
                $locked->setOwnerStatus('unclaimed');
            }
            $claim->markFailed($reason);
        });
    }
}
