<?php

namespace App\Billing\Controller;

use App\Auth\Entity\User;
use App\Billing\Entity\Claim;
use App\Billing\Repository\ClaimRepository;
use App\Billing\Repository\ClaimStatusLogRepository;
use App\Billing\Repository\InvoiceItemRepository;
use App\Billing\Repository\InvoiceRepository;
use App\Billing\Service\ClaimService;
use App\Billing\Service\InvoiceService;
use App\Insurance\Repository\InsuranceRepository;
use App\Patient\Repository\PatientRecordRepository;
use App\Patient\Repository\PatientSessionRepository;
use App\Patient\Security\PatientRecordScopeResolver;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
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: 'Billing')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class BillingController extends BaseController
{
    public function __construct(
        private readonly InvoiceService            $invoiceService,
        private readonly InvoiceRepository          $invoiceRepo,
        private readonly InvoiceItemRepository      $invoiceItemRepo,
        private readonly ClaimService               $claimService,
        private readonly ClaimRepository            $claimRepo,
        private readonly PatientSessionRepository   $sessionRepo,
        private readonly PatientRecordRepository    $recordRepo,
        private readonly InsuranceRepository        $insuranceRepo,
        private readonly ClaimStatusLogRepository  $statusLogRepo,
        private readonly PatientRecordScopeResolver $scopeResolver,
        private readonly \App\UserProfile\Repository\UserProfileRepository $profileRepo,
    ) {}

    /**
     * مطالبات را با نام بیمه و جزئیات هر آیتم (نوع/نام، تاریخ مراجعه، تعداد) غنی می‌کند.
     * @param Claim[] $claims
     */
    private function enrichClaims(array $claims): array
    {
        // جمع‌آوری همه‌ی invoice_item_idها در یک رفت‌وآمد
        $invoiceItemIds = [];
        foreach ($claims as $claim) {
            foreach ($claim->getItems() as $ci) {
                $invoiceItemIds[] = $ci->getInvoiceItemId();
            }
        }
        $itemDetails = $this->invoiceItemRepo->detailsForIds(array_values(array_unique($invoiceItemIds)));

        $sessionIds = [];
        foreach ($itemDetails as $d) {
            if ($d['patient_session_id'] !== null) {
                $sessionIds[] = $d['patient_session_id'];
            }
        }
        $uniqueSessionIds = array_values(array_unique($sessionIds));
        $sessionDates = $this->sessionRepo->datesForIds($uniqueSessionIds);
        $patientInfo  = $this->sessionRepo->patientInfoForIds($uniqueSessionIds);

        // Batch-fetch insurance names instead of one find() per claim (N+1).
        $insuranceIds   = array_values(array_unique(array_map(fn(Claim $c) => $c->getInsuranceId(), $claims)));
        $insuranceNames = [];
        if ($insuranceIds !== []) {
            foreach ($this->insuranceRepo->findBy(['id' => $insuranceIds]) as $ins) {
                $insuranceNames[$ins->getId()] = $ins->getName();
            }
        }

        return array_map(function (Claim $claim) use ($itemDetails, $sessionDates, $patientInfo, $insuranceNames) {
            $data = $claim->toArray();
            $data['insurance_name'] = $insuranceNames[$claim->getInsuranceId()] ?? null;

            $patientName = null;
            $patientMobile = null;

            $data['items'] = array_map(function (array $item) use ($itemDetails, $sessionDates, $patientInfo, &$patientName, &$patientMobile) {
                $detail = $itemDetails[$item['invoice_item_id']] ?? null;
                $sessionId = $detail['patient_session_id'] ?? null;
                $item['title']      = $detail['title'] ?? null;
                $item['is_visit']   = $detail !== null && $detail['service_item_id'] === null;
                $item['quantity']   = $detail['quantity'] ?? 1;
                $item['total_rials'] = $detail['total_rials'] ?? null;
                $item['visit_date'] = $sessionId !== null ? ($sessionDates[$sessionId] ?? null) : null;

                if ($patientName === null && $sessionId !== null && isset($patientInfo[$sessionId])) {
                    $patientName   = $patientInfo[$sessionId]['name'];
                    $patientMobile = $patientInfo[$sessionId]['mobile'];
                }
                return $item;
            }, $data['items']);

            $data['patient_name']   = $patientName;
            $data['patient_mobile'] = $patientMobile;

            return $data;
        }, $claims);
    }

    #[Route('/api/v1/billing/invoices', methods: ['POST'])]
    public function create(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        $data        = json_decode($request->getContent(), true) ?? [];
        $sessionUuid = trim($data['session_uuid'] ?? '');
        if ($sessionUuid === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'session_uuid الزامی است', 422);
        }

        $session = $this->sessionRepo->findByUuid($sessionUuid);
        if ($session === null || !$this->ownsSession($session, $entityType, $entityId)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مراجعه یافت نشد', 404);
        }

        $invoice = $this->invoiceService->createFromSession($session, $entityType, $entityId);

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

    #[Route('/api/v1/billing/invoices/{uuid}', methods: ['GET'])]
    public function show(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $invoice = $this->invoiceRepo->findByUuid($uuid);
        if ($invoice === null || $invoice->getEntityType() !== $entityType || $invoice->getEntityId() !== $entityId) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'صورتحساب یافت نشد', 404);
        }

        return $this->success(['data' => $this->invoiceService->detailWithSession($invoice)]);
    }

    #[Route('/api/v1/billing/invoices/{uuid}/finalize', methods: ['POST'])]
    public function finalize(string $uuid, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $invoice = $this->invoiceRepo->findByUuid($uuid);
        if ($invoice === null || $invoice->getEntityType() !== $entityType || $invoice->getEntityId() !== $entityId) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'صورتحساب یافت نشد', 404);
        }

        $this->invoiceService->finalize($invoice);

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

    /**
     * لیست پرداخت‌ها — یک ردیف به‌ازای هر صورتحساب ثبت‌شده‌ی tenant (flat).
     * فیلترها: national_code، status (paid|unsettled)، from/to (unix ثانیه).
     * پاسخ صفحه‌بندی: هر ردیف { invoice_uuid, patient_uuid, patient_name,
     * national_code, issued_at, amount_rials, status }.
     */
    #[Route('/api/v1/my/billing/payments', methods: ['GET'])]
    public function listPayments(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        $page  = max(1, (int) $request->query->get('page', 1));
        $limit = min(100, max(1, (int) $request->query->get('limit', 20)));

        $result = $this->invoiceService->tenantInvoiceList(
            $entityType,
            $entityId,
            $this->paymentFilters($request),
            $page,
            $limit,
        );

        return $this->paginated($result['items'], $result['total'], $page, $limit);
    }

    /**
     * خلاصه‌ی مالی همان مجموعه‌ی فیلترشده‌ی listPayments — برای کارت‌های آمار.
     * پاسخ: { total_rials, paid_rials, unsettled_rials, invoices_count }.
     */
    #[Route('/api/v1/my/billing/payments/summary', methods: ['GET'])]
    public function paymentsSummary(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        return $this->success(
            $this->invoiceService->tenantInvoiceSummary($entityType, $entityId, $this->paymentFilters($request)),
        );
    }

    /**
     * فیلترهای مشترک لیست پرداخت‌ها و خلاصه‌ی آن؛ یک منبع تا دو نما واگرا نشوند.
     *
     * @return array{national_code:?string,status:?string,from:?string,to:?string}
     */
    private function paymentFilters(Request $request): array
    {
        return [
            'national_code' => $request->query->get('national_code') ?: null,
            'status'        => $request->query->get('status') ?: null,
            'from'          => $request->query->get('from') ?: null,
            'to'            => $request->query->get('to') ?: null,
        ];
    }

    /**
     * پرداخت‌های ثبت‌شده‌ی یک بیمار — سربرگ بیمار + فهرست صفحه‌بندی‌شده‌ی صورتحساب‌ها.
     * فقط مالک رکورد (همان tenant) اجازه دارد؛ در غیر این صورت ۴۰۴.
     * پاسخ: { patient:{uuid,name,national_code}, data:[صورتحساب‌ها], summary:{...}, meta:{...} }.
     */
    #[Route('/api/v1/my/billing/patients/{patientUuid}/invoices', methods: ['GET'])]
    public function listPatientInvoices(string $patientUuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        $record = $this->recordRepo->findByUuid($patientUuid);
        if ($record === null || $record->getEntityType() !== $entityType || $record->getEntityId() !== $entityId) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'بیمار یافت نشد', 404);
        }

        $page  = max(1, (int) $request->query->get('page', 1));
        $limit = min(100, max(1, (int) $request->query->get('limit', 20)));

        $result  = $this->invoiceService->patientInvoiceList($entityType, $entityId, $record->getId(), $page, $limit);
        $patient = $record->getUser();
        // کد ملی روی پروفایل ذخیره می‌شود؛ users.national_code برای بیمارِ ساخته‌شده در نوبت‌دهی خالی است.
        $nationalCode = $this->profileRepo->findByUser($patient)?->getNationalCode() ?? $patient->getNationalCode();

        return $this->success([
            'patient' => [
                'uuid'          => $record->getUuid(),
                'name'          => $patient->getRealName(),
                'national_code' => $nationalCode,
            ],
            'data'    => $result['items'],
            'summary' => $result['summary'],
            'meta' => [
                'totalRecords' => $result['total'],
                'totalPages'   => (int) ceil($result['total'] / $limit),
                'currentPage'  => $page,
            ],
        ]);
    }

    // ── Claims (مطالبات بیمه) ──────────────────────────────────────────────────

    #[Route('/api/v1/billing/claims', methods: ['POST'])]
    public function createClaim(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        $data        = json_decode($request->getContent(), true) ?? [];
        $invoiceUuid = trim($data['invoice_uuid'] ?? '');
        if ($invoiceUuid === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'invoice_uuid الزامی است', 422);
        }

        $invoice = $this->invoiceRepo->findByUuid($invoiceUuid);
        if ($invoice === null || $invoice->getEntityType() !== $entityType || $invoice->getEntityId() !== $entityId) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'صورتحساب یافت نشد', 404);
        }

        $claims = $this->claimService->createFromInvoice($invoice);

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

    #[Route('/api/v1/billing/claims', methods: ['GET'])]
    public function listClaims(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        $filters = [
            'status'       => $request->query->get('status') ?: null,
            'insurance_id' => $request->query->get('insurance_id') ?: null,
            'from'         => $request->query->get('from') ?: null,
            'to'           => $request->query->get('to') ?: null,
            'q'            => $request->query->get('q') ?: null,
        ];

        $page  = max(1, (int) $request->query->get('page', 1));
        $limit = min(100, max(1, (int) $request->query->get('limit', 50)));

        $claims = $this->claimRepo->findByTenant($entityType, $entityId, $filters, $page, $limit);
        $total  = $this->claimRepo->countByTenant($entityType, $entityId, $filters);

        // meta added inside the existing { data: { data: [...] } } envelope so
        // existing clients keep reading data.data unchanged.
        return $this->success([
            'data' => $this->enrichClaims($claims),
            'meta' => [
                'totalRecords' => $total,
                'totalPages'   => (int) ceil($total / $limit),
                'currentPage'  => $page,
            ],
        ]);
    }

    /**
     * نمای سطح‌اول داشبورد مطالبات: یک ردیف به‌ازای هر بیمار با جمع‌های تجمیعی.
     * فیلترها همان فیلترهای مطالبه‌اند و روی count هم اعمال می‌شوند.
     */
    #[Route('/api/v1/billing/claims/by-patient', methods: ['GET'])]
    public function claimsByPatient(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        $filters = $this->claimFilters($request);
        if ($filters['kind'] !== null && !in_array($filters['kind'], [Claim::KIND_BASE, Claim::KIND_SUPPLEMENTARY], true)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نوع بیمه نامعتبر است', 422, 'kind');
        }

        $page    = max(1, (int) $request->query->get('page', 1));
        $limit   = min(100, max(1, (int) $request->query->get('limit', 20)));
        $sort    = (string) $request->query->get('sort', 'last_activity_at');
        $dir     = (string) $request->query->get('dir', 'desc');

        $rows  = $this->claimRepo->aggregateByPatient($entityType, $entityId, $filters, $sort, $dir, $page, $limit);
        $total = $this->claimRepo->countPatientsWithClaims($entityType, $entityId, $filters);

        return $this->paginated($rows, $total, $page, $limit);
    }

    /** جزئیات کامل مطالبات یک بیمار، به‌همراه تاریخچه‌ی تغییر وضعیت هر مطالبه. */
    #[Route('/api/v1/billing/claims/by-patient/{patientUuid}', methods: ['GET'])]
    public function claimsForPatient(string $patientUuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        $record = $this->recordRepo->findByUuid($patientUuid);
        if ($record === null || $record->getEntityType() !== $entityType || $record->getEntityId() !== $entityId) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پرونده بیمار یافت نشد', 404);
        }

        $rows      = $this->claimRepo->detailsForPatient($entityType, $entityId, (int) $record->getId(), $this->claimFilters($request));
        $timelines = $this->statusLogRepo->timelinesForClaims(array_map(static fn(array $r) => (int) $r['claim_id'], $rows));

        $insuranceNames = $this->insuranceNamesFor(array_map(static fn(array $r) => (int) $r['insurance_id'], $rows));

        $items = array_map(function (array $r) use ($timelines, $insuranceNames) {
            $claimed = (int) $r['total_claimed_rials'];
            $base    = (int) $r['service_base_rials'];

            return [
                'uuid'                 => $r['uuid'],
                'invoice_uuid'         => $r['invoice_uuid'],
                'visit_date'           => $r['visit_date'] !== null ? (int) $r['visit_date'] : null,
                'doctor_name'          => $r['doctor_name'],
                'insurance_id'         => (int) $r['insurance_id'],
                'insurance_name'       => $insuranceNames[(int) $r['insurance_id']] ?? null,
                'insurance_kind'       => $r['insurance_kind'],
                'service_base_rials'   => $base,
                'coverage_percent'     => $base > 0 ? round($claimed * 100 / $base, 2) : 0.0,
                'insurance_share_rials'=> $claimed,
                'patient_share_rials'  => (int) $r['patient_share_rials'],
                'total_approved_rials' => $r['total_approved_rials'] !== null ? (int) $r['total_approved_rials'] : null,
                'total_paid_rials'     => $r['total_paid_rials'] !== null ? (int) $r['total_paid_rials'] : null,
                'status'               => $r['status'],
                'tracking_number'      => $r['tracking_number'],
                'reject_reason'        => $r['reject_reason'],
                'submitted_at'         => $r['submitted_at'] !== null ? (int) $r['submitted_at'] : null,
                'settled_at'           => $r['settled_at'] !== null ? (int) $r['settled_at'] : null,
                'created_at'           => (int) $r['created_at'],
                'allowed_transitions'  => Claim::transitionsFrom($r['status']),
                'logs'                 => $timelines[(int) $r['claim_id']] ?? [],
            ];
        }, $rows);

        return $this->success([
            'patient' => [
                'uuid'          => $record->getUser()->getUuid(),
                'record_uuid'   => $record->getUuid(),
                'full_name'     => $record->getUser()->getRealName(),
                'mobile'        => $record->getUser()->getMobileNumber(),
                'national_code' => $record->getUser()->getNationalCode(),
            ],
            'claims' => $items,
        ]);
    }

    /** @param int[] $ids @return array<int, string> */
    private function insuranceNamesFor(array $ids): array
    {
        $unique = array_values(array_unique($ids));
        if ($unique === []) {
            return [];
        }

        $names = [];
        foreach ($this->insuranceRepo->findBy(['id' => $unique]) as $insurance) {
            $names[$insurance->getId()] = $insurance->getName();
        }

        return $names;
    }

    /** @return array<string, mixed> */
    private function claimFilters(Request $request): array
    {
        return [
            'status'         => $request->query->get('status') ?: null,
            'insurance_id'   => $request->query->get('insurance_id') ?: null,
            'kind'           => $request->query->get('kind') ?: null,
            'doctor_id'      => $request->query->get('doctor_id') ?: null,
            'payment_status' => $request->query->get('payment_status') ?: null,
            'from'           => $request->query->get('from') ?: null,
            'to'             => $request->query->get('to') ?: null,
            'search'         => $request->query->get('search') ?: null,
        ];
    }

    #[Route('/api/v1/billing/claims/{uuid}/{action}', methods: ['POST'], requirements: ['action' => 'submit|approve|reject|pay'])]
    public function transitionClaim(string $uuid, string $action, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        $claim = $this->claimRepo->findByUuid($uuid);
        if ($claim === null || $claim->getEntityType() !== $entityType || $claim->getEntityId() !== $entityId) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مطالبه یافت نشد', 404);
        }

        $data   = json_decode($request->getContent(), true) ?? [];
        $target = match ($action) {
            'submit'  => Claim::STATUS_SUBMITTED,
            'approve' => Claim::STATUS_APPROVED,
            'reject'  => Claim::STATUS_REJECTED,
            'pay'     => Claim::STATUS_PAID,
        };

        // Bound the financial figures: approved/paid cannot be negative, approved
        // cannot exceed the claimed total, and paid cannot exceed approved.
        if ($action === 'approve' && isset($data['approved_rials'])) {
            $approved = (int) $data['approved_rials'];
            if ($approved < 0 || $approved > $claim->getTotalClaimedRials()) {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'مبلغ تأییدشده باید بین ۰ و مبلغ مطالبه‌شده باشد', 422, 'approved_rials');
            }
        }
        if ($action === 'pay' && isset($data['paid_rials'])) {
            $paid    = (int) $data['paid_rials'];
            $ceiling = $claim->getTotalApprovedRials() ?? $claim->getTotalClaimedRials();
            if ($paid < 0 || $paid > $ceiling) {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'مبلغ پرداختی باید بین ۰ و مبلغ تأییدشده باشد', 422, 'paid_rials');
            }
        }

        $this->claimService->transition($claim, $target, [
            'approved_rials'  => isset($data['approved_rials']) ? (int) $data['approved_rials'] : null,
            'paid_rials'      => isset($data['paid_rials']) ? (int) $data['paid_rials'] : null,
            'reason'          => trim($data['reason'] ?? ''),
            'tracking_number' => isset($data['tracking_number']) ? trim((string) $data['tracking_number']) : null,
            'note'            => isset($data['note']) ? trim((string) $data['note']) : null,
        ], $user);

        return $this->success(['data' => $this->enrichClaims([$claim])[0]]);
    }

    #[Route('/api/v1/billing/reports/insurance-debt', methods: ['GET'])]
    public function insuranceDebt(#[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->resolveEntity($user);
        if ($entityId === null) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
        }

        $rows = array_map(function (array $row) {
            $row['insurance_name'] = $this->insuranceRepo->find($row['insurance_id'])?->getName();
            return $row;
        }, $this->claimRepo->debtReport($entityType, $entityId));

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

    private function ownsSession($session, string $entityType, int $entityId): bool
    {
        $record = $session->getRecord();
        return $record->getEntityType() === $entityType && $record->getEntityId() === $entityId;
    }

    /**
     * محیط صورتحساب همان محیط پرونده است — صورتحساب و مطالبه از دل مراجعه بیرون می‌آیند.
     * ترتیب نقش‌ها به‌تنهایی کافی نبود: مالک کلینیکی که خودش پزشک هم هست به مطب شخصی‌اش
     * نگاشت می‌شد و صورتحساب‌های کلینیک خودش را «یافت نشد» می‌گرفت.
     */
    private function resolveEntity(User $user): array
    {
        return $this->scopeResolver->resolve($user)->toLegacyTuple();
    }
}
