<?php

namespace Modules\VetAdmission\Entities\Services;

use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Support\Collection;
use Modules\VetAdmission\Entities\VaAdmission;
use Modules\VetAdmission\Entities\VaAdmissionCarePlan;
use Modules\VetAdmission\Entities\VaAdmissionMedication;
use Modules\VetAdmission\Entities\VaCareLog;
use Modules\VetAdmission\Entities\VaMedicationAdministration;

/**
 * Expands an admission's care-plan rows into expected time slots within a
 * date window, then overlays any existing care_logs rows so callers see a
 * unified `status` for every slot.
 *
 * Status grammar:
 *   pending  : no log, slot more than DUE_WINDOW minutes in the future
 *   due      : no log, slot within +- DUE_WINDOW minutes of "now"
 *   missed   : no log, slot more than LATE_WINDOW minutes in the past
 *   done     : VaCareLog row with status='done'
 *   skipped  : VaCareLog row with status='skipped'
 */
class CarePlanSlotService
{
    /** Window around "now" within which an undone slot is considered "due". */
    public const DUE_WINDOW_MIN = 30;

    /** Past slot becomes "missed" after this many minutes without a log. */
    public const LATE_WINDOW_MIN = 60;

    /**
     * Build the full flowsheet matrix for an admission within [from, to].
     *
     * @return Collection list of plan rows, each with `slots[]` keyed by
     *                    "Y-m-d|slot_index", carrying a status + due_at +
     *                    log_id when present. Format suitable for direct
     *                    JSON serialisation.
     */
    public function buildMatrix(VaAdmission $admission, Carbon $from, Carbon $to): Collection
    {
        $plans = $admission->carePlan()
            ->with('taskType')
            ->where('active', true)
            ->orderBy('id')->get();

        if ($plans->isEmpty()) {
            return collect();
        }

        $logs = VaCareLog::where('admission_id', $admission->id)
            ->whereBetween('care_date', [$from->toDateString(), $to->toDateString()])
            ->get()
            ->groupBy(fn($l) => $l->care_plan_id . '|' . $l->care_date->toDateString() . '|' . $l->slot_index);

        $now = Carbon::now();

        $careRows = $plans->map(function (VaAdmissionCarePlan $plan) use ($from, $to, $logs, $now) {

            $slotTimes = $this->slotTimesFor($plan);
            $slots = [];

            foreach (CarbonPeriod::create($from->copy()->startOfDay(), $to->copy()->endOfDay()) as $day) {
                foreach ($slotTimes as $index => $clock) {
                    $dueAt = $day->copy()->setTimeFromTimeString($clock);
                    $key   = $plan->id . '|' . $day->toDateString() . '|' . $index;
                    $log   = $logs->get($key)?->first();

                    if ($log) {
                        $status = $log->status === 'skipped' ? 'skipped' : 'done';
                    } else {
                        $diffMin = $now->diffInMinutes($dueAt, false); // negative = past
                        if ($diffMin >= self::DUE_WINDOW_MIN) {
                            $status = 'pending';
                        } elseif ($diffMin >= -self::DUE_WINDOW_MIN) {
                            $status = 'due';
                        } elseif ($diffMin >= -self::LATE_WINDOW_MIN) {
                            $status = 'due';
                        } else {
                            $status = 'missed';
                        }
                    }

                    $slots[] = [
                        'plan_id'    => $plan->id,
                        'date'       => $day->toDateString(),
                        'slot_index' => $index,
                        'due_at'     => $dueAt->toIso8601String(),
                        'status'     => $status,
                        'log_id'     => $log?->id,
                        'done_by'    => $log?->done_by,
                        'notes'      => $log?->notes,
                    ];
                }
            }

            return [
                'kind'            => 'care_plan',
                'plan_id'         => $plan->id,
                'task_type_id'    => $plan->care_task_type_id,
                'task_name'       => optional($plan->taskType)->name,
                'task_icon'       => optional($plan->taskType)->icon,
                'task_color'      => optional($plan->taskType)->color,
                'task_category'   => optional($plan->taskType)->category,
                'instructions'    => $plan->instructions,
                'times_per_day'   => $plan->times_per_day,
                'schedule_times'  => $slotTimes,
                'slots'           => $slots,
            ];
        });

        // Medication rows — appended after care-plan rows so the order in the
        // grid is: routines first, then drug administrations.
        $medRows = $this->medicationRows($admission, $from, $to);

        return $careRows->concat($medRows)->values();
    }

