<?php

namespace Modules\MedBloodBank\Services;

use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
use Modules\MedBloodBank\Entities\MbbCrossmatch;
use Modules\MedBloodBank\Entities\MbbDonation;
use Modules\MedBloodBank\Entities\MbbDonor;
use Modules\MedBloodBank\Entities\MbbIssue;

/**
 * BloodBankService (SIFA P6-T6) — the blood-bank domain engine.
 *
 * Owns the rules that must never live in a controller:
 *   - donor eligibility (deferral + 90-day interval)
 *   - the donation lifecycle (collected → quarantined → available/discarded)
 *   - infectious-disease screening evaluation
 *   - the ABO/Rh compatibility matrix + FIFO issuance suggestion
 *   - shelf-life / expiry derivation + the daily expiry sweep
 *   - transfusion completion + the adverse-reaction workflow (critical alert)
 *   - the stock grid + low-stock MedNotify fan-out (fail-soft)
 *
 * Every method is business_id-scoped. Cross-module touches (MedicalAlertService,
 * MedNotify) are best-effort / fail-soft so the module runs standalone.
 */
class BloodBankService
{
    // ── Expiry / shelf life ──────────────────────────────────────────────

    /**
     * Shelf-life end date for a bag from its product_type + collected_at.
     * whole_blood/prbc +35d, ffp/cryo +365d, platelets +5d (config-tunable).
     */
    public function expiryFor(string $productType, Carbon $collectedAt): Carbon
    {
        $days = (int) (config('medbloodbank.shelf_life_days.' . $productType)
            ?? config('medbloodbank.shelf_life_days.whole_blood', 35));

        return $collectedAt->copy()->addDays($days);
    }

    // ── Donor eligibility ────────────────────────────────────────────────

    /**
     * Assert a donor may donate now. Blocks when deferred_until is in the future
     * or the last donation was < min_days_between_donations ago.
     *
     * @throws ValidationException with a human message when ineligible
     */
    public function assertEligible(MbbDonor $donor): void
    {
        $check = $this->eligibility($donor);
        if (! $check['eligible']) {
            throw ValidationException::withMessages(['donor_id' => $check['reason']]);
        }
    }

    /** @return array{eligible:bool, reason:?string} */
    public function eligibility(MbbDonor $donor): array
    {
        $today = Carbon::today();

        if ($donor->deferred_until && $donor->deferred_until->gte($today)) {
            return [
                'eligible' => false,
                'reason'   => __('medbloodbank::lang.donor_deferred', [
                    'date'   => $donor->deferred_until->toDateString(),
                    'reason' => $donor->deferral_reason ?: __('medbloodbank::lang.not_specified'),
                ]),
            ];
        }

        $minDays = (int) config('medbloodbank.min_days_between_donations', 90);
        if ($donor->last_donation_at && $donor->last_donation_at->gt(Carbon::now()->subDays($minDays))) {
            $nextEligible = $donor->last_donation_at->copy()->addDays($minDays)->toDateString();

            return [
                'eligible' => false,
                'reason'   => __('medbloodbank::lang.donor_too_recent', [
                    'days' => $minDays,
                    'date' => $nextEligible,
                ]),
            ];
        }

        return ['eligible' => true, 'reason' => null];
    }

    // ── Donation lifecycle ───────────────────────────────────────────────

