<?php

namespace App\Appointment\Controller;

use App\Appointment\Entity\Appointment;
use App\Appointment\Entity\WeeklySchedule;
use App\Appointment\Repository\AppointmentRepository;
use App\Appointment\Repository\SlotTakenException;
use App\Appointment\Repository\WeeklyScheduleRepository;
use App\Appointment\Service\SlotCalculatorService;
use App\Doctor\Entity\Doctor;
use App\Auth\Entity\User;
use App\Doctor\Repository\DoctorRepository;
use App\Patient\Service\PatientService;
use App\Shared\Service\InputValidator;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use Doctrine\ORM\OptimisticLockException;
use OpenApi\Attributes as OA;
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;

#[OA\Tag(name: 'Appointments')]
class AppointmentController extends BaseController
{
    public function __construct(
        private readonly AppointmentRepository  $appointmentRepo,
        private readonly DoctorRepository       $doctorRepo,
        private readonly SlotCalculatorService  $slotCalculator,
        private readonly PatientService         $patientService,
        private readonly WeeklyScheduleRepository $scheduleRepo,
        private readonly \App\Representation\Service\DomainContextResolver $domainResolver,
    ) {}

    // ── Public: available slots ───────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/appointment-slots',
        summary: 'Get available appointment slots for a doctor on a given date',
        parameters: [
            new OA\Parameter(
                name: 'doctor_uuid',
                in: 'query',
                required: true,
                schema: new OA\Schema(type: 'string', format: 'uuid')
            ),
            new OA\Parameter(
                name: 'date',
                in: 'query',
                required: true,
                description: 'Date in Y-m-d format',
                schema: new OA\Schema(type: 'string', format: 'date', example: '2025-06-15')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Available slots returned',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            properties: [
                                new OA\Property(property: 'doctor_uuid', type: 'string', format: 'uuid'),
                                new OA\Property(property: 'date', type: 'string', format: 'date'),
                                new OA\Property(
                                    property: 'slots',
                                    type: 'array',
                                    items: new OA\Items(
                                        properties: [
                                            new OA\Property(property: 'start', type: 'integer', description: 'Unix timestamp'),
                                            new OA\Property(property: 'end', type: 'integer', description: 'Unix timestamp'),
                                            new OA\Property(property: 'available', type: 'boolean'),
                                        ],
                                        type: 'object'
                                    )
                                ),
                            ],
                            type: 'object'
                        ),
                        new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
                    ]
                )
            ),
            new OA\Response(
                response: 404,
                description: 'Doctor not found',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: false),
                        new OA\Property(
                            property: 'errors',
                            type: 'array',
                            items: new OA\Items(
                                properties: [
                                    new OA\Property(property: 'code', type: 'string'),
                                    new OA\Property(property: 'message', type: 'string'),
                                ],
                                type: 'object'
                            )
                        ),
                    ]
                )
            ),
            new OA\Response(response: 422, description: 'Invalid date format'),
        ]
    )]
    #[Route('/api/v1/appointment-slots', methods: ['GET'])]
    public function slots(Request $request): JsonResponse
    {
        $doctorUuid = trim($request->query->get('doctor_uuid', ''));
        $date       = trim($request->query->get('date', ''));

        $doctor = $this->doctorRepo->findByUuid($doctorUuid);
        if ($doctor === null) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
        }

        if (empty($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'فرمت تاریخ نادرست است (Y-m-d)', 422, 'date');
        }

        $sessions = $this->slotCalculator->getAllSlotsWithAvailability($doctor, $date);

        return $this->success([
            'doctor_uuid' => $doctorUuid,
            'date'        => $date,
            'sessions'    => $sessions,
        ]);
    }

    #[Route('/api/v1/appointment-settings/month-availability/{doctorUuid}', methods: ['GET'])]
    public function monthAvailability(string $doctorUuid, Request $request): JsonResponse
    {
        $doctor = $this->doctorRepo->findByUuid($doctorUuid);
        if ($doctor === null) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
        }

        $year  = (int) $request->query->get('year');
        $month = (int) $request->query->get('month');
        if ($year < 1970 || $month < 1 || $month > 12) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'سال یا ماه نامعتبر است', 422, 'month');
        }

        $daysInMonth = (int) date('t', (int) strtotime(sprintf('%04d-%02d-01', $year, $month)));

        $disabled = [];
        $enabled  = [];
        for ($day = 1; $day <= $daysInMonth; $day++) {
            $date = sprintf('%04d-%02d-%02d', $year, $month, $day);
            if ($this->slotCalculator->hasAnyAvailability($doctor, $date)) {
                $enabled[] = $date;
            } else {
                $disabled[] = $date;
            }
        }

        $schedule = $this->scheduleRepo->findByDoctor($doctor);
        $meta     = $schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META;

        return $this->success([
            'year'                   => $year,
            'month'                  => $month,
            'disabled_dates'         => $disabled,
            'enabled_dates'          => $enabled,
            'online_booking_enabled' => (bool) $meta['online_booking_enabled'],
            'booking_window'         => [
                'value' => (int) $meta['booking_window_value'],
                'unit'  => $meta['booking_window_unit'],
            ],
        ]);
    }

    // ── Authenticated: book / manage ─────────────────────────────────────────

    #[OA\Post(
        path: '/api/v1/appointment',
        summary: 'Book a new appointment',
        security: [['bearerAuth' => []]],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                required: ['doctor_uuid', 'slot_start', 'slot_end'],
                properties: [
                    new OA\Property(property: 'doctor_uuid', type: 'string', format: 'uuid'),
                    new OA\Property(property: 'slot_start', type: 'integer', description: 'Slot start Unix timestamp'),
                    new OA\Property(property: 'slot_end', type: 'integer', description: 'Slot end Unix timestamp'),
                    new OA\Property(property: 'note', type: 'string'),
                ]
            )
        ),
        responses: [
            new OA\Response(
                response: 201,
                description: 'Appointment booked successfully',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'object', description: 'Appointment object'),
                        new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthenticated'),
            new OA\Response(response: 404, description: 'Doctor not found'),
            new OA\Response(response: 409, description: 'Slot already taken'),
            new OA\Response(response: 422, description: 'Validation error'),
        ]
    )]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    #[Route('/api/v1/appointment', methods: ['POST'])]
    public function book(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $data       = json_decode($request->getContent(), true) ?? [];
        $doctorUuid = trim($data['doctor_uuid'] ?? '');
        $slotStart  = (int) ($data['slot_start'] ?? 0);
        $slotEnd    = (int) ($data['slot_end'] ?? 0);

        if (empty($doctorUuid) || $slotStart <= 0 || $slotEnd <= $slotStart) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'doctor_uuid، slot_start و slot_end الزامی است', 422);
        }

        if ($slotStart < time()) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'زمان این اسلات گذشته است', 422);
        }

        $doctor = $this->doctorRepo->findByUuid($doctorUuid);
        if ($doctor === null) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
        }

        $forSelf = (bool) ($data['for_self'] ?? true);

        // کد ملی و جنسیت بیمار همیشه الزامی است (چه برای خود، چه برای دیگری).
        $nationalCode = InputValidator::toEnglishDigits(trim((string) ($data['patient_national_code'] ?? '')));
        // فرانت‌اند male/female می‌فرستد؛ به فرمِ متعارفِ بک‌اند (man/woman) نگاشت می‌شود.
        $gender = match (strtolower(trim((string) ($data['patient_gender'] ?? '')))) {
            'man', 'male'     => 'man',
            'woman', 'female' => 'woman',
            default           => '',
        };

        if ($nationalCode === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی بیمار الزامی است', 422, 'patient_national_code');
        }
        if (!InputValidator::isValidIranNationalCode($nationalCode)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی نامعتبر است', 422, 'patient_national_code');
        }
        if ($gender === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'جنسیت بیمار الزامی است', 422, 'patient_gender');
        }

        $appointment = new Appointment($doctor, $user, $slotStart, $slotEnd);
        $appointment->setPatientNationalCode($nationalCode);
        $appointment->setPatientGender($gender);
        if (isset($data['note'])) $appointment->setNote($data['note']);

        // نماینده‌ی دامنه‌ی مبدأ رزرو (از Origin مرورگر)؛ گاردِ نهایی پورسانت در لحظه‌ی
        // پرداخت دوباره از payment.frontend_address محاسبه می‌شود — این فقط ثبتِ لحظه‌ی رزرو است.
        $bookingCtx = $this->domainResolver->resolve($request->headers->get('origin'));
        if ($bookingCtx->representationId() !== null) {
            $appointment->setBookingRepresentationId($bookingCtx->representationId());
        }

        // آدرس نوبت از روی session متناظر در برنامه‌ی هفتگی تعیین می‌شود (location_id).
        $locationId = $this->resolveSlotLocationId($doctor, $slotStart);
        if ($locationId !== null) {
            $appointment->setAddressId($locationId);
        }

        if ($forSelf) {
            $appointment->setPatientName($user->getRealName());
            $appointment->setPatientMobile($user->getMobileNumber());
        } else {
            $patientName   = trim($data['patient_name'] ?? '');
            $patientMobile = trim($data['patient_mobile'] ?? '');
            if ($patientName === '' || $patientMobile === '') {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نام و شماره موبایل بیمار الزامی است', 422);
            }
            $appointment->setPatientName($patientName);
            $appointment->setPatientMobile($patientMobile);
            $appointment->setPatientReason($data['patient_reason'] ?? null);
        }

        $appointment->markPendingWithTtl(Appointment::PAYMENT_TTL);

        try {
            $this->appointmentRepo->bookAtomically($appointment);
        } catch (SlotTakenException) {
            return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این نوبت قبلاً رزرو شده است', 409);
        }

        return $this->success(['data' => $appointment->toArray()], 201);
    }

    #[OA\Get(
        path: '/api/v1/appointment/{uuid}',
        summary: 'Get a single appointment by UUID',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(
                name: 'uuid',
                in: 'path',
                required: true,
                schema: new OA\Schema(type: 'string', format: 'uuid')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Appointment returned',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'object', description: 'Appointment object'),
                        new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthenticated'),
            new OA\Response(response: 403, description: 'Access denied'),
            new OA\Response(response: 404, description: 'Appointment not found'),
        ]
    )]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    #[Route('/api/v1/appointment/{uuid}', methods: ['GET'])]
    public function get(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        $appointment = $this->appointmentRepo->findByUuid($uuid);
        if ($appointment === null) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
        }

        if (!$this->canView($appointment, $user)) {
            return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
        }

        return $this->success(['data' => $appointment->toArray()]);
    }

    #[OA\Get(
        path: '/api/v1/appointments/doctor/{doctorUuid}',
        summary: 'List appointments for a specific doctor',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(
                name: 'doctorUuid',
                in: 'path',
                required: true,
                schema: new OA\Schema(type: 'string', format: 'uuid')
            ),
            new OA\Parameter(
                name: 'status',
                in: 'query',
                required: false,
                description: 'Filter by appointment status',
                schema: new OA\Schema(type: 'string', example: 'pending')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Appointment list returned',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            type: 'array',
                            items: new OA\Items(type: 'object', description: 'Appointment object')
                        ),
                        new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthenticated'),
            new OA\Response(response: 403, description: 'Access denied'),
            new OA\Response(response: 404, description: 'Doctor not found'),
        ]
    )]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    #[Route('/api/v1/appointments/doctor/{doctorUuid}', methods: ['GET'])]
    public function listByDoctor(string $doctorUuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $doctor = $this->doctorRepo->findByUuid($doctorUuid);
        if ($doctor === null) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
        }

        if ($doctor->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) {
            return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
        }

        $status       = $request->query->get('status');
        $appointments = $this->appointmentRepo->findByDoctor($doctor, $status);

        return $this->success(['data' => array_map(fn(Appointment $a) => $a->toArray(), $appointments)]);
    }

    #[OA\Get(
        path: '/api/v1/appointments/user',
        summary: 'List appointments for the authenticated user',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(
                name: 'status',
                in: 'query',
                required: false,
                description: 'Filter by appointment status',
                schema: new OA\Schema(type: 'string', example: 'pending')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Appointment list returned',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            type: 'array',
                            items: new OA\Items(type: 'object', description: 'Appointment object')
                        ),
                        new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthenticated'),
        ]
    )]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    #[Route('/api/v1/appointments/user', methods: ['GET'])]
    public function listByUser(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $status       = $request->query->get('status');
        $appointments = $this->appointmentRepo->findByUser($user, $status);

        return $this->success(['data' => array_map(fn(Appointment $a) => $a->toArray(), $appointments)]);
    }

    private function canView(Appointment $a, User $user): bool
    {
        return $a->getUser()->getId() === $user->getId()
            || $a->getDoctor()->getUser()->getId() === $user->getId()
            || $user->hasRole('ROLE_ADMIN');
    }

    private function canManage(Appointment $a, User $user): bool
    {
        return $a->getUser()->getId() === $user->getId()
            || $a->getDoctor()->getUser()->getId() === $user->getId()
            || $user->hasRole('ROLE_ADMIN');
    }

    private function resolveSlotLocationId(Doctor $doctor, int $slotStart): ?int
    {
        return $this->slotCalculator->resolveSlotLocationId($doctor, $slotStart);
    }

    #[OA\Patch(
        path: '/api/v1/appointment/{uuid}/status',
        summary: 'Update the status of an appointment',
        security: [['bearerAuth' => []]],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                required: ['status'],
                properties: [
                    new OA\Property(property: 'status', type: 'string', example: 'confirmed'),
                    new OA\Property(property: 'version', type: 'integer', description: 'Optimistic lock version'),
                ]
            )
        ),
        parameters: [
            new OA\Parameter(
                name: 'uuid',
                in: 'path',
                required: true,
                schema: new OA\Schema(type: 'string', format: 'uuid')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Appointment status updated',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'object', description: 'Updated appointment object'),
                        new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthenticated'),
            new OA\Response(response: 403, description: 'Access denied'),
            new OA\Response(response: 404, description: 'Appointment not found'),
            new OA\Response(response: 409, description: 'Optimistic lock conflict'),
            new OA\Response(response: 422, description: 'Invalid status transition'),
        ]
    )]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    #[Route('/api/v1/appointment/{uuid}/status', methods: ['PATCH'])]
    public function updateStatus(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $appointment = $this->appointmentRepo->findByUuid($uuid);
        if ($appointment === null) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نوبت یافت نشد', 404);
        }

        if (!$this->canManage($appointment, $user)) {
            return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
        }

        $data      = json_decode($request->getContent(), true) ?? [];
        $newStatus = trim($data['status'] ?? '');
        $version   = (int) ($data['version'] ?? $appointment->getVersion());

        if (!$appointment->canTransitionTo($newStatus)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, sprintf(
                'انتقال از "%s" به "%s" مجاز نیست', $appointment->getStatus(), $newStatus
            ), 422);
        }

        $appointment->transitionTo($newStatus);

        try {
            $this->appointmentRepo->saveWithLock($appointment, $version);
        } catch (OptimisticLockException) {
            return $this->error(ErrorCodes::ERR_CONFLICT_001, 'تداخل ویرایش همزمان. لطفاً دوباره تلاش کنید', 409);
        }

        if ($newStatus === Appointment::STATUS_CONFIRMED) {
            $this->patientService->autoCreateOnAppointmentConfirm($appointment);
        }

        return $this->success(['data' => $appointment->toArray()]);
    }
}