    /** One flowsheet row per active medication order in the window. */
    private function medicationRows(VaAdmission $admission, Carbon $from, Carbon $to): Collection
    {
        $meds = VaAdmissionMedication::where('admission_id', $admission->id)
            ->where(function ($q) use ($from, $to) {
                $q->whereBetween('start_at', [$from, $to])
                  ->orWhere(function ($qq) use ($from) {
                      $qq->where('start_at', '<=', $from)
                         ->where(function ($qqq) use ($from) {
                             $qqq->whereNull('end_at')->orWhere('end_at', '>=', $from);
                         });
                  });
            })
            ->orderBy('id')
            ->get();

        if ($meds->isEmpty()) return collect();

        $admins = VaMedicationAdministration::whereIn('admission_medication_id', $meds->pluck('id'))
            ->whereBetween('due_at', [$from, $to])
            ->orderBy('due_at')
            ->get()
            ->groupBy('admission_medication_id');

        return $meds->map(function (VaAdmissionMedication $med) use ($admins) {
            $slots = [];
            foreach ($admins->get($med->id, collect()) as $row) {
                $slots[] = [
                    'admin_id'    => $row->id,
                    'date'        => $row->due_at->toDateString(),
                    'slot_index'  => $row->id,                          // stable per admin row
                    'due_at'      => $row->due_at->toIso8601String(),
                    'status'      => $this->medStatus($row),
                    'given_by'    => $row->given_by,
                    'notes'       => $row->notes,
                ];
            }

            return [
                'kind'            => 'medication',
                'medication_id'   => $med->id,
                'task_name'       => $med->medicine_name
                    . ' · ' . $med->route
                    . ($med->volume_per_dose_ml ? ' · ' . rtrim(rtrim(number_format((float) $med->volume_per_dose_ml, 3, '.', ''), '0'), '.') . ' ml' : '')
                    . ($med->dose_total_mg ? ' (' . rtrim(rtrim(number_format((float) $med->dose_total_mg, 3, '.', ''), '0'), '.') . ' mg)' : ''),
                'task_icon'       => 'fa-pills',
                'task_color'      => '#ef4444',
                'task_category'   => 'medication',
                'instructions'    => $med->notes,
                'times_per_day'   => null,
                'schedule_times'  => [],
                'slots'           => $slots,
            ];
        });
    }

    private function medStatus(VaMedicationAdministration $row): string
    {
        if (in_array($row->status, ['given', 'missed', 'refused', 'rescheduled'], true)) {
            return $row->status === 'given' ? 'done' : 'skipped';
        }
        $diffMin = Carbon::now()->diffInMinutes($row->due_at, false);
        if ($diffMin >= self::DUE_WINDOW_MIN) return 'pending';
        if ($diffMin >= -self::LATE_WINDOW_MIN) return 'due';
        return 'missed';
    }

    /**
     * Resolve the list of clock-time strings ("HH:MM") for one care-plan row.
     * Explicit schedule_times wins; otherwise evenly distribute times_per_day
     * starting at 06:00.
     */
    public function slotTimesFor(VaAdmissionCarePlan $plan): array
    {
        $explicit = is_array($plan->schedule_times) ? array_values($plan->schedule_times) : [];
        if (!empty($explicit)) {
            return array_map(fn($t) => substr((string) $t, 0, 5), $explicit);
        }

        $n = max(1, (int) $plan->times_per_day);
        $start = 6 * 60;                        // 06:00 in minutes
        $step  = (int) floor((24 * 60) / $n);
        $out = [];
        for ($i = 0; $i < $n; $i++) {
            $m = ($start + $i * $step) % (24 * 60);
            $out[] = sprintf('%02d:%02d', intdiv($m, 60), $m % 60);
        }
        return $out;
    }

    /**
     * The current status of one specific slot, useful for cheap quick-log
     * responses without re-running the whole matrix.
     */
    public function statusForSlot(VaAdmission $admission, int $planId, string $date, int $slotIndex): string
    {
        $plan = VaAdmissionCarePlan::where('admission_id', $admission->id)->find($planId);
        if (!$plan) return 'pending';

        $log = VaCareLog::where('care_plan_id', $planId)
            ->whereDate('care_date', $date)
            ->where('slot_index', $slotIndex)
            ->first();
        if ($log) {
            return $log->status === 'skipped' ? 'skipped' : 'done';
        }

        $slotTimes = $this->slotTimesFor($plan);
        $clock = $slotTimes[$slotIndex] ?? '12:00';
        $dueAt = Carbon::parse($date)->setTimeFromTimeString($clock);
        $diffMin = Carbon::now()->diffInMinutes($dueAt, false);

        if ($diffMin >= self::DUE_WINDOW_MIN)  return 'pending';
        if ($diffMin >= -self::LATE_WINDOW_MIN) return 'due';
        return 'missed';
    }
}