    /**
     * Record a new collection. Runs the eligibility gate, stamps expiry, moves
     * the bag to `quarantined` (auto after entry, pending screening) and bumps
     * the donor's counters.
     *
     * @param  array  $data  bag_no, donation_type, product_type, volume_ml,
     *                       blood_group, collected_at, collected_by
     */
    public function recordDonation(MbbDonor $donor, array $data): MbbDonation
    {
        $this->assertEligible($donor);

        return DB::transaction(function () use ($donor, $data) {
            $collectedAt = ! empty($data['collected_at'])
                ? Carbon::parse($data['collected_at'])
                : Carbon::now();

            $productType = $data['product_type'] ?? 'whole_blood';

            $donation = MbbDonation::create([
                'business_id'      => (int) $donor->business_id,
                'donor_id'         => (int) $donor->id,
                'bag_no'           => $data['bag_no'],
                'donation_type'    => $data['donation_type'] ?? 'voluntary',
                'product_type'     => $productType,
                'volume_ml'        => $data['volume_ml'] ?? null,
                'blood_group'      => $data['blood_group'] ?? $donor->blood_group,
                'collected_at'     => $collectedAt,
                'collected_by'     => $data['collected_by'] ?? (auth()->check() ? (int) auth()->id() : null),
                'screening'        => $this->blankScreening(),
                'screening_status' => 'pending',
                'expiry_date'      => $this->expiryFor($productType, $collectedAt)->toDateString(),
                // Auto-quarantine after entry — the bag is unusable until screening passes.
                'status'           => 'quarantined',
            ]);

            // Keep the donor's blood group + counters current.
            $donor->update([
                'blood_group'      => $donor->blood_group ?: ($data['blood_group'] ?? null),
                'last_donation_at' => $collectedAt,
                'total_donations'  => (int) $donor->total_donations + 1,
            ]);

            return $donation->fresh();
        });
    }

    /** The default pending marker panel. */
    public function blankScreening(): array
    {
        $panel = [];
        foreach (MbbDonation::SCREENING_MARKERS as $m) {
            $panel[$m] = 'pending';
        }

        return $panel;
    }

    // ── Screening ────────────────────────────────────────────────────────

    /**
     * Enter screening results. All four markers negative → passed → the bag
     * becomes `available`. Any reactive marker → failed → the bag is `discarded`
     * AND the donor is permanently deferred (deferred_until far future + note).
     *
     * @param  array<string,string>  $markers  hiv/hbv/hcv/syphilis => negative|reactive|pending
     */
    public function enterScreening(MbbDonation $donation, array $markers): MbbDonation
    {
        return DB::transaction(function () use ($donation, $markers) {
            $panel = array_merge($this->blankScreening(), $donation->screening ?? []);
            foreach (MbbDonation::SCREENING_MARKERS as $m) {
                if (isset($markers[$m]) && in_array($markers[$m], ['negative', 'reactive', 'pending'], true)) {
                    $panel[$m] = $markers[$m];
                }
            }

            $anyReactive = in_array('reactive', $panel, true);
            $anyPending  = in_array('pending', $panel, true);

            if ($anyReactive) {
                $donation->update([
                    'screening'        => $panel,
                    'screening_status' => 'failed',
                    'status'           => 'discarded',
                ]);
                $this->permanentlyDeferDonor($donation);
            } elseif (! $anyPending) {
                // All four negative → cleared for stock.
                $donation->update([
                    'screening'        => $panel,
                    'screening_status' => 'passed',
                    'status'           => 'available',
                ]);
            } else {
                // Still awaiting some markers — stays quarantined / pending.
                $donation->update([
                    'screening'        => $panel,
                    'screening_status' => 'pending',
                ]);
            }

            return $donation->fresh();
        });
    }

    /** Permanently defer the donor behind a failed-screening bag. */
    protected function permanentlyDeferDonor(MbbDonation $donation): void
    {
        $donor = MbbDonor::find($donation->donor_id);
        if (! $donor) {
            return;
        }

        $note = trim(((string) $donor->notes) . "\n"
            . __('medbloodbank::lang.permanent_defer_note', ['bag' => $donation->bag_no, 'date' => Carbon::today()->toDateString()]));

        $donor->update([
            // Far-future date = permanent deferral (no "forever" column in spec).
            'deferred_until'  => Carbon::today()->addYears(100)->toDateString(),
            'deferral_reason' => __('medbloodbank::lang.reactive_screening'),
            'notes'           => ltrim($note),
        ]);
    }

    // ── ABO / Rh compatibility ───────────────────────────────────────────

