<?php

namespace Modules\MedWard\Services;

use App\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Modules\MedWard\Entities\MedwardAdmission;
use Modules\MedWard\Entities\MedwardBed;
use Modules\MedWard\Entities\MedwardBedAssignment;

/**
 * AdmissionService (SIFA P6-T2) — the admission lifecycle & bed-occupancy engine.
 *
 * admit()      validate the target bed is available → occupy it, open a bed_assignment
 *              history row, allocate ADM-{biz}-{seq}, status = admitted.
 * transfer()   release the current bed (→ dirty), occupy the new one, roll the
 *              bed_assignment history and repoint the admission.
 * requestDischarge()  status → discharge_pending (opens the discharge checklist).
 * discharge()  require a discharge summary (+ optional med-reconciliation note),
 *              status → discharged, release the bed to dirty and FIRE MedNotify
 *              bed.available (fail-soft).
 *
 * Every bed/admission mutation runs inside a DB transaction. Cross-module notify
 * is fail-soft (Rule 6): a notify failure never rolls back the discharge.
 */
class AdmissionService
{
    /**
     * Admit a patient to a bed.
     *
     * @param  array  $data  business_id, patient_id, contact_id, ward_id, bed_id,
     *                       admission_type, admission_source, admitting_diagnosis?,
     *                       admitting_doctor_id?, appointment_id?, expected_discharge?,
     *                       created_by?, is_icu?, icu_type?, apache_ii?, sofa?,
     *                       ventilator?, ventilator_mode?
     */
    public function admit(array $data): MedwardAdmission
    {
        $businessId = (int) $data['business_id'];

        return DB::transaction(function () use ($data, $businessId) {
            $bed = MedwardBed::forBusiness($businessId)
                ->where('id', (int) $data['bed_id'])
                ->lockForUpdate()
                ->first();

            if (! $bed) {
                throw ValidationException::withMessages(['bed_id' => __('medward::lang.bed_not_found')]);
            }
            if ($bed->status !== 'available') {
                throw ValidationException::withMessages(['bed_id' => __('medward::lang.bed_not_available')]);
            }
            if ((int) $bed->ward_id !== (int) $data['ward_id']) {
                throw ValidationException::withMessages(['bed_id' => __('medward::lang.bed_wrong_ward')]);
            }

            $now = Carbon::now();

            $admission = MedwardAdmission::create([
                'business_id'          => $businessId,
                'admission_no'         => MedwardAdmission::nextAdmissionNo($businessId),
                'patient_id'           => (int) $data['patient_id'],
                'contact_id'           => (int) $data['contact_id'],
                'appointment_id'       => $data['appointment_id'] ?? null,
                'admitting_doctor_id'  => $data['admitting_doctor_id'] ?? null,
                'ward_id'              => (int) $data['ward_id'],
                'bed_id'               => (int) $bed->id,
                'admission_type'       => $data['admission_type'] ?? 'elective',
                'admission_source'     => $data['admission_source'] ?? 'outpatient',
                'admitting_diagnosis'  => $data['admitting_diagnosis'] ?? null,
                'admitted_at'          => $data['admitted_at'] ?? $now,
                'expected_discharge'   => $data['expected_discharge'] ?? null,
                'status'               => 'admitted',
                'is_icu'               => (bool) ($data['is_icu'] ?? false),
                'icu_type'             => $data['icu_type'] ?? null,
                'apache_ii'            => $data['apache_ii'] ?? null,
                'sofa'                 => $data['sofa'] ?? null,
                'ventilator'           => (bool) ($data['ventilator'] ?? false),
                'ventilator_mode'      => $data['ventilator_mode'] ?? null,
                'created_by'           => $data['created_by'] ?? (auth()->check() ? (int) auth()->id() : null),
            ]);

            $this->occupyBed($bed, $admission, $now);

            MedwardBedAssignment::create([
                'admission_id' => $admission->id,
                'bed_id'       => $bed->id,
                'assigned_at'  => $now,
                'reason'       => __('medward::lang.reason_admission'),
            ]);

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

    /**
     * Transfer an active admission to a different available bed.
     */
    public function transfer(MedwardAdmission $admission, int $newBedId, ?string $reason = null): MedwardAdmission
    {
        if (! $admission->isActive()) {
            throw ValidationException::withMessages(['admission' => __('medward::lang.admission_not_active')]);
        }

        return DB::transaction(function () use ($admission, $newBedId, $reason) {
            $businessId = (int) $admission->business_id;

            $newBed = MedwardBed::forBusiness($businessId)->where('id', $newBedId)->lockForUpdate()->first();
            if (! $newBed) {
                throw ValidationException::withMessages(['bed_id' => __('medward::lang.bed_not_found')]);
            }
            if ((int) $newBed->id === (int) $admission->bed_id) {
                throw ValidationException::withMessages(['bed_id' => __('medward::lang.bed_same')]);
            }
            if ($newBed->status !== 'available') {
                throw ValidationException::withMessages(['bed_id' => __('medward::lang.bed_not_available')]);
            }

            $now = Carbon::now();

            // Release the old bed (→ dirty) and close its history row.
            $oldBed = MedwardBed::forBusiness($businessId)->where('id', $admission->bed_id)->lockForUpdate()->first();
            if ($oldBed) {
                $this->releaseBed($oldBed);
            }
            $this->closeOpenAssignment($admission, $now, $reason);

            // Occupy the new bed and open a fresh history row.
            $this->occupyBed($newBed, $admission, $now);
            $admission->update(['ward_id' => $newBed->ward_id, 'bed_id' => $newBed->id]);

            MedwardBedAssignment::create([
                'admission_id' => $admission->id,
                'bed_id'       => $newBed->id,
                'assigned_at'  => $now,
                'reason'       => $reason ?: __('medward::lang.reason_transfer'),
            ]);

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

    /** Move an admitted episode into the discharge checklist (discharge_pending). */
    public function requestDischarge(MedwardAdmission $admission): MedwardAdmission
    {
        if ($admission->status !== 'admitted') {
            throw ValidationException::withMessages(['status' => __('medward::lang.discharge_bad_state')]);
        }

        $admission->update(['status' => 'discharge_pending']);

        return $admission->fresh();
    }

    /**
     * Complete the discharge workflow: require a discharge summary, record the
     * discharge fields, release the bed to dirty and fire bed.available.
     *
     * @param  array  $data  discharge_type, discharge_summary (required),
     *                       discharge_instructions?, med_reconciliation?,
     *                       followup_date?
     * @return array{admission: MedwardAdmission, bed_notified: bool}
     */
    public function discharge(MedwardAdmission $admission, array $data): array
    {
        if (! $admission->isActive()) {
            throw ValidationException::withMessages(['status' => __('medward::lang.discharge_bad_state')]);
        }
        if (empty(trim((string) ($data['discharge_summary'] ?? '')))) {
            throw ValidationException::withMessages(['discharge_summary' => __('medward::lang.summary_required')]);
        }

        [$admission, $bed] = DB::transaction(function () use ($admission, $data) {
            $now = Carbon::now();

            // Fold the optional med-reconciliation note into the summary.
            $summary = trim((string) $data['discharge_summary']);
            if (! empty(trim((string) ($data['med_reconciliation'] ?? '')))) {
                $summary .= "\n\n" . __('medward::lang.med_reconciliation') . ': ' . trim($data['med_reconciliation']);
            }

            $admission->update([
                'status'                 => 'discharged',
                'discharge_type'         => $data['discharge_type'] ?? 'recovered',
                'discharge_summary'      => $summary,
                'discharge_instructions' => $data['discharge_instructions'] ?? null,
                'followup_date'          => $data['followup_date'] ?? null,
                'discharged_at'          => $now,
            ]);

            $bed = MedwardBed::forBusiness((int) $admission->business_id)
                ->where('id', $admission->bed_id)->lockForUpdate()->first();
            if ($bed) {
                $this->releaseBed($bed);
            }
            $this->closeOpenAssignment($admission, $now, __('medward::lang.reason_discharge'));

            return [$admission->fresh(), $bed];
        });

        // Fail-soft: notify never rolls back the discharge.
        $notified = $bed ? $this->fireBedAvailable($bed, $admission) : false;

        return ['admission' => $admission, 'bed_notified' => $notified];
    }

    // ── internals ────────────────────────────────────────────────────────

    /** Occupy a bed for an admission (denormalised pointer + status). */
    protected function occupyBed(MedwardBed $bed, MedwardAdmission $admission, Carbon $now): void
    {
        $bed->update([
            'status'               => 'occupied',
            'current_admission_id' => $admission->id,
        ]);
    }

    /** Release a bed: clear the occupant, mark available + dirty (needs turnover). */
    protected function releaseBed(MedwardBed $bed): void
    {
        $bed->update([
            'status'               => 'available',
            'cleaning_status'      => 'dirty',
            'current_admission_id' => null,
        ]);
    }

    /** Close the currently open (released_at NULL) bed-assignment row. */
    protected function closeOpenAssignment(MedwardAdmission $admission, Carbon $now, ?string $reason): void
    {
        MedwardBedAssignment::where('admission_id', $admission->id)
            ->whereNull('released_at')
            ->update(['released_at' => $now, 'reason' => $reason ?: DB::raw('reason')]);
    }

    /**
     * Fire the MedNotify `bed.available` event to ward users (fail-soft: no-op if
     * MedNotify is absent or no recipients resolve).
     */
    protected function fireBedAvailable(MedwardBed $bed, MedwardAdmission $admission): bool
    {
        try {
            if (! class_exists(\Modules\MedNotify\Services\MedNotify::class)) {
                return false;
            }

            $userIds = $this->wardUserIds((int) $bed->business_id);
            if (empty($userIds)) {
                return false;
            }

            $wardName = optional($bed->ward()->first())->name ?? '';

            app(\Modules\MedNotify\Services\MedNotify::class)->notify('bed.available', [
                'business_id' => (int) $bed->business_id,
                'users'       => $userIds,
                'variables'   => [
                    'ward_name'    => $wardName,
                    'bed_no'       => (string) $bed->bed_number,
                    'patient_name' => '',
                    'link'         => url('/med-ward/board'),
                ],
            ]);

            return true;
        } catch (\Throwable $e) {
            \Log::warning('MedWard bed.available notify skipped: ' . $e->getMessage());

            return false;
        }
    }

    /** Users who can view the bed board in this business (best-effort). */
    protected function wardUserIds(int $businessId): array
    {
        try {
            return User::where('business_id', $businessId)
                ->get()
                ->filter(fn ($u) => $u->can('medward.board.view'))
                ->pluck('id')
                ->map(fn ($id) => (int) $id)
                ->all();
        } catch (\Throwable $e) {
            return [];
        }
    }
}
