<?php

namespace App\Billing\Repository;

use App\Billing\Entity\Invoice;
use App\Billing\Service\InvoicePaymentStatus;
use App\Patient\Entity\PatientRecord;
use App\Patient\Entity\SessionPayment;
use App\UserProfile\Entity\UserProfile;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

class InvoiceRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Invoice::class);
    }

    public function findByUuid(string $uuid): ?Invoice
    {
        return $this->findOneBy(['uuid' => $uuid]);
    }

    public function findBySession(int $patientSessionId): ?Invoice
    {
        return $this->findOneBy(['patientSessionId' => $patientSessionId]);
    }

    /**
     * صورتحساب‌های نهایی‌شده‌ای که بیمه‌ای روی خودشان دارند ولی هیچ مطالبه‌ای برایشان
     * ثبت نشده — بازمانده‌های دورانی که مطالبه فقط در یک مسیر ساخته می‌شد.
     *
     * @return Invoice[]
     */
    public function findFinalizedInsuredWithoutClaims(): array
    {
        return $this->createQueryBuilder('i')
            ->where('i.status = :finalized')
            ->andWhere('i.baseInsuranceId IS NOT NULL OR i.supplementaryInsuranceId IS NOT NULL')
            ->andWhere('NOT EXISTS (
                SELECT 1 FROM App\Billing\Entity\ClaimItem ci
                JOIN App\Billing\Entity\InvoiceItem ii WITH ii.id = ci.invoiceItemId
                WHERE IDENTITY(ii.invoice) = i.id
            )')
            ->setParameter('finalized', Invoice::STATUS_FINALIZED)
            ->orderBy('i.id', 'ASC')
            ->getQuery()
            ->getResult();
    }

    /**
     * A flat, newest-first page of a tenant's recorded (finalized/paid) invoices,
     * one row per invoice with the patient's name and national code joined in.
     *
     * @param array{national_code?:?string,status?:?string,from?:?int,to?:?int} $filters
     *        status: paid | unsettled (maps to invoice paid / finalized).
     * @return list<array{invoice_uuid:string,patient_uuid:string,patient_name:?string,national_code:?string,issued_at:int,amount_rials:int,status:string}>
     */
    public function tenantInvoices(string $entityType, int $entityId, array $filters, int $page, int $limit): array
    {
        $rows = $this->tenantInvoicesQuery($entityType, $entityId, $filters)
            ->select(
                'i.uuid AS invoice_uuid', 'r.uuid AS patient_uuid', 'u.realName AS patient_name',
                'COALESCE(p.nationalCode, u.nationalCode) AS national_code', 'i.issuedAt AS issued_at',
                'i.patientRials AS amount_rials',
                sprintf('%s AS paid_rials', self::paidSumDql('sp_row')),
            )
            ->orderBy('i.issuedAt', 'DESC')
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit)
            ->getQuery()
            ->getArrayResult();

        return array_map(static fn(array $r): array => [
            'invoice_uuid'  => $r['invoice_uuid'],
            'patient_uuid'  => $r['patient_uuid'],
            'patient_name'  => $r['patient_name'],
            'national_code' => $r['national_code'],
            'issued_at'     => (int) $r['issued_at'],
            'amount_rials'  => (int) $r['amount_rials'],
            'paid_rials'    => (int) $r['paid_rials'],
            'status'        => InvoicePaymentStatus::resolve((int) $r['amount_rials'], (int) $r['paid_rials']),
        ], $rows);
    }

    /**
     * زیرپرس‌وجوی مجموع پرداخت‌های مراجعه‌ی همان صورتحساب. صورتحساب بدون مراجعه
     * هیچ پرداختی ندارد، پس `COALESCE` صفر می‌دهد و «تسویه‌نشده» می‌ماند.
     *
     * @param string $alias نام یکتا؛ استفاده‌ی دوبار از یک alias در یک query خطای semantical می‌دهد.
     */
    private static function paidSumDql(string $alias): string
    {
        return sprintf(
            '(SELECT COALESCE(SUM(%1$s.amountRials), 0) FROM %2$s %1$s WHERE IDENTITY(%1$s.session) = i.patientSessionId)',
            $alias,
            SessionPayment::class,
        );
    }

    /**
     * پرداخت‌های ثبت‌شده‌ی هر صورتحساب (از راه مراجعه‌اش)، در یک کوئری برای کل صفحه.
     * مبلغ وصول‌شده هم از همین ردیف‌ها جمع می‌شود تا کوئری دوم لازم نباشد.
     *
     * @param list<Invoice> $invoices
     * @return array<int, list<array{method:string,amount_rials:int,paid_at:int,created_by_name:?string}>>
     *         کلید = شناسه‌ی صورتحساب؛ قدیمی‌ترین پرداخت اول.
     */
    public function paymentsForInvoices(array $invoices): array
    {
        $sessionIds = [];
        foreach ($invoices as $invoice) {
            $sessionId = $invoice->getPatientSessionId();
            if ($sessionId !== null) {
                $sessionIds[$invoice->getId()] = $sessionId;
            }
        }
        if ($sessionIds === []) {
            return [];
        }

        $rows = $this->getEntityManager()->createQueryBuilder()
            ->select(
                'IDENTITY(sp.session) AS session_id', 'sp.method AS method',
                'sp.amountRials AS amount_rials', 'sp.paidAt AS paid_at',
                'sp.createdByName AS created_by_name',
            )
            ->from(SessionPayment::class, 'sp')
            ->where('IDENTITY(sp.session) IN (:sessions)')
            ->setParameter('sessions', array_values($sessionIds))
            ->orderBy('sp.paidAt', 'ASC')
            ->getQuery()
            ->getArrayResult();

        $bySession = [];
        foreach ($rows as $row) {
            $bySession[(int) $row['session_id']][] = [
                'method'          => $row['method'],
                'amount_rials'    => (int) $row['amount_rials'],
                'paid_at'         => (int) $row['paid_at'],
                'created_by_name' => $row['created_by_name'],
            ];
        }

        $byInvoice = [];
        foreach ($sessionIds as $invoiceId => $sessionId) {
            $byInvoice[$invoiceId] = $bySession[$sessionId] ?? [];
        }

        return $byInvoice;
    }

    public function countTenantInvoices(string $entityType, int $entityId, array $filters): int
    {
        return (int) $this->tenantInvoicesQuery($entityType, $entityId, $filters)
            ->select('COUNT(i.id)')
            ->getQuery()
            ->getSingleScalarResult();
    }

    /**
     * Aggregate totals over the same filtered set as {@see tenantInvoices}, so the
     * summary cards always agree with the table below them.
     *
     * @param array{national_code?:?string,status?:?string,from?:?int,to?:?int} $filters
     * @return array{total_rials:int,paid_rials:int,unsettled_rials:int,invoices_count:int}
     */
    public function tenantInvoiceSummary(string $entityType, int $entityId, array $filters): array
    {
        return $this->summarize($this->tenantInvoicesQuery($entityType, $entityId, $filters), 'patientRials');
    }

    /**
     * Aggregate totals over one patient's invoices — the same set {@see invoicesForPatient}
     * pages through, so the detail page's cards match its table.
     *
     * @return array{total_rials:int,paid_rials:int,unsettled_rials:int,invoices_count:int}
     */
    public function patientInvoiceSummary(string $entityType, int $entityId, int $recordId): array
    {
        return $this->summarize($this->patientInvoicesQuery($entityType, $entityId, $recordId), 'totalRials');
    }

    /**
     * Sum `$field` over a prepared invoice query, split by paid vs. still unsettled.
     *
     * @return array{total_rials:int,paid_rials:int,unsettled_rials:int,invoices_count:int}
     */
    private function summarize(QueryBuilder $qb, string $field): array
    {
        $row = (clone $qb)
            ->select(
                sprintf('COALESCE(SUM(i.%s), 0) AS total_rials', $field),
                'COUNT(i.id) AS invoices_count',
            )
            ->getQuery()
            ->getSingleResult();

        // «پرداخت‌شده» = مجموع صورتحساب‌هایی که سهم بیمارشان کامل وصول شده
        $paid = (int) (clone $qb)
            ->select(sprintf('COALESCE(SUM(i.%s), 0)', $field))
            ->andWhere(sprintf('%s >= i.patientRials', self::paidSumDql('sp_sum')))
            ->getQuery()
            ->getSingleScalarResult();

        $total = (int) $row['total_rials'];

        return [
            'total_rials'     => $total,
            'paid_rials'      => $paid,
            'unsettled_rials' => $total - $paid,
            'invoices_count'  => (int) $row['invoices_count'],
        ];
    }

    private function tenantInvoicesQuery(string $entityType, int $entityId, array $filters): QueryBuilder
    {
        $qb = $this->createQueryBuilder('i')
            ->innerJoin(PatientRecord::class, 'r', Join::WITH, 'r.id = i.patientRecordId')
            ->innerJoin('r.user', 'u')
            // کد ملی روی پروفایل ذخیره می‌شود (profiles.national_code)؛ users.national_code
            // برای بیمارانِ ساخته‌شده در نوبت‌دهی خالی می‌ماند.
            ->leftJoin(UserProfile::class, 'p', Join::WITH, 'p.user = u')
            ->where('i.entityType = :type')
            ->andWhere('i.entityId = :id')
            ->andWhere('i.status IN (:active)')
            ->setParameter('type', $entityType)
            ->setParameter('id', $entityId)
            ->setParameter('active', [Invoice::STATUS_FINALIZED, Invoice::STATUS_PAID]);

        if (!empty($filters['national_code'])) {
            $qb->andWhere('(p.nationalCode LIKE :nc OR u.nationalCode LIKE :nc)')
                ->setParameter('nc', '%' . $filters['national_code'] . '%');
        }
        if (!empty($filters['from'])) {
            $qb->andWhere('i.issuedAt >= :from')->setParameter('from', (int) $filters['from']);
        }
        if (!empty($filters['to'])) {
            $qb->andWhere('i.issuedAt <= :to')->setParameter('to', (int) $filters['to']);
        }
        // وضعیت پرداخت مشتق است (پرداخت‌های مراجعه در برابر سهم بیمار)، نه ستون status
        match ($filters['status'] ?? null) {
            InvoicePaymentStatus::PAID      => $qb->andWhere(sprintf('%s >= i.patientRials', self::paidSumDql('sp_f1'))),
            InvoicePaymentStatus::PARTIAL   => $qb->andWhere(sprintf(
                '%s > 0 AND %s < i.patientRials',
                self::paidSumDql('sp_f1'),
                self::paidSumDql('sp_f2'),
            )),
            InvoicePaymentStatus::UNSETTLED => $qb->andWhere(sprintf('%s <= 0 AND i.patientRials > 0', self::paidSumDql('sp_f1'))),
            default                         => null,
        };

        return $qb;
    }

    /**
     * A patient's recorded (finalized/paid) invoices for a tenant, newest first.
     * @return list<Invoice>
     */
    public function invoicesForPatient(string $entityType, int $entityId, int $recordId, int $page, int $limit): array
    {
        return $this->patientInvoicesQuery($entityType, $entityId, $recordId)
            ->orderBy('i.issuedAt', 'DESC')
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

    private function patientInvoicesQuery(string $entityType, int $entityId, int $recordId): QueryBuilder
    {
        return $this->createQueryBuilder('i')
            ->where('i.entityType = :type')
            ->andWhere('i.entityId = :id')
            ->andWhere('i.patientRecordId = :record')
            ->andWhere('i.status IN (:active)')
            ->setParameter('type', $entityType)
            ->setParameter('id', $entityId)
            ->setParameter('record', $recordId)
            ->setParameter('active', [Invoice::STATUS_FINALIZED, Invoice::STATUS_PAID]);
    }

    public function save(Invoice $entity, bool $flush = true): void
    {
        $this->getEntityManager()->persist($entity);
        if ($flush) {
            $this->getEntityManager()->flush();
        }
    }
}