    /**
     * Donor groups whose RBCs are compatible with a recipient group, most
     * specific first (identical group, then the broader compatible set). O- is
     * the universal donor; AB+ is the universal recipient. Used for RBC/whole
     * blood; also applied as a conservative default for other products.
     *
     * @return string[] donor blood groups compatible with $recipient
     */
    public function compatibleDonorGroups(string $recipient): array
    {
        $matrix = [
            'O-'  => ['O-'],
            'O+'  => ['O+', 'O-'],
            'A-'  => ['A-', 'O-'],
            'A+'  => ['A+', 'A-', 'O+', 'O-'],
            'B-'  => ['B-', 'O-'],
            'B+'  => ['B+', 'B-', 'O+', 'O-'],
            'AB-' => ['AB-', 'A-', 'B-', 'O-'],
            'AB+' => ['AB+', 'AB-', 'A+', 'A-', 'B+', 'B-', 'O+', 'O-'],
        ];

        return $matrix[$recipient] ?? [$recipient];
    }

    public function isCompatible(string $recipient, string $donor): bool
    {
        return in_array($donor, $this->compatibleDonorGroups($recipient), true);
    }

    /**
     * FIFO issuance suggestion: available, non-expired bags whose group is
     * compatible with the recipient, earliest expiry first (then oldest bag).
     * When $productType is given, prefer that product but never exclude a
     * compatible RBC fallback.
     */
    public function suggestBags(int $businessId, string $recipientGroup, ?string $productType = null, int $limit = 10)
    {
        $groups = $this->compatibleDonorGroups($recipientGroup);

        $query = MbbDonation::forBusiness($businessId)
            ->available()
            ->whereIn('blood_group', $groups)
            ->where(function ($q) {
                $q->whereNull('expiry_date')->orWhere('expiry_date', '>=', Carbon::today());
            });

        if ($productType) {
            $query->where('product_type', $productType);
        }

        return $query
            ->orderByRaw('expiry_date IS NULL')       // dated bags first
            ->orderBy('expiry_date')                   // earliest expiry (FIFO)
            ->orderBy('collected_at')                  // then oldest collection
            ->limit($limit)
            ->get();
    }

    // ── Crossmatch + issuance ────────────────────────────────────────────

    /**
     * Issue a crossmatched bag. Requires a `compatible` crossmatch for THIS bag
     * (an incompatible or missing crossmatch blocks). Reserves→issues the bag.
     *
     * @param  array  $data  issued_to, issued_by
     */
    public function issueBag(MbbCrossmatch $crossmatch, array $data): MbbIssue
    {
        if ($crossmatch->result !== 'compatible') {
            throw ValidationException::withMessages([
                'crossmatch_id' => __('medbloodbank::lang.crossmatch_not_compatible'),
            ]);
        }

        $donation = MbbDonation::forBusiness($crossmatch->business_id)->find($crossmatch->donation_id);
        if (! $donation || ! $donation->isIssuable()) {
            throw ValidationException::withMessages([
                'donation_id' => __('medbloodbank::lang.bag_not_issuable'),
            ]);
        }

        return DB::transaction(function () use ($crossmatch, $donation, $data) {
            $issue = MbbIssue::create([
                'business_id'  => (int) $crossmatch->business_id,
                'crossmatch_id' => (int) $crossmatch->id,
                'donation_id'  => (int) $donation->id,
                'patient_id'   => (int) $crossmatch->patient_id,
                'issued_to'    => $data['issued_to'] ?? 'ward',
                'issued_by'    => $data['issued_by'] ?? (auth()->check() ? (int) auth()->id() : null),
                'issued_at'    => Carbon::now(),
                'status'       => 'issued',
            ]);

            $donation->update(['status' => 'issued']);

            return $issue->fresh();
        });
    }

    /** Start the transfusion clock. */
    public function startTransfusion(MbbIssue $issue): MbbIssue
    {
        $issue->update([
            'status'                 => 'transfusing',
            'transfusion_started_at' => $issue->transfusion_started_at ?: Carbon::now(),
            'transfused_by'          => $issue->transfused_by ?: (auth()->check() ? (int) auth()->id() : null),
        ]);

        return $issue->fresh();
    }

