<?php

namespace App\Patient\Controller;

use App\Auth\Entity\User;
use App\Auth\Repository\UserActiveContextRepository;
use App\Auth\Repository\UserRepository;
use App\Billing\Repository\InvoiceRepository;
use App\Billing\Service\ClaimService;
use App\Billing\Service\InvoiceService;
use Psr\Log\LoggerInterface;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Patient\Entity\PatientRecord;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Patient\Repository\PatientRecordRepository;
use App\Patient\Repository\PatientSessionRepository;
use App\Patient\Service\PatientService;
use App\UserProfile\Entity\UserProfile;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Exception\AppException;
use App\Subscription\Service\SubscriptionService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use OpenApi\Attributes as OA;

#[OA\Tag(name: 'Patients')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class PatientController extends BaseController
{
    public function __construct(
        private readonly PatientRecordRepository     $recordRepo,
        private readonly PatientSessionRepository    $sessionRepo,
        private readonly PatientService              $patientService,
        private readonly SubscriptionService         $subscriptionService,
        private readonly UserRepository              $userRepo,
        private readonly DoctorRepository            $doctorRepo,
        private readonly ClinicRepository            $clinicRepo,
        private readonly DoctorSecretaryRepository   $secretaryRepo,
        private readonly UserActiveContextRepository $contextRepo,
        private readonly \App\UserProfile\Repository\UserProfileRepository $profileRepo,
        private readonly \App\Insurance\Repository\InsuranceRepository $insuranceRepo,
        private readonly InvoiceService              $invoiceService,
        private readonly ClaimService                $claimService,
        private readonly InvoiceRepository           $invoiceRepo,
        private readonly \App\Appointment\Repository\AppointmentRepository $appointmentRepo,
        private readonly \App\ClinicInvitation\Repository\ClinicDoctorInvitationRepository $invitationRepo,
        private readonly LoggerInterface             $logger,
    ) {}

    private function buildPatientProfile(User $patient): array
    {
        $p = $this->profileRepo->findByUser($patient);
        $insName = function (?int $id): ?string {
            if ($id === null) { return null; }
            return $this->insuranceRepo->find($id)?->getName();
        };

        return [
            'full_name'                  => trim(($patient->getRealName() ?? '') . ' ' . ($p?->getFamily() ?? '')) ?: $patient->getRealName(),
            'name'                       => $patient->getRealName(),
            'family'                     => $p?->getFamily(),
            'fathers_name'               => $p?->getFathersName(),
            'national_code'              => $p?->getNationalCode() ?? $patient->getNationalCode(),
            'gender'                     => $p?->getGender(),
            'date_of_birth'              => $p?->getDateOfBirth(),
            'blood_type'                 => $p?->getBloodType(),
            'marital_status'             => $p?->getMaritalStatus(),
            'education'                  => $p?->getEducation(),
            'field_of_study'            => $p?->getFieldOfStudy(),
            'job'                        => $p?->getJob(),
            'address'                    => $p?->getAddress(),
            'province_id'                => $p?->getProvinceId(),
            'city_id'                    => $p?->getCityId(),
            'postal_code'                => $p?->getPostalCode(),
            'referral_source'            => $p?->getReferralSource(),
            'description'                => $p?->getDescription(),
            'home_phone'                 => $p?->getHomePhone(),
            'work_phone'                 => $p?->getWorkPhone(),
            'mobile'                     => $patient->getMobileNumber(),
            'basic_insurance_id'         => $p?->getBasicInsuranceId(),
            'basic_insurance_name'       => $insName($p?->getBasicInsuranceId()),
            'supplementary_insurance_id' => $p?->getSupplementaryInsuranceId(),
            'supplementary_insurance_name' => $insName($p?->getSupplementaryInsuranceId()),
        ];
    }

    #[Route('/api/v1/patient/search-user', methods: ['GET'])]
    public function searchUser(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $mobile = trim($request->query->get('mobile', ''));
        if ($mobile === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'mobile الزامی است', 422);
        }

        $patient = $this->userRepo->findByMobile($mobile);
        if ($patient === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'کاربری با این شماره یافت نشد', 404);
        }

        return $this->success([
            'uuid' => $patient->getUuid(),
            'name' => $patient->getRealName(),
            'mobile' => $patient->getMobileNumber(),
            'national_code' => $patient->getNationalCode(),
        ]);
    }

    #[Route('/api/v1/patients', methods: ['GET'])]
    public function list(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(50, max(10, (int) $request->query->get('limit', 20)));
        $search = $request->query->get('search') ?: null;

        $records = $this->recordRepo->findByEntity($entityType, $entityId, $page, $limit, $search);
        $total   = $this->recordRepo->countByEntity($entityType, $entityId, $search);

        return $this->paginated(
            array_map(fn(PatientRecord $r) => $r->toArray(), $records),
            $total,
            $page,
            $limit
        );
    }

    #[Route('/api/v1/patient', methods: ['POST'])]
    public function create(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $data     = json_decode($request->getContent(), true) ?? [];
        $userUuid = trim($data['user_uuid'] ?? '');
        $mobile   = trim($data['mobile'] ?? '');
        $name     = trim($data['name'] ?? '');

        $nationalCode = trim((string) ($data['national_code'] ?? ''));
        if ($nationalCode !== '' && !preg_match('/^\d{10}$/', $nationalCode)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی باید ۱۰ رقم باشد', 422);
        }

        $patient = null;
        if ($userUuid !== '') {
            $patient = $this->userRepo->findByUuid($userUuid);
        } elseif ($mobile !== '') {
            if (!preg_match('/^09\d{9}$/', $mobile)) {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422);
            }
            $patient = $this->userRepo->findByMobile($mobile);
        }

        // کد ملی باید در سطح بیمار یکتا باشد: اگر به پروفایلِ کاربر دیگری تعلق دارد، رد کن
        if ($nationalCode !== '') {
            $owner = $this->profileRepo->findOneByNationalCode($nationalCode);
            if ($owner !== null && ($patient === null || $owner->getUser()->getId() !== $patient->getId())) {
                $masked = \App\Shared\Service\InputValidator::maskMobile($owner->getUser()->getMobileNumber());
                return $this->error(
                    ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN,
                    "این کد ملی قبلاً با شماره {$masked} ثبت شده است",
                    409,
                    'national_code'
                );
            }
        }

        // بیمار جدید بدون ثبت‌نام قبلی: موبایل + نام آمده ولی کاربری وجود ندارد
        if ($patient === null) {
            if ($mobile === '' || $name === '') {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'برای ساخت بیمار جدید، شماره موبایل و نام الزامی است', 422);
            }
            $patient = new User($mobile);
            $patient->setRealName($name);
            if ($nationalCode !== '') {
                $patient->setNationalCode($nationalCode);
            }
            $this->userRepo->save($patient);
        } elseif ($nationalCode !== '' && $patient->getNationalCode() === null) {
            $patient->setNationalCode($nationalCode);
            $this->userRepo->save($patient);
        }

        $existing = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
        if ($existing !== null) {
            return $this->success($existing->toArray());
        }

        $record = new PatientRecord($entityType, $entityId, $patient, $user->hasRole('ROLE_DOCTOR') ? 'doctor' : 'clinic', $entityId);
        $this->recordRepo->save($record);

        return $this->success($record->toArray(), 201);
    }

    #[Route('/api/v1/patient/{uuid}', methods: ['GET'])]
    public function show(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data = $record->toArray();
        $data['profile'] = $this->buildPatientProfile($record->getUser());

        return $this->success($data);
    }

    #[Route('/api/v1/patient/{uuid}', methods: ['PATCH'])]
    public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $patient = $record->getUser();
        $data    = json_decode($request->getContent(), true) ?? [];

        if (array_key_exists('name', $data)) {
            $name = trim((string) $data['name']);
            if ($name !== '') {
                $patient->setRealName($name);
            }
        }

        // کد ملی باید ۱۰ رقم و در سطح بیمار یکتا باشد
        if (array_key_exists('national_code', $data)) {
            $nationalCode = trim((string) ($data['national_code'] ?? ''));
            if ($nationalCode !== '') {
                if (!preg_match('/^\d{10}$/', $nationalCode)) {
                    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی باید ۱۰ رقم باشد', 422, 'national_code');
                }
                $owner = $this->profileRepo->findOneByNationalCode($nationalCode);
                if ($owner !== null && $owner->getUser()->getId() !== $patient->getId()) {
                    $masked = \App\Shared\Service\InputValidator::maskMobile($owner->getUser()->getMobileNumber());
                    return $this->error(
                        ErrorCodes::ERR_PROFILE_NATIONAL_CODE_TAKEN,
                        "این کد ملی قبلاً با شماره {$masked} ثبت شده است",
                        409,
                        'national_code'
                    );
                }
                $patient->setNationalCode($nationalCode);
            }
        }

        // موبایل = شناسه ورود کاربر؛ تغییر آن باید ۱۱ رقمی معتبر و در سطح کاربران یکتا باشد
        if (array_key_exists('mobile', $data)) {
            $mobile = trim((string) ($data['mobile'] ?? ''));
            if ($mobile !== '' && $mobile !== $patient->getMobileNumber()) {
                if (!preg_match('/^09\d{9}$/', $mobile)) {
                    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422, 'mobile');
                }
                $owner = $this->userRepo->findByMobile($mobile);
                if ($owner !== null && $owner->getId() !== $patient->getId()) {
                    return $this->error(
                        ErrorCodes::ERR_PROFILE_MOBILE_TAKEN,
                        ErrorCodes::message(ErrorCodes::ERR_PROFILE_MOBILE_TAKEN),
                        409,
                        'mobile'
                    );
                }
                $patient->setMobileNumber($mobile);
            }
        }
        $this->userRepo->save($patient);

        $profile = $this->profileRepo->findByUser($patient) ?? new UserProfile($patient);

        $stringFields = [
            'family'          => 'setFamily',
            'fathers_name'    => 'setFathersName',
            'gender'          => 'setGender',
            'blood_type'      => 'setBloodType',
            'marital_status'  => 'setMaritalStatus',
            'education'       => 'setEducation',
            'field_of_study'  => 'setFieldOfStudy',
            'job'             => 'setJob',
            'address'         => 'setAddress',
            'postal_code'     => 'setPostalCode',
            'referral_source' => 'setReferralSource',
            'description'     => 'setDescription',
            'home_phone'      => 'setHomePhone',
            'work_phone'      => 'setWorkPhone',
        ];
        foreach ($stringFields as $key => $setter) {
            if (array_key_exists($key, $data)) {
                $v = is_string($data[$key]) ? trim($data[$key]) : $data[$key];
                $profile->$setter($v === '' ? null : $v);
            }
        }

        $intFields = [
            'province_id' => 'setProvinceId',
            'city_id'     => 'setCityId',
        ];
        foreach ($intFields as $key => $setter) {
            if (array_key_exists($key, $data)) {
                $v = $data[$key];
                $profile->$setter(($v === null || $v === '') ? null : (int) $v);
            }
        }

        if (array_key_exists('national_code', $data)) {
            $nc = trim((string) ($data['national_code'] ?? ''));
            $profile->setNationalCode($nc === '' ? null : $nc);
        }
        if (array_key_exists('date_of_birth', $data)) {
            $dob = $data['date_of_birth'];
            $profile->setDateOfBirth(($dob === null || $dob === '') ? null : (int) $dob);
        }
        if (array_key_exists('basic_insurance_id', $data)) {
            $bi = $data['basic_insurance_id'];
            $profile->setBasicInsuranceId(($bi === null || $bi === '') ? null : (int) $bi);
        }
        if (array_key_exists('supplementary_insurance_id', $data)) {
            $si = $data['supplementary_insurance_id'];
            $profile->setSupplementaryInsuranceId(($si === null || $si === '') ? null : (int) $si);
        }
        $this->profileRepo->save($profile);

        $out = $record->toArray();
        $out['profile'] = $this->buildPatientProfile($patient);

        return $this->success($out);
    }

    #[Route('/api/v1/patient/{uuid}/sessions', methods: ['GET'])]
    public function sessions(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $page     = max(1, (int) $request->query->get('page', 1));
        $limit    = min(50, max(10, (int) $request->query->get('limit', 20)));
        $sessions = $this->sessionRepo->findByRecord($record, $page, $limit);
        $total    = $this->sessionRepo->countByRecord($record);

        return $this->paginated(
            array_map(fn($s) => $this->sessionWithBilling($s), $sessions),
            $total,
            $page,
            $limit
        );
    }

    #[Route('/api/v1/patient/{uuid}/appointments', methods: ['GET'])]
    public function appointments(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        // نوبت‌های این بیمار فقط با پزشک(های) همین ارائه‌دهنده نمایش داده می‌شوند تا
        // نوبت‌های او با کلینیک‌های دیگر نشت نکند.
        $doctorIds = $entityType === 'doctor'
            ? [$entityId]
            : $this->invitationRepo->acceptedDoctorIdsByClinic($entityId);

        $appointments = $this->appointmentRepo->findByUserAndDoctorIds($record->getUser(), $doctorIds);

        return $this->success(array_map(fn(\App\Appointment\Entity\Appointment $a) => [
            'uuid'         => $a->getUuid(),
            'starts_at'    => $a->getSlotStart(),
            'ends_at'      => $a->getSlotEnd(),
            'status'       => $a->getStatus(),
            'doctor_name'  => $a->getDoctor()->getName(),
            'service_name' => null,
            'price_rials'  => null,
            'created_at'   => $a->getSlotStart(),
        ], $appointments));
    }

    /**
     * خروجی session به‌همراه خلاصه‌ی صورتحساب: uuid فاکتور (در صورت وجود) و
     * مانده‌ی بدهیِ سهم بیمار. اگر session تسویه شده باشد (payment_method != pending)
     * بدهی صفر است؛ در غیر این صورت سهم بیمار از فاکتور یا کل مبلغ نهایی.
     */
    private function sessionWithBilling(\App\Patient\Entity\PatientSession $session): array
    {
        $data    = $session->toArray();
        $invoice = $session->getId() !== null ? $this->invoiceRepo->findBySession($session->getId()) : null;

        $data['invoice_uuid']   = $invoice?->getUuid();
        $data['invoice_status'] = $invoice?->getStatus();

        if ($data['is_paid']) {
            $data['patient_debt_rials'] = 0;
        } elseif ($invoice !== null) {
            $data['patient_debt_rials'] = $invoice->getPatientRials();
        } else {
            $data['patient_debt_rials'] = $session->getFinalPriceRials();
        }

        return $data;
    }

    #[Route('/api/v1/patient/{uuid}/session', methods: ['POST'])]
    public function createSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $record = $this->recordRepo->findByUuid($uuid);
        if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
            return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
        }

        $data    = json_decode($request->getContent(), true) ?? [];
        $session = $this->patientService->createSession($record, $data, $entityType, $entityId);

        $this->autoCreateClaim($session, $entityType, $entityId);

        return $this->success($session->toArray(), 201);
    }

    /**
     * اتصال خودکار مطالبه‌ی بیمه: اگر session بیمه‌ی پایه یا مکمل داشته باشد،
     * صورتحساب ساخته و نهایی می‌شود و مطالبه (در وضعیت pending) ایجاد می‌گردد.
     * ارسال مطالبه دستی از صفحه‌ی مطالبات انجام می‌شود.
     * خطا در این مرحله نباید ثبت session را خراب کند.
     */
    private function autoCreateClaim(\App\Patient\Entity\PatientSession $session, string $entityType, int $entityId): void
    {
        if ($session->getInsuranceBaseId() === null && $session->getInsuranceSupplementaryId() === null) {
            return;
        }

        try {
            $invoice = $this->invoiceService->createFromSession($session, $entityType, $entityId);
            $this->invoiceService->finalize($invoice);
            $this->claimService->createFromInvoice($invoice);
        } catch (\App\Shared\Exception\AppException $e) {
            // سهم بیمه‌ای صفر بود یا صورتحساب از قبل وجود داشت — قابل چشم‌پوشی.
            $this->logger->info('auto claim skipped', ['session' => $session->getUuid(), 'reason' => $e->getMessage()]);
        } catch (\Throwable $e) {
            $this->logger->error('auto claim failed', ['session' => $session->getUuid(), 'error' => $e->getMessage()]);
        }
    }

    #[Route('/api/v1/session/{uuid}', methods: ['PATCH'])]
    public function updateSession(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $this->assertPatientGate($entityType, $entityId);

        $session = $this->sessionRepo->findByUuid($uuid);
        if ($session === null || !$this->ownsRecord($session->getRecord(), $entityType, $entityId)) {
            return $this->error(ErrorCodes::ERR_SESSION_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_SESSION_NOT_FOUND), 404);
        }

        $data = json_decode($request->getContent(), true) ?? [];

        if (isset($data['notes']))          { $session->setNotes($data['notes']); }
        if (isset($data['payment_method'])) { $session->setPaymentMethod($data['payment_method']); }

        $this->sessionRepo->save($session);

        return $this->success($session->toArray());
    }

    private function resolveEntity(User $user): array
    {
        if ($user->hasRole('ROLE_DOCTOR')) {
            $doctor = $this->doctorRepo->findByUser($user);
            return $doctor !== null ? ['doctor', $doctor->getId()] : ['doctor', null];
        }

        if ($user->hasRole('ROLE_CLINIC')) {
            $clinic = $this->clinicRepo->findByUser($user);
            return $clinic !== null ? ['clinic', $clinic->getId()] : ['clinic', null];
        }

        if ($user->hasRole('ROLE_SECRETARY')) {
            $dbUuid = $this->contextRepo->findByUser($user)?->getDbUuid();
            if ($dbUuid !== null) {
                $clinic = $this->clinicRepo->findByUuid($dbUuid);
                if ($clinic !== null) {
                    $rel = $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic);
                    if ($rel !== null) {
                        return ['clinic', $clinic->getId()];
                    }
                }
                $doctor = $this->doctorRepo->findByUuid($dbUuid);
                if ($doctor !== null) {
                    $rel = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
                    if ($rel !== null) {
                        return ['doctor', $doctor->getId()];
                    }
                }
            }
        }

        return ['unknown', null];
    }

    private function assertPatientGate(string $entityType, ?int $entityId): void
    {
        if ($entityId === null) {
            throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
        }

        if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) {
            throw new AppException(ErrorCodes::ERR_SUBSCRIPTION_REQUIRED, null, 403);
        }
    }

    private function ownsRecord($record, string $entityType, ?int $entityId): bool
    {
        return $entityId !== null
            && $record->getEntityType() === $entityType
            && $record->getEntityId() === $entityId;
    }
}
