<?php

namespace App\Billing\Repository;

use App\Billing\Entity\Claim;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry;

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

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

    /**
     * @param array{status?:?string, insurance_id?:?int, from?:?int, to?:?int, q?:?string} $filters
     * @return Claim[]
     */
    public function findByTenant(string $entityType, int $entityId, array $filters = [], int $page = 1, int $limit = 50): array
    {
        // fetch-join items so enrichClaims()/toArray() don't lazy-load the
        // items collection per claim (N+1). fetchJoinCollection keeps the LIMIT
        // paginating by claim.
        $qb = $this->tenantQb($entityType, $entityId, $filters)
            ->addSelect('i')
            ->leftJoin('c.items', 'i')
            ->orderBy('c.id', 'DESC')
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit);

        return iterator_to_array(new Paginator($qb, fetchJoinCollection: true));
    }

    public function countByTenant(string $entityType, int $entityId, array $filters = []): int
    {
        return (int) $this->tenantQb($entityType, $entityId, $filters)
            ->select('COUNT(c.id)')
            ->getQuery()
            ->getSingleScalarResult();
    }

    private function tenantQb(string $entityType, int $entityId, array $filters): \Doctrine\ORM\QueryBuilder
    {
        $qb = $this->createQueryBuilder('c')
            ->where('c.entityType = :type')
            ->andWhere('c.entityId = :id')
            ->setParameter('type', $entityType)
            ->setParameter('id', $entityId);

        if (!empty($filters['status'])) {
            $qb->andWhere('c.status = :status')->setParameter('status', $filters['status']);
        }
        if (!empty($filters['insurance_id'])) {
            $qb->andWhere('c.insuranceId = :insId')->setParameter('insId', (int) $filters['insurance_id']);
        }
        if (!empty($filters['from'])) {
            $qb->andWhere('c.createdAt >= :from')->setParameter('from', (int) $filters['from']);
        }
        if (!empty($filters['to'])) {
            $qb->andWhere('c.createdAt <= :to')->setParameter('to', (int) $filters['to']);
        }

        // فیلتر بیمار: مطالباتی که آیتم‌شان به صورتحساب پرونده‌ی همان کاربر اشاره دارد (موبایل/کدملی/نام).
        if (!empty($filters['q'])) {
            $q = trim((string) $filters['q']);
            $qb->andWhere(
                'EXISTS (' .
                    'SELECT 1 FROM App\Billing\Entity\ClaimItem ci2 ' .
                    'JOIN App\Billing\Entity\InvoiceItem ii2 WITH ii2.id = ci2.invoiceItemId ' .
                    'JOIN App\Billing\Entity\Invoice inv2 WITH inv2 = ii2.invoice ' .
                    'JOIN App\Patient\Entity\PatientRecord pr2 WITH pr2.id = inv2.patientRecordId ' .
                    'JOIN App\Auth\Entity\User u2 WITH u2 = pr2.user ' .
                    'WHERE ci2.claim = c AND (' .
                        'u2.mobileNumber LIKE :q OR u2.nationalCode LIKE :q OR u2.realName LIKE :q' .
                    ')' .
                ')'
            )->setParameter('q', '%' . $q . '%');
        }

        return $qb;
    }

    /**
     * گزارش بدهی بیمه: جمع claimed/approved/paid بر اساس بیمه برای tenant.
     * @return array<int, array{insurance_id:int, claimed:int, approved:int, paid:int, debt:int}>
     */
    public function debtReport(string $entityType, int $entityId): array
    {
        $rows = $this->createQueryBuilder('c')
            ->select('c.insuranceId AS insurance_id')
            ->addSelect('SUM(c.totalClaimedRials) AS claimed')
            ->addSelect('SUM(COALESCE(c.totalApprovedRials, 0)) AS approved')
            ->addSelect('SUM(COALESCE(c.totalPaidRials, 0)) AS paid')
            ->where('c.entityType = :type')
            ->andWhere('c.entityId = :id')
            ->setParameter('type', $entityType)
            ->setParameter('id', $entityId)
            ->groupBy('c.insuranceId')
            ->getQuery()
            ->getArrayResult();

        return array_map(function (array $r) {
            $claimed  = (int) $r['claimed'];
            $paid     = (int) $r['paid'];
            return [
                'insurance_id' => (int) $r['insurance_id'],
                'claimed'      => $claimed,
                'approved'     => (int) $r['approved'],
                'paid'         => $paid,
                'debt'         => max(0, $claimed - $paid),
            ];
        }, $rows);
    }

    /**
     * تجمیع مطالبات بر اساس بیمار — نمای سطح‌اول داشبورد.
     *
     * مبالغ خدمات/سهم بیمار از صورتحساب‌های **یکتا** جمع می‌شوند، نه از مطالبات؛ یک
     * صورتحساب می‌تواند دو مطالبه (پایه و مکمل) داشته باشد و جمع‌زدن از سمت مطالبه
     * مبلغ خدمات را دوبار می‌شمرد.
     *
     * @param array<string, mixed> $filters
     * @return array<int, array<string, mixed>>
     */
    public function aggregateByPatient(string $entityType, int $entityId, array $filters, string $sort, string $dir, int $page, int $limit): array
    {
        [$where, $params] = $this->patientAggregateFilters($filters);

        $orderBy = match ($sort) {
            'claims_count'          => 'claims_count',
            'total_services_rials'  => 'total_services_rials',
            'total_insurance_rials' => 'total_insurance_rials',
            'last_activity_at'      => 'last_activity_at',
            default                 => 'full_name',
        };
        $direction = strtolower($dir) === 'asc' ? 'ASC' : 'DESC';

        // LIMIT/OFFSET به‌صورت مقدار درج می‌شوند: MariaDB پارامتر رشته‌ای در LIMIT نمی‌پذیرد.
        // هر دو از قبل به int تبدیل شده‌اند، پس تزریقی ممکن نیست.
        $offset = ($page - 1) * $limit;
        $sql    = $this->patientAggregateSql($where)
            . " ORDER BY {$orderBy} {$direction} LIMIT {$limit} OFFSET {$offset}";

        $params['type'] = $entityType;
        $params['id']   = $entityId;

        $rows = $this->getEntityManager()->getConnection()->executeQuery($sql, $params)->fetchAllAssociative();

        return array_map(static function (array $r): array {
            $statuses = array_filter(explode(',', (string) $r['statuses']));

            return [
                'patient_uuid'          => $r['patient_uuid'],
                'record_uuid'           => $r['record_uuid'],
                'full_name'             => $r['full_name'],
                'mobile'                => $r['mobile'],
                'national_code'         => $r['national_code'],
                'claims_count'          => (int) $r['claims_count'],
                'total_services_rials'  => (int) $r['total_services_rials'],
                'total_insurance_rials' => (int) $r['total_insurance_rials'],
                'total_patient_rials'   => (int) $r['total_patient_rials'],
                'total_approved_rials'  => (int) $r['total_approved_rials'],
                'total_paid_rials'      => (int) $r['total_paid_rials'],
                'overall_status'        => count($statuses) === 1 ? reset($statuses) : 'mixed',
                'insurances'            => self::parseInsurances((string) ($r['insurances'] ?? '')),
                'last_activity_at'      => (int) $r['last_activity_at'],
            ];
        }, $rows);
    }

    /**
     * ردیف‌های GROUP_CONCAT بیمه‌های یک بیمار → آرایهٔ ساخت‌یافته. یک بیمار می‌تواند
     * مطالبه زیر چند بیمه داشته باشد، پس ستون «بیمه» یک لیست است نه یک مقدار.
     *
     * @return list<array{insurance_id: int, insurance_name: string|null, kind: string|null}>
     */
    private static function parseInsurances(string $concatenated): array
    {
        if ($concatenated === '') {
            return [];
        }

        $rows = [];
        foreach (explode('~', $concatenated) as $chunk) {
            [$id, $name, $kind] = array_pad(explode('|', $chunk), 3, null);
            if ($id === null || $id === '') {
                continue;
            }

            $rows[] = [
                'insurance_id'   => (int) $id,
                'insurance_name' => $name === '' ? null : $name,
                'kind'           => $kind === '' ? null : $kind,
            ];
        }

        return $rows;
    }

    public function countPatientsWithClaims(string $entityType, int $entityId, array $filters): int
    {
        [$where, $params] = $this->patientAggregateFilters($filters);
        $params['type'] = $entityType;
        $params['id']   = $entityId;

        $sql = 'SELECT COUNT(*) FROM (' . $this->patientAggregateSql($where) . ') agg';

        return (int) $this->getEntityManager()->getConnection()->executeQuery($sql, $params)->fetchOne();
    }

    /**
     * ⚠️ SQL خام است، پس TenantFilter آن را نمی‌بیند. شرط محیط دستی و همیشگی است:
     * هر سه کوئریِ این فایل با `c.entity_type = :type AND c.entity_id = :id` بسته
     * می‌شوند و فراخوان جفت را اجباراً پاس می‌دهد. اگر روزی این WHERE از این متد
     * برداشته شود، داشبورد مطالبات cross-tenant می‌شود بی‌آنکه هیچ تستی بشکند.
     */
    private function patientAggregateSql(string $where): string
    {
        return <<<SQL
            WITH claim_map AS (
                SELECT c.id AS claim_id, MIN(inv.id) AS invoice_id, MIN(inv.patient_record_id) AS record_id
                FROM claims c
                JOIN claim_items ci ON ci.claim_id = c.id
                JOIN invoice_items ii ON ii.id = ci.invoice_item_id
                JOIN invoices inv ON inv.id = ii.invoice_id
                WHERE c.entity_type = :type AND c.entity_id = :id
                GROUP BY c.id
            )
            SELECT
                u.uuid  AS patient_uuid,
                pr.uuid AS record_uuid,
                u.real_name AS full_name,
                u.mobile_number AS mobile,
                u.national_code AS national_code,
                COUNT(DISTINCT c.id) AS claims_count,
                COALESCE(SUM(c.total_claimed_rials), 0) AS total_insurance_rials,
                COALESCE(SUM(c.total_approved_rials), 0) AS total_approved_rials,
                COALESCE(SUM(c.total_paid_rials), 0) AS total_paid_rials,
                COALESCE((
                    SELECT SUM(i2.total_rials) FROM invoices i2
                    WHERE i2.id IN (SELECT DISTINCT cm2.invoice_id FROM claim_map cm2 WHERE cm2.record_id = pr.id)
                ), 0) AS total_services_rials,
                COALESCE((
                    SELECT SUM(i3.patient_rials) FROM invoices i3
                    WHERE i3.id IN (SELECT DISTINCT cm3.invoice_id FROM claim_map cm3 WHERE cm3.record_id = pr.id)
                ), 0) AS total_patient_rials,
                GROUP_CONCAT(DISTINCT c.status) AS statuses,
                GROUP_CONCAT(DISTINCT CONCAT_WS('|', c.insurance_id, COALESCE(ins.name, ''), c.insurance_kind) SEPARATOR '~') AS insurances,
                MAX(c.updated_at) AS last_activity_at
            FROM claim_map cm
            JOIN claims c ON c.id = cm.claim_id
            JOIN invoices inv ON inv.id = cm.invoice_id
            JOIN patient_records pr ON pr.id = cm.record_id
            JOIN users u ON u.id = pr.user_id
            LEFT JOIN insurances ins ON ins.id = c.insurance_id
            {$where}
            GROUP BY pr.id, u.uuid, pr.uuid, u.real_name, u.mobile_number, u.national_code
            SQL;
    }

    /**
     * @param array<string, mixed> $filters
     * @return array{0: string, 1: array<string, mixed>}
     */
    private function patientAggregateFilters(array $filters): array
    {
        $conditions = [];
        $params     = [];

        if (!empty($filters['status'])) {
            $conditions[]     = 'c.status = :status';
            $params['status'] = $filters['status'];
        }
        if (!empty($filters['insurance_id'])) {
            $conditions[]    = 'c.insurance_id = :insId';
            $params['insId'] = (int) $filters['insurance_id'];
        }
        if (!empty($filters['kind'])) {
            $conditions[]   = 'c.insurance_kind = :kind';
            $params['kind'] = (string) $filters['kind'];
        }
        if (!empty($filters['from'])) {
            $conditions[]   = 'c.created_at >= :from';
            $params['from'] = (int) $filters['from'];
        }
        if (!empty($filters['to'])) {
            $conditions[] = 'c.created_at <= :to';
            $params['to'] = (int) $filters['to'];
        }
        if (!empty($filters['doctor_id'])) {
            $conditions[]  = 'EXISTS (SELECT 1 FROM patient_sessions ps '
                . 'JOIN appointments a ON a.id = ps.appointment_id '
                . 'WHERE ps.id = inv.patient_session_id AND a.doctor_id = :docId)';
            $params['docId'] = (int) $filters['doctor_id'];
        }
        if (!empty($filters['payment_status'])) {
            $conditions[] = $filters['payment_status'] === 'paid'
                ? 'c.status = \'paid\''
                : 'c.status <> \'paid\'';
        }
        if (!empty($filters['search'])) {
            // نام بیمه هم جستجو می‌شود: کاربر «آسیا» را می‌نویسد و انتظار دارد بیماران
            // همان بیمه بیایند، نه فقط بیماری که اسمش آسیاست.
            $conditions[]     = '(u.real_name LIKE :search OR u.mobile_number LIKE :search'
                . ' OR u.national_code LIKE :search OR ins.name LIKE :search)';
            $params['search'] = '%' . trim((string) $filters['search']) . '%';
        }

        return [$conditions === [] ? '' : 'WHERE ' . implode(' AND ', $conditions), $params];
    }

    /**
     * مطالبات یک بیمار با جزئیات نمایشی (پزشک، سرویس، تاریخ مراجعه، سهم‌ها).
     *
     * @return array<int, array<string, mixed>>
     */
    public function detailsForPatient(string $entityType, int $entityId, int $recordId, array $filters): array
    {
        [$where, $params] = $this->patientAggregateFilters($filters);
        $where = $where === '' ? 'WHERE pr.id = :recordId' : $where . ' AND pr.id = :recordId';

        $params['type']     = $entityType;
        $params['id']       = $entityId;
        $params['recordId'] = $recordId;

        $sql = <<<SQL
            WITH claim_map AS (
                SELECT c.id AS claim_id, MIN(inv.id) AS invoice_id, MIN(inv.patient_record_id) AS record_id
                FROM claims c
                JOIN claim_items ci ON ci.claim_id = c.id
                JOIN invoice_items ii ON ii.id = ci.invoice_item_id
                JOIN invoices inv ON inv.id = ii.invoice_id
                WHERE c.entity_type = :type AND c.entity_id = :id
                GROUP BY c.id
            )
            SELECT
                c.id AS claim_id,
                c.uuid,
                c.insurance_id,
                c.insurance_kind,
                c.status,
                c.total_claimed_rials,
                c.total_approved_rials,
                c.total_paid_rials,
                c.reject_reason,
                c.tracking_number,
                c.submitted_at,
                c.settled_at,
                c.created_at,
                inv.uuid AS invoice_uuid,
                inv.total_rials AS service_base_rials,
                inv.patient_rials AS patient_share_rials,
                ps.session_at AS visit_date,
                d.name AS doctor_name
            FROM claim_map cm
            JOIN claims c ON c.id = cm.claim_id
            JOIN invoices inv ON inv.id = cm.invoice_id
            JOIN patient_records pr ON pr.id = cm.record_id
            JOIN users u ON u.id = pr.user_id
            LEFT JOIN patient_sessions ps ON ps.id = inv.patient_session_id
            LEFT JOIN appointments a ON a.id = ps.appointment_id
            LEFT JOIN doctors d ON d.id = a.doctor_id
            LEFT JOIN insurances ins ON ins.id = c.insurance_id
            {$where}
            ORDER BY c.created_at DESC, c.id DESC
            SQL;

        return $this->getEntityManager()->getConnection()->executeQuery($sql, $params)->fetchAllAssociative();
    }

    /** آیا برای این صورتحساب از قبل مطالبه‌ای ساخته شده؟ (از طریق آیتم‌های صورتحساب) */
    public function existsForInvoice(int $invoiceId): bool
    {
        $count = (int) $this->createQueryBuilder('c')
            ->select('COUNT(c.id)')
            ->join('c.items', 'ci')
            ->join('App\Billing\Entity\InvoiceItem', 'ii', 'WITH', 'ii.id = ci.invoiceItemId')
            ->where('IDENTITY(ii.invoice) = :invoiceId')
            ->setParameter('invoiceId', $invoiceId)
            ->getQuery()
            ->getSingleScalarResult();

        return $count > 0;
    }

    /**
     * نوع مطالبه‌هایی که برای این صورتحساب از قبل ثبت شده‌اند (`base` / `supplementary`).
     * مبنای idempotent بودنِ ساخت خودکار مطالبه است.
     *
     * @return list<string>
     */
    public function kindsForInvoice(int $invoiceId): array
    {
        $rows = $this->createQueryBuilder('c')
            ->select('DISTINCT c.insuranceKind AS kind')
            ->join('c.items', 'ci')
            ->join('App\Billing\Entity\InvoiceItem', 'ii', 'WITH', 'ii.id = ci.invoiceItemId')
            ->where('IDENTITY(ii.invoice) = :invoiceId')
            ->setParameter('invoiceId', $invoiceId)
            ->getQuery()
            ->getArrayResult();

        return array_column($rows, 'kind');
    }

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