    /**
     * Complete a transfusion. The bag is marked `transfused`. When reaction data
     * is supplied the issue is flagged and a CRITICAL, patient-linked alert is
     * raised (idempotent per issue).
     *
     * @param  array|null  $reaction  type, severity, management (present => a reaction occurred)
     */
    public function completeTransfusion(MbbIssue $issue, ?array $reaction = null): MbbIssue
    {
        return DB::transaction(function () use ($issue, $reaction) {
            $hasReaction = ! empty($reaction) && ! empty($reaction['type']);

            $issue->update([
                'status'                   => 'completed',
                'transfusion_started_at'   => $issue->transfusion_started_at ?: Carbon::now(),
                'transfusion_completed_at' => Carbon::now(),
                'transfused_by'            => $issue->transfused_by ?: (auth()->check() ? (int) auth()->id() : null),
                'reaction'                 => $hasReaction,
                'reaction_type'            => $hasReaction ? $reaction['type'] : null,
                'reaction_severity'        => $hasReaction ? ($reaction['severity'] ?? null) : null,
                'reaction_management'      => $hasReaction ? ($reaction['management'] ?? null) : null,
            ]);

            MbbDonation::where('id', $issue->donation_id)->update(['status' => 'transfused']);

            if ($hasReaction) {
                $this->raiseReactionAlert($issue->fresh());
            }

            return $issue->fresh();
        });
    }

    /**
     * Raise a critical, patient-linked medical alert for a transfusion reaction
     * via ClinicCore's MedicalAlertService. Idempotent per issue (source_type =
     * mbb_issues, source_id = issue id). Fail-soft if ClinicCore absent.
     */
    protected function raiseReactionAlert(MbbIssue $issue): void
    {
        try {
            if (! class_exists(\Modules\ClinicCore\Entities\Services\MedicalAlertService::class)) {
                return;
            }

            app(\Modules\ClinicCore\Entities\Services\MedicalAlertService::class)->raise([
                'business_id' => (int) $issue->business_id,
                'patient_id'  => (int) $issue->patient_id,
                'alert_type'  => 'critical_value',
                'severity'    => 'critical',
                'title'       => __('medbloodbank::lang.reaction_alert_title', [
                    'type' => __('medbloodbank::lang.reaction_type_' . $issue->reaction_type),
                ]),
                'details'     => __('medbloodbank::lang.reaction_alert_details', [
                    'severity'   => $issue->reaction_severity ? __('medbloodbank::lang.reaction_severity_' . $issue->reaction_severity) : '',
                    'management' => $issue->reaction_management ?: '',
                ]),
                'source_type' => 'mbb_issues',
                'source_id'   => (int) $issue->id,
            ]);
        } catch (\Throwable $e) {
            \Log::warning('MedBloodBank reaction alert raise skipped: ' . $e->getMessage());
        }
    }

    // ── Expiry sweep ─────────────────────────────────────────────────────

    /**
     * Mark every non-terminal bag past its expiry_date as `expired`.
     * Idempotent — only touches still-holdable statuses. Returns the count.
     */
    public function expireBags(?int $businessId = null): int
    {
        $query = MbbDonation::whereNotNull('expiry_date')
            ->whereDate('expiry_date', '<', Carbon::today())
            ->whereIn('status', ['collected', 'quarantined', 'available', 'reserved']);

        if ($businessId) {
            $query->where('business_id', $businessId);
        }

        return $query->update(['status' => 'expired']);
    }

    // ── Stock dashboard + low-stock notify ───────────────────────────────

