<?php

namespace Modules\MedWard\Services;

use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
use Modules\MedWard\Entities\MedwardAdmission;
use Modules\MedWard\Entities\MedwardMarEntry;

/**
 * MarService (SIFA P6-T2 MAR) — generates the Medication Administration Record on
 * demand and drives the administer / hold actions.
 *
 * generateForAdmission()
 *   Reads the active inpatient prescription items linked to the admission's
 *   patient from ClinicCore (clc_prescriptions.status = active, joined to
 *   clc_prescription_items) and, for a target day, drafts `scheduled` MAR rows —
 *   one per per-day slot from a simple frequency parse (OD=1, BD=2, TID=3, QID=4,
 *   Qn h → 24/n). Idempotent: an existing row for the same (prescription_item_id,
 *   scheduled_at) is never duplicated.
 *
 *   FAIL-SOFT (Rule 6): if ClinicCore tables are absent the method returns 0 and
 *   the caller can still add manual MAR rows.
 *
 * administer() / hold()
 *   Record the outcome. Controlled meds (clc_medicines.controlled_schedule set —
 *   read fail-soft) require a witness_user_id on administer.
 */
class MarService
{
    /** Default clock slots per per-day frequency count (spread across the day). */
    protected array $slotTimes = [
        1 => ['08:00'],
        2 => ['08:00', '20:00'],
        3 => ['08:00', '14:00', '20:00'],
        4 => ['06:00', '12:00', '18:00', '00:00'],
    ];

    /**
     * Generate scheduled MAR rows for an admission for a given day.
     *
     * @param  Carbon|null  $day  target day (defaults to today)
     * @return int  number of new scheduled rows created
     */
    public function generateForAdmission(MedwardAdmission $admission, ?Carbon $day = null): int
    {
        $day = ($day ?: Carbon::today())->copy()->startOfDay();
        $businessId = (int) $admission->business_id;

        if (! Schema::hasTable('clc_prescriptions') || ! Schema::hasTable('clc_prescription_items')) {
            return 0; // ClinicCore absent — nothing to source.
        }

        $items = $this->activeRxItems($businessId, (int) $admission->patient_id);
        if ($items->isEmpty()) {
            return 0;
        }

        $created = 0;

        DB::transaction(function () use ($items, $admission, $businessId, $day, &$created) {
            foreach ($items as $item) {
                $perDay = $this->slotsPerDay((string) ($item->frequency ?? ''));
                if ($perDay < 1) {
                    continue;
                }

                $times = $this->slotTimes[$perDay] ?? $this->evenSlots($perDay);

                foreach ($times as $clock) {
                    $scheduledAt = Carbon::parse($day->format('Y-m-d') . ' ' . $clock);

                    $exists = MedwardMarEntry::where('admission_id', $admission->id)
                        ->where('prescription_item_id', $item->id)
                        ->where('scheduled_at', $scheduledAt)
                        ->exists();
                    if ($exists) {
                        continue;
                    }

                    MedwardMarEntry::create([
                        'business_id'          => $businessId,
                        'admission_id'         => $admission->id,
                        'prescription_item_id' => (int) $item->id,
                        'medicine_name'        => (string) $item->medicine_name,
                        'scheduled_at'         => $scheduledAt,
                        'status'               => 'scheduled',
                        'dose'                 => $item->dosage ?: null,
                        'route'                => $this->routeHint($item),
                    ]);
                    $created++;
                }
            }
        });

        return $created;
    }

    /**
     * Administer (give) a scheduled MAR entry. Controlled meds require a witness.
     *
     * @param  array  $data  administered_by, witness_user_id?, dose?, route?
     */
    public function administer(MedwardMarEntry $entry, array $data): MedwardMarEntry
    {
        if (! in_array($entry->status, ['scheduled', 'missed'], true)) {
            throw ValidationException::withMessages(['status' => __('medward::lang.mar_already_actioned')]);
        }

        if ($this->isControlled($entry) && empty($data['witness_user_id'])) {
            throw ValidationException::withMessages(['witness_user_id' => __('medward::lang.witness_required')]);
        }

        $entry->update([
            'status'          => 'given',
            'administered_at' => Carbon::now(),
            'administered_by' => $data['administered_by'] ?? (auth()->check() ? (int) auth()->id() : null),
            'witness_user_id' => ! empty($data['witness_user_id']) ? (int) $data['witness_user_id'] : $entry->witness_user_id,
            'dose'            => $data['dose'] ?? $entry->dose,
            'route'           => $data['route'] ?? $entry->route,
        ]);

        return $entry->fresh();
    }

