<?php

namespace Modules\NX600Lab\Entities\Services;

use Modules\NX600Lab\Entities\NxTestResult;
use Modules\NX600Lab\Entities\NxTestSession;

/**
 * Builds the per-pet lab-trend dataset:
 *  - chronological list of sessions
 *  - for each parameter (grouped by base_name), a time-series of numeric
 *    readings plus current vs previous vs first deltas and trend direction.
 *
 * Used by both the full Lab Trends page and the appointment Lab tab sparklines.
 */
class NxLabTrendsService
{
    /**
     * @return array{
     *   pet_id: int, sessions: array, parameters: array
     * }
     */
    public static function forPet(int $petId, int $businessId): array
    {
        $sessions = NxTestSession::where('business_id', $businessId)
            ->where('vc_pet_id', $petId)
            ->orderBy('test_date')->orderBy('test_time')
            ->get(['id', 'sample_no', 'test_date', 'test_time']);

        if ($sessions->isEmpty()) {
            return ['pet_id' => $petId, 'sessions' => [], 'parameters' => []];
        }

        $sessionIds = $sessions->pluck('id')->all();

        $results = NxTestResult::whereIn('test_session_id', $sessionIds)
            ->orderBy('test_session_id')
            ->get(['test_session_id', 'base_name', 'test_name', 'result', 'unit',
                   'ref_lower', 'ref_upper', 'is_abnormal']);

        // session_id => index, so we can place points on a chronological axis
        $sessionOrder = $sessions->pluck('id')->flip();
        $sessionDates = $sessions->mapWithKeys(fn($s) => [
            $s->id => optional($s->test_date)->format('Y-m-d'),
        ]);

        // Group results by canonical parameter name (base_name preferred).
        $byParam = [];
        foreach ($results as $r) {
            $key = trim($r->base_name) !== '' ? $r->base_name : $r->test_name;
            if (!isset($byParam[$key])) {
                $byParam[$key] = [
                    'name'       => $key,
                    'full_name'  => $r->test_name,
                    'unit'       => $r->unit,
                    'ref_lower'  => self::numOrNull($r->ref_lower),
                    'ref_upper'  => self::numOrNull($r->ref_upper),
                    'points'     => [],
                ];
            }
            // Keep most recent reference range (overwrites earlier nulls).
            if ($r->ref_lower !== null && $r->ref_lower !== '') {
                $byParam[$key]['ref_lower'] = self::numOrNull($r->ref_lower);
            }
            if ($r->ref_upper !== null && $r->ref_upper !== '') {
                $byParam[$key]['ref_upper'] = self::numOrNull($r->ref_upper);
            }
            if ($r->unit) $byParam[$key]['unit'] = $r->unit;

            $value = self::numOrNull($r->result);
            if ($value === null) continue; // skip non-numeric results (e.g. "POS")

            $byParam[$key]['points'][] = [
                'session_id'  => $r->test_session_id,
                'date'        => $sessionDates[$r->test_session_id] ?? null,
                'order'       => $sessionOrder[$r->test_session_id] ?? null,
                'value'       => $value,
                'is_abnormal' => (bool) $r->is_abnormal,
            ];
        }

        // Per parameter, dedupe (keep last per session), sort, compute deltas.
        foreach ($byParam as $k => &$p) {
            // If the same parameter appears multiple times in one session
            // (with different suffixes), keep the latest one per session.
            $bySession = [];
            foreach ($p['points'] as $pt) {
                $bySession[$pt['session_id']] = $pt;
            }
            $points = array_values($bySession);
            usort($points, fn($a, $b) => $a['order'] <=> $b['order']);

            if (count($points) < 1) {
                unset($byParam[$k]);
                continue;
            }

            $values = array_column($points, 'value');
            $latest   = end($values);
            $first    = reset($values);
            $previous = count($values) >= 2 ? $values[count($values) - 2] : null;

            $p['points']      = $points;
            $p['count']       = count($points);
            $p['latest']      = $latest;
            $p['first']       = $first;
            $p['previous']    = $previous;
            $p['delta_prev']  = $previous !== null ? round($latest - $previous, 3) : null;
            $p['delta_first'] = round($latest - $first, 3);
            $p['direction']   = self::direction($previous, $latest);
            $p['in_range']    = self::inRange($latest, $p['ref_lower'], $p['ref_upper']);
        }
        unset($p);

        // Sort parameters alphabetically by base name for stable rendering.
        ksort($byParam);

        return [
            'pet_id'     => $petId,
            'sessions'   => $sessions->map(fn($s) => [
                'id'         => $s->id,
                'sample_no'  => $s->sample_no,
                'date'       => optional($s->test_date)->format('Y-m-d'),
                'date_label' => optional($s->test_date)->format('d M Y'),
                'time'       => $s->test_time,
            ])->all(),
            'parameters' => $byParam,
        ];
    }

    private static function numOrNull($v): ?float
    {
        if ($v === null || $v === '') return null;
        // Strip trailing punctuation/letters; analyzer sometimes returns "143." or ">200"
        $v = trim((string) $v);
        if ($v === '') return null;
        $v = ltrim($v, '<>=');
        if (!is_numeric($v) && !preg_match('/^-?\d+(?:\.\d+)?/', $v, $m)) {
            return null;
        }
        return is_numeric($v) ? (float) $v : (float) $m[0];
    }

    private static function direction(?float $prev, ?float $curr): string
    {
        if ($prev === null || $curr === null) return 'stable';
        $diff = $curr - $prev;
        $base = abs($prev) > 0.01 ? abs($prev) : 1.0;
        $pct  = abs($diff) / $base;
        if ($pct < 0.03) return 'stable';
        return $diff > 0 ? 'up' : 'down';
    }

    private static function inRange(?float $value, ?float $lo, ?float $hi): ?bool
    {
        if ($value === null) return null;
        if ($lo !== null && $value < $lo) return false;
        if ($hi !== null && $value > $hi) return false;
        if ($lo === null && $hi === null) return null;
        return true;
    }
}