    /**
     * Available-stock grid: group × product_type counts + per-group thresholds
     * and a low flag. Shape:
     *   [ 'groups' => [group => ['products'=>[pt=>count], 'total'=>n, 'min'=>t, 'low'=>bool]],
     *     'products' => [...], 'low_groups' => [group,...] ]
     */
    public function stockGrid(int $businessId): array
    {
        $rows = MbbDonation::forBusiness($businessId)
            ->available()
            ->where(function ($q) {
                $q->whereNull('expiry_date')->orWhere('expiry_date', '>=', Carbon::today());
            })
            ->select('blood_group', 'product_type', DB::raw('COUNT(*) as c'))
            ->groupBy('blood_group', 'product_type')
            ->get();

        $groups   = MbbDonation::BLOOD_GROUPS;
        $products = MbbDonation::PRODUCT_TYPES;

        $grid = [];
        foreach ($groups as $g) {
            $grid[$g] = ['products' => array_fill_keys($products, 0), 'total' => 0];
        }
        foreach ($rows as $r) {
            if (! isset($grid[$r->blood_group])) {
                continue;
            }
            $grid[$r->blood_group]['products'][$r->product_type] = (int) $r->c;
            $grid[$r->blood_group]['total'] += (int) $r->c;
        }

        $lowGroups = [];
        foreach ($grid as $g => &$data) {
            $min = (int) (config('medbloodbank.min_stock.' . $g) ?? config('medbloodbank.min_stock.default', 2));
            $data['min'] = $min;
            $data['low'] = $data['total'] <= $min;
            if ($data['low']) {
                $lowGroups[] = $g;
            }
        }
        unset($data);

        return [
            'groups'     => $grid,
            'products'   => $products,
            'low_groups' => $lowGroups,
        ];
    }

    /** Available, non-expired bags expiring within the ladder days, soonest first. */
    public function expiringSoon(int $businessId, ?int $days = null)
    {
        $ladder = config('medbloodbank.expiry_ladder_days', [7, 15, 30]);
        $days   = $days ?: (int) max($ladder);

        return MbbDonation::forBusiness($businessId)
            ->available()
            ->whereNotNull('expiry_date')
            ->whereDate('expiry_date', '>=', Carbon::today())
            ->whereDate('expiry_date', '<=', Carbon::today()->addDays($days))
            ->orderBy('expiry_date')
            ->get();
    }

    /**
     * Evaluate low stock and fire a fail-soft MedNotify `bloodbank.stock_low`
     * event per low group. No-op without MedNotify. Returns the low groups.
     *
     * @return string[]
     */
    public function notifyLowStock(int $businessId): array
    {
        $grid = $this->stockGrid($businessId);
        $low  = $grid['low_groups'];

        if (empty($low)) {
            return [];
        }

        try {
            if (! class_exists(\Modules\MedNotify\Services\MedNotify::class)) {
                return $low;
            }

            $userIds = $this->bloodBankUserIds($businessId);
            if (empty($userIds)) {
                return $low;
            }

            foreach ($low as $group) {
                app(\Modules\MedNotify\Services\MedNotify::class)->notify('bloodbank.stock_low', [
                    'business_id' => $businessId,
                    'users'       => $userIds,
                    'variables'   => [
                        'blood_group' => $group,
                        'available'   => (string) $grid['groups'][$group]['total'],
                        'min'         => (string) $grid['groups'][$group]['min'],
                        'link'        => url('/med-blood-bank'),
                    ],
                ]);
            }
        } catch (\Throwable $e) {
            \Log::warning('MedBloodBank low-stock notify skipped: ' . $e->getMessage());
        }

        return $low;
    }

    /** Users who can manage issuance in this business (best-effort recipients). */
    protected function bloodBankUserIds(int $businessId): array
    {
        try {
            if (! Schema::hasTable('users')) {
                return [];
            }

            return \App\User::where('business_id', $businessId)
                ->get()
                ->filter(fn ($u) => $u->can('mbb.issue.manage') || $u->can('mbb.donations.manage'))
                ->pluck('id')
                ->map(fn ($id) => (int) $id)
                ->all();
        } catch (\Throwable $e) {
            return [];
        }
    }
}