    /**
     * Hold / refuse / mark-missed a scheduled MAR entry with a reason.
     *
     * @param  string  $status  one of held|refused|missed
     */
    public function hold(MedwardMarEntry $entry, string $status, string $reason): MedwardMarEntry
    {
        if (! in_array($status, ['held', 'refused', 'missed'], true)) {
            throw ValidationException::withMessages(['status' => __('medward::lang.mar_invalid_status')]);
        }
        if ($entry->status !== 'scheduled') {
            throw ValidationException::withMessages(['status' => __('medward::lang.mar_already_actioned')]);
        }
        if (trim($reason) === '') {
            throw ValidationException::withMessages(['reason' => __('medward::lang.reason_required')]);
        }

        $entry->update([
            'status'         => $status,
            'reason_if_held' => $reason,
            'administered_by' => auth()->check() ? (int) auth()->id() : null,
        ]);

        return $entry->fresh();
    }

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

    /**
     * Active inpatient prescription items for a patient. Prefers rows where the
     * prescription's rx_type is inpatient/discharge when that column exists; else
     * falls back to all active-prescription items (fail-soft to schema shape).
     */
    protected function activeRxItems(int $businessId, int $patientId)
    {
        $q = DB::table('clc_prescription_items as pi')
            ->join('clc_prescriptions as p', 'pi.prescription_id', '=', 'p.id')
            ->where('p.business_id', $businessId)
            ->where('p.patient_id', $patientId)
            ->where('p.status', 'active');

        if (Schema::hasColumn('clc_prescriptions', 'rx_type')) {
            $q->whereIn('p.rx_type', ['inpatient', 'discharge']);
        }

        return $q->get(['pi.id', 'pi.medicine_id', 'pi.medicine_name', 'pi.dosage', 'pi.frequency', 'pi.instructions']);
    }

    /**
     * Parse a free-text frequency into a per-day slot count.
     *   OD / QD / daily / once     → 1
     *   BD / BID / twice           → 2
     *   TID / TDS / thrice         → 3
     *   QID / QDS / four times     → 4
     *   Q<n>H (every n hours)      → floor(24 / n)
     * Unknown / PRN → 0 (no scheduled rows).
     */
    public function slotsPerDay(string $frequency): int
    {
        $f = strtolower(trim($frequency));
        if ($f === '') {
            return 0;
        }

        if (str_contains($f, 'prn') || str_contains($f, 'as needed')) {
            return 0;
        }

        // Q<n>H — every n hours.
        if (preg_match('/q\s*(\d{1,2})\s*h/', $f, $m)) {
            $n = (int) $m[1];

            return $n > 0 ? max(1, (int) floor(24 / $n)) : 0;
        }

        $map = [
            'qid' => 4, 'qds' => 4, 'four times' => 4,
            'tid' => 3, 'tds' => 3, 'thrice' => 3, 'three times' => 3,
            'bid' => 2, 'bds' => 2, 'bd' => 2, 'twice' => 2, 'two times' => 2,
            'od' => 1, 'qd' => 1, 'daily' => 1, 'once' => 1, 'hs' => 1, 'nocte' => 1, 'mane' => 1,
        ];
        foreach ($map as $needle => $count) {
            if (str_contains($f, $needle)) {
                return $count;
            }
        }

        return 0;
    }

    /** Evenly spread N slots across 24h as HH:00 strings (for Q<n>H frequencies). */
    protected function evenSlots(int $perDay): array
    {
        $perDay = max(1, min(24, $perDay));
        $step = (int) floor(24 / $perDay);
        $out = [];
        for ($i = 0; $i < $perDay; $i++) {
            $out[] = sprintf('%02d:00', ($i * $step) % 24);
        }

        return $out;
    }

    /** Best-effort route hint from the item instructions (falls back to null). */
    protected function routeHint($item): ?string
    {
        $text = strtolower((string) ($item->instructions ?? ''));
        foreach (['iv' => 'IV', 'im' => 'IM', 'sc' => 'SC', 'oral' => 'PO', 'po' => 'PO'] as $needle => $route) {
            if (str_contains($text, $needle)) {
                return $route;
            }
        }

        return null;
    }

    /**
     * Whether a MAR entry's source medicine is a controlled substance. Reads
     * clc_medicines.controlled_schedule fail-soft (returns false if ClinicCore /
     * the column / the row is absent).
     */
    protected function isControlled(MedwardMarEntry $entry): bool
    {
        try {
            if (! $entry->prescription_item_id
                || ! Schema::hasTable('clc_medicines')
                || ! Schema::hasTable('clc_prescription_items')
                || ! Schema::hasColumn('clc_medicines', 'controlled_schedule')) {
                return false;
            }

            $schedule = DB::table('clc_prescription_items as pi')
                ->join('clc_medicines as m', 'pi.medicine_id', '=', 'm.id')
                ->where('pi.id', $entry->prescription_item_id)
                ->value('m.controlled_schedule');

            return ! empty($schedule);
        } catch (\Throwable $e) {
            \Log::warning('MedWard MAR controlled-check skipped: ' . $e->getMessage());

            return false;
        }
    }
}
