<?php

namespace App\ClinicInvitation\Service;

use App\Auth\Entity\User;
use App\Auth\Repository\UserRepository;
use App\Clinic\Entity\Clinic;
use App\ClinicInvitation\Entity\ClinicDoctorInvitation;
use App\ClinicInvitation\Repository\ClinicDoctorInvitationRepository;
use App\Doctor\Entity\Doctor;
use App\Doctor\Repository\DoctorRepository;
use App\Shared\Exception\AppException;
use App\Shared\Util\DisplayName;
use App\Sms\Service\SmsService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class ClinicInvitationService
{
    private const UNNAMED_DOCTOR = 'پزشک دعوت‌شده';

    public function __construct(
        private readonly ClinicDoctorInvitationRepository $repo,
        private readonly DoctorRepository                 $doctorRepo,
        private readonly UserRepository                   $userRepo,
        private readonly UserPasswordHasherInterface      $hasher,
        private readonly SmsService                       $smsService,
        private readonly EntityManagerInterface           $em,
        private readonly string                           $appUrl,
    ) {}

    public function invite(Clinic $clinic, User $invitedBy, string $mobile, ?string $name, ?string $specialty): ClinicDoctorInvitation
    {
        $existing = $this->repo->findPendingByMobileAndClinic($mobile, $clinic->getId());
        if ($existing !== null) {
            throw new AppException('ERR_CONFLICT_001', 'این شماره قبلاً برای این کلینیک دعوت شده است', 409);
        }

        $inv = new ClinicDoctorInvitation($clinic, $invitedBy, $mobile);
        $inv->setInvitedName($name);
        $inv->setInvitedSpecialty($specialty);

        // پروفایل پزشک همین‌جا ساخته می‌شود تا بلافاصله پس از دعوت قابل مشاهده باشد،
        // ولی بدون رمز عبور و بدون اتصال به کلینیک — اتصال فقط پس از پذیرش انجام می‌شود.
        $inv->setDoctor($this->provisionDoctor($inv));

        $this->repo->save($inv);
        $this->sendSms($inv, $clinic);

        return $inv;
    }

    public function resend(ClinicDoctorInvitation $inv): void
    {
        if (in_array($inv->getStatus(), [ClinicDoctorInvitation::STATUS_REMOVED, ClinicDoctorInvitation::STATUS_ACCEPTED], true)) {
            throw new AppException('ERR_CONFLICT_001', 'امکان ارسال مجدد دعوتنامه وجود ندارد', 409);
        }

        $inv->refresh();
        $this->em->flush();
        $this->sendSms($inv, $inv->getClinic());
    }

    public function changeStatus(ClinicDoctorInvitation $inv, string $status): void
    {
        $allowed = [
            ClinicDoctorInvitation::STATUS_PENDING,
            ClinicDoctorInvitation::STATUS_SUSPENDED,
            ClinicDoctorInvitation::STATUS_REMOVED,
        ];
        if (!in_array($status, $allowed, true)) {
            throw new AppException('ERR_VALIDATION_001', 'وضعیت نامعتبر است', 422);
        }

        // بازگشت به «در انتظار» فقط وقتی معنا دارد که لینک هم دوباره قابل استفاده شود،
        // پس توکن تازه می‌شود و پیامک مجدداً ارسال می‌گردد.
        if ($status === ClinicDoctorInvitation::STATUS_PENDING) {
            if (in_array($inv->getStatus(), [ClinicDoctorInvitation::STATUS_ACCEPTED, ClinicDoctorInvitation::STATUS_REJECTED], true)) {
                throw new AppException('ERR_CONFLICT_001', 'دعوتنامه پاسخ‌داده‌شده را نمی‌توان به حالت انتظار برگرداند', 409);
            }
            $inv->refresh();
            $this->em->flush();
            $this->sendSms($inv, $inv->getClinic());

            return;
        }

        $inv->setStatus($status);
        $this->em->flush();
    }

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

    public function accept(ClinicDoctorInvitation $inv): void
    {
        if (!$inv->isUsable()) {
            throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه منقضی یا غیرمعتبر است', 410);
        }

        $password = $this->em->wrapInTransaction(function () use ($inv): ?string {
            $doctor = $this->attachDoctorToClinic($inv);

            $inv->setStatus(ClinicDoctorInvitation::STATUS_ACCEPTED);
            $inv->markUsed();

            return $this->ensureLoginCredentials($doctor->getUser());
        });

        if ($password !== null) {
            $this->sendCredentialsSms($inv->getMobile(), $password);
        }
    }

    /**
     * ترمیم دعوت‌نامه‌ای که قبلاً «پذیرفته‌شده» ثبت شده ولی پروفایل پزشک برایش ساخته نشده است.
     * برخلاف accept()، وضعیت دعوت‌نامه را دست نمی‌زند.
     */
    public function repairAccepted(ClinicDoctorInvitation $inv): void
    {
        $password = $this->em->wrapInTransaction(function () use ($inv): ?string {
            $doctor = $this->attachDoctorToClinic($inv);

            return $this->ensureLoginCredentials($doctor->getUser());
        });

        if ($password !== null) {
            $this->sendCredentialsSms($inv->getMobile(), $password);
        }
    }

    /**
     * پروفایل پزشکِ دعوت‌نامه را قطعی می‌کند (در صورت نبود می‌سازد) و به کلینیک متصل می‌کند.
     */
    private function attachDoctorToClinic(ClinicDoctorInvitation $inv): Doctor
    {
        $doctor = $this->provisionDoctor($inv);
        $inv->setDoctor($doctor);

        if ($doctor->getOwnerStatus() !== 'claimed') {
            $doctor->transferOwnershipTo($doctor->getUser());
        }

        $clinic = $inv->getClinic();
        if (!$clinic->getDoctors()->contains($doctor)) {
            $clinic->getDoctors()->add($doctor);
        }

        return $doctor;
    }

    /**
     * پروفایل پزشک متناظر با شماره موبایل دعوت‌نامه را برمی‌گرداند و در صورت نبود
     * کاربر و پروفایل را می‌سازد. رمز عبور اینجا ست نمی‌شود — آن کار فقط هنگام پذیرش.
     */
    private function provisionDoctor(ClinicDoctorInvitation $inv): Doctor
    {
        $doctor = $inv->getDoctor() ?? $this->doctorRepo->findOneByMobile($inv->getMobile());
        if ($doctor !== null) {
            return $doctor;
        }

        $mobile = $inv->getMobile();
        // شمارهٔ موبایل نامِ پزشک نیست. دعوت بدون نام قبلاً موبایل را به‌عنوان نام
        // می‌نشاند و همان رکورد در نتایج عمومی و <title> صفحات سایت منتشر می‌شد.
        $name = $this->resolveInvitedName($inv->getInvitedName());

        $user = $this->userRepo->findOneBy(['mobileNumber' => $mobile]);
        if ($user === null) {
            $user = new User($mobile);
            $user->setRealName($name);
            $this->em->persist($user);
        }
        $user->addRole('ROLE_DOCTOR');

        $doctor = $this->doctorRepo->findOneBy(['user' => $user]);
        if ($doctor === null) {
            $doctor = new Doctor($user, $this->resolveInvitedName($user->getRealName()));
            $doctor->setMobileNumber($mobile);
            $doctor->setOwnerStatus('unclaimed');
            $this->em->persist($doctor);
        }

        return $doctor;
    }

    /**
     * نام پزشکِ دعوت‌شده. کلینیک اغلب فقط شمارهٔ موبایل را دارد، پس نام واقعی هنوز
     * ناشناخته است — یک برچسب خنثی می‌نشیند تا وقتی خود پزشک پروفایلش را claim کند.
     * هرگز موبایل یا مقدار آزمایشی برنمی‌گرداند.
     */
    private function resolveInvitedName(?string $candidate): string
    {
        return DisplayName::isPlaceholder($candidate) ? self::UNNAMED_DOCTOR : $candidate;
    }

    /**
     * برای کاربری که هنوز رمز عبور ندارد یک رمز تولید می‌کند تا بتواند وارد شود.
     * رمز کاربران موجود هرگز بازنویسی نمی‌شود.
     *
     * @return string|null رمز خام برای ارسال پیامک، یا null اگر کاربر از قبل رمز داشته
     */
    private function ensureLoginCredentials(User $user): ?string
    {
        if ($user->getPasswordHash() !== null) {
            return null;
        }

        $password = bin2hex(random_bytes(4));
        $user->setPasswordHash($this->hasher->hashPassword($user, $password));

        return $password;
    }

    public function reject(ClinicDoctorInvitation $inv): void
    {
        if (!$inv->isUsable()) {
            throw new AppException('ERR_NOT_FOUND_001', 'دعوتنامه منقضی یا غیرمعتبر است', 410);
        }
        $inv->setStatus(ClinicDoctorInvitation::STATUS_REJECTED);
        $inv->markUsed();
        $this->em->flush();
    }

    private function sendSms(ClinicDoctorInvitation $inv, Clinic $clinic): void
    {
        $clinicName = $clinic->getName() ?? 'کلینیک';
        $link = rtrim($this->appUrl, '/') . '/i/' . $inv->getToken();

        $this->smsService->dispatchTemplate(\App\Sms\Entity\SmsLog::TAG_CLINIC_INVITATION, $inv->getMobile(), [
            'clinic' => $clinicName,
            'link'   => $link,
        ]);
    }

    private function sendCredentialsSms(string $mobile, string $password): void
    {
        $this->smsService->dispatchTemplate(\App\Sms\Entity\SmsLog::TAG_PRE_REGISTRATION, $mobile, [
            'username' => $mobile,
            'password' => $password,
            'link'     => rtrim($this->appUrl, '/') . '/admin',
        ]);
    }
}
