<?php

namespace Modules\ClinicLab\Entities\Services;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\ClinicLab\Entities\CllResultReview;
use Modules\ClinicLab\Entities\CllTestCatalog;
use Modules\ClinicLab\Entities\CllTestCatalogAnalyte;
use Modules\LabCore\Entities\LabTestSession;

/**
 * Validation workflow over LabCore analyzer sessions (SIFA P4-T4).
 *
 * LabCore drops results straight into lab_test_sessions/lab_test_results with no
 * human check. This service adds the tech→verify layer:
 *
 *   listing → lazy-create a pending_review row per session
 *   review  → a technician has eyeballed the run
 *   verify  → apply catalog critical bounds; on ANY critical analyte fire a
 *             MedicalAlertService 'critical_value' alert (idempotent per session,
 *             patient-linked when clc_patient_id present) + a MedNotify
 *             'lab.result.ready' to the patient contact; then write the result
 *             back onto the linked clc_order_item (status resulted, abnormal_flag,
 *             result_summary) — closing the ClinicCore Wave C order loop.
 *
 * Every cross-module call is fail-soft: a missing/erroring ClinicCore or
 * MedNotify never blocks a lab tech from verifying a result.
 */
class ResultReviewService
{
    /**
     * Ensure a review row exists for each of the given sessions (lazy create on
     * listing). Returns a map of test_session_id => CllResultReview.
     */
    public function ensureReviews(int $businessId, $sessions): array
    {
        $map = [];
        foreach ($sessions as $session) {
            $map[$session->id] = CllResultReview::firstOrCreate(
                ['business_id' => $businessId, 'test_session_id' => $session->id],
                ['status' => 'pending_review']
            );
        }
        return $map;
    }

    /** Mark a session reviewed (technician sign-off before verification). */
    public function review(CllResultReview $review, ?int $userId = null): CllResultReview
    {
        // Never walk a verified/corrected row backwards.
        if (in_array($review->status, ['verified', 'corrected'], true)) {
            return $review;
        }
        $review->update([
            'status'      => 'reviewed',
            'reviewed_by' => $userId ?? auth()->id(),
            'reviewed_at' => now(),
        ]);
        return $review;
    }

    /**
     * Verify a session: run critical-bound evaluation, raise a critical alert +
     * notify + write results back to the order item. Idempotent — re-verifying
     * refreshes the same alert (dedupe on session) rather than duplicating.
     */
    public function verify(CllResultReview $review, ?int $userId = null): CllResultReview
    {
        $session = LabTestSession::with('results')->find($review->test_session_id);
        if (! $session) {
            return $review;
        }

        $review->update([
            'status'      => 'verified',
            'verified_by' => $userId ?? auth()->id(),
            'verified_at' => now(),
            // If a technician never explicitly reviewed, stamp that too.
            'reviewed_by' => $review->reviewed_by ?: ($userId ?? auth()->id()),
            'reviewed_at' => $review->reviewed_at ?: now(),
        ]);

        $eval = $this->evaluate($review->business_id, $session);

        if ($eval['has_critical']) {
            $this->raiseCriticalAlert($review->business_id, $session, $eval);
        }

        $this->notifyResultReady($review->business_id, $session, $eval);
        $this->writeBackOrderItem($review->business_id, $session, $eval);

        return $review;
    }

    /**
     * Evaluate a session's analytes against the catalog critical/reference
     * bounds, falling back to the analyzer's own ref range + abnormal roll-up.
     *
     * @return array{has_critical:bool, worst_flag:string, critical:array, abnormal:bool, summary:string}
     */
    public function evaluate(int $businessId, LabTestSession $session): array
    {
        $catalogAnalytes = $this->catalogAnalytesFor($businessId, $session);

        $critical    = [];
        $anyAbnormal = false;
        $worstFlag   = 'normal';
        $summaryBits = [];

        foreach ($session->results as $r) {
            $numeric = is_numeric($r->value) ? (float) $r->value : null;
            $flag    = 'normal';

            $def = $catalogAnalytes[strtoupper((string) $r->analyte_code)]
                ?? $catalogAnalytes[strtoupper((string) $r->base_name)]
                ?? null;

            // 1) Catalog critical bounds (authoritative when defined).
            if ($def && $numeric !== null) {
                if ($def->critical_low !== null && $numeric < (float) $def->critical_low) {
                    $flag = 'critical_low';
                } elseif ($def->critical_high !== null && $numeric > (float) $def->critical_high) {
                    $flag = 'critical_high';
                } elseif ($def->ref_low !== null && $numeric < (float) $def->ref_low) {
                    $flag = 'low';
                } elseif ($def->ref_high !== null && $numeric > (float) $def->ref_high) {
                    $flag = 'high';
                }
            }

            // 2) Fall back to the analyzer's own ref range when no catalog def.
            if ($flag === 'normal' && $def === null && $numeric !== null) {
                $lo = is_numeric($r->ref_lower) ? (float) $r->ref_lower : null;
                $hi = is_numeric($r->ref_upper) ? (float) $r->ref_upper : null;
                if ($lo !== null && $numeric < $lo) {
                    $flag = 'low';
                } elseif ($hi !== null && $numeric > $hi) {
                    $flag = 'high';
                }
            }

            // 3) Final fallback to the analyzer's normalized abnormal roll-up.
            if ($flag === 'normal' && $r->is_abnormal) {
                $flag = 'high'; // direction unknown → treat as a generic abnormal
            }

            if (in_array($flag, ['critical_low', 'critical_high'], true)) {
                $critical[] = [
                    'name'  => $r->analyte_name ?: $r->analyte_code,
                    'value' => $r->value,
                    'unit'  => $r->unit,
                    'flag'  => $flag,
                ];
                $summaryBits[] = ($r->analyte_name ?: $r->analyte_code) . ' ' . $r->value
                    . ' (' . str_replace('_', ' ', $flag) . ')';
            }

            if ($flag !== 'normal') {
                $anyAbnormal = true;
                $worstFlag = $this->worseFlag($worstFlag, $flag);
            }
        }

        if (empty($summaryBits)) {
            $summaryBits[] = $anyAbnormal
                ? 'Abnormal result(s) present.'
                : 'All analytes within range.';
        }

        return [
            'has_critical' => ! empty($critical),
            'worst_flag'   => $worstFlag,
            'critical'     => $critical,
            'abnormal'     => $anyAbnormal,
            'summary'      => mb_substr(implode('; ', $summaryBits), 0, 600),
        ];
    }

    /**
     * Build a code => analyte-def map for a session by matching the ordered
     * catalog test on the worklist tests / order item. Keyed by UPPER code.
     *
     * @return array<string,CllTestCatalogAnalyte>
     */
    protected function catalogAnalytesFor(int $businessId, LabTestSession $session): array
    {
        // Gather candidate catalog codes from the session's own tests hint and
        // any linked worklist row. This stays a best-effort match — analytes not
        // in the catalog simply fall back to analyzer ranges.
        $codes = collect();

        // The worklist that produced this sample carries the ordered test codes.
        try {
            if (Schema::hasTable('lab_worklist')) {
                $tests = DB::table('lab_worklist')
                    ->where('business_id', $businessId)
                    ->where('sample_no', $session->sample_no)
                    ->value('tests');
                if ($tests) {
                    $decoded = json_decode($tests, true);
                    if (is_array($decoded)) {
                        $codes = $codes->merge($decoded);
                    }
                }
            }
        } catch (\Throwable $e) {
            // best-effort
        }

        $query = CllTestCatalogAnalyte::query()
            ->join('cll_test_catalog', 'cll_test_catalog.id', '=', 'cll_test_catalog_analytes.catalog_id')
            ->where('cll_test_catalog.business_id', $businessId);

        if ($codes->isNotEmpty()) {
            $query->where(function ($q) use ($codes) {
                $q->whereIn('cll_test_catalog.code', $codes->all())
                  ->orWhereIn('cll_test_catalog.name', $codes->all());
            });
        }

        $defs = $query->select('cll_test_catalog_analytes.*')->get();

        $map = [];
        foreach ($defs as $def) {
            $map[strtoupper((string) $def->analyte_code)] = $def;
        }
        return $map;
    }

    protected function worseFlag(string $current, string $candidate): string
    {
        $rank = ['normal' => 0, 'low' => 1, 'high' => 1, 'critical_low' => 2, 'critical_high' => 2];
        return ($rank[$candidate] ?? 0) > ($rank[$current] ?? 0) ? $candidate : $current;
    }

    /**
     * Raise a critical-value medical alert via ClinicCore's Wave A service.
     * Idempotent per session (source_type/source_id dedupe). Fail-soft.
     */
    protected function raiseCriticalAlert(int $businessId, LabTestSession $session, array $eval): void
    {
        try {
            $patientId = $session->clc_patient_id ?? null;
            if (empty($patientId)) {
                return; // Only patient-linked sessions produce a patient alert.
            }
            if (! class_exists(\Modules\ClinicCore\Entities\Services\MedicalAlertService::class)) {
                return;
            }

            $lines = array_map(function ($c) {
                return $c['name'] . ' ' . $c['value'] . ' ' . ($c['unit'] ?? '')
                    . ' (' . str_replace('_', ' ', $c['flag']) . ')';
            }, $eval['critical']);

            app(\Modules\ClinicCore\Entities\Services\MedicalAlertService::class)->raise([
                'business_id' => $businessId,
                'patient_id'  => (int) $patientId,
                'alert_type'  => 'critical_value',
                'severity'    => 'critical',
                'title'       => 'Critical lab value',
                'details'     => 'Sample ' . $session->sample_no . ': ' . implode('; ', $lines),
                // Idempotency: one alert per analyzer session.
                'source_type' => 'cll_lab_session',
                'source_id'   => (int) $session->id,
            ]);
        } catch (\Throwable $e) {
            \Log::warning('ClinicLab critical-value alert skipped: ' . $e->getMessage());
        }
    }

    /**
     * Notify the patient contact (and ordering context) that a result is ready.
     * Uses the MedNotify 'lab.result.ready' event (seeded). Fail-soft.
     */
    protected function notifyResultReady(int $businessId, LabTestSession $session, array $eval): void
    {
        try {
            $mu = app(\App\Utils\ModuleUtil::class);
            if (! $mu->isModuleInstalled('MedNotify')) {
                return;
            }

            $contactId = $session->contact_id ?? null;
            $contacts  = $contactId ? [(int) $contactId] : [];

            app(\Modules\MedNotify\Services\MedNotify::class)->notify('lab.result.ready', [
                'business_id' => $businessId,
                'contacts'    => $contacts,
                'variables'   => [
                    'patient_name' => $session->patient_name ?: ('#' . ($session->clc_patient_id ?? '')),
                    'test_name'    => is_array($session->tests ?? null) ? implode(', ', $session->tests) : ($session->sample_type ?: 'Lab result'),
                    'order_no'     => $session->sample_no,
                    'link'         => '',
                ],
            ]);
        } catch (\Throwable $e) {
            \Log::warning('ClinicLab lab.result.ready notify skipped: ' . $e->getMessage());
        }
    }

    /**
     * Close the Wave C loop: update the linked clc_order_item (status resulted,
     * abnormal_flag, result_summary) via a guarded raw update. We find the item
     * either through the specimen linkage or the worklist that carried this
     * sample_no (OrderRoutingService stored downstream_id = lab_worklist.id).
     * Fully guarded + fail-soft: no ClinicCore tables → silent no-op.
     */
    protected function writeBackOrderItem(int $businessId, LabTestSession $session, array $eval): void
    {
        try {
            if (! Schema::hasTable('clc_order_items')) {
                return;
            }

            $orderItemId = $this->resolveOrderItemId($businessId, $session);
            if (empty($orderItemId)) {
                return;
            }

            $abnormalFlag = $eval['worst_flag'] === 'normal' ? 'normal' : $eval['worst_flag'];

            $update = ['status' => 'resulted', 'updated_at' => now()];
            if (Schema::hasColumn('clc_order_items', 'abnormal_flag')) {
                $update['abnormal_flag'] = $abnormalFlag;
            }
            if (Schema::hasColumn('clc_order_items', 'result_summary')) {
                $update['result_summary'] = $eval['summary'];
            }

            DB::table('clc_order_items')->where('id', $orderItemId)->update($update);
        } catch (\Throwable $e) {
            \Log::warning('ClinicLab order write-back skipped: ' . $e->getMessage());
        }
    }

    /**
     * Find the clc_order_items row this session belongs to. Priority:
     *   1) an explicit cll_specimens.clc_order_item_id for this sample
     *   2) clc_order_items where downstream_type=lab_worklist and downstream_id
     *      = the lab_worklist row matching this sample_no.
     */
    protected function resolveOrderItemId(int $businessId, LabTestSession $session): ?int
    {
        // 1) Via a specimen we generated for this sample.
        if (Schema::hasTable('cll_specimens')) {
            $viaSpecimen = DB::table('cll_specimens')
                ->where('business_id', $businessId)
                ->where(function ($q) use ($session) {
                    $q->where('specimen_no', $session->sample_no);
                    // also match specimens whose worklist carried this sample
                })
                ->whereNotNull('clc_order_item_id')
                ->value('clc_order_item_id');
            if ($viaSpecimen) {
                return (int) $viaSpecimen;
            }
        }

        // 2) Via the worklist row → order item downstream linkage.
        if (Schema::hasTable('lab_worklist') && Schema::hasColumn('clc_order_items', 'downstream_type')) {
            $worklistId = DB::table('lab_worklist')
                ->where('business_id', $businessId)
                ->where('sample_no', $session->sample_no)
                ->value('id');

            if ($worklistId) {
                $itemId = DB::table('clc_order_items')
                    ->where('downstream_type', 'lab_worklist')
                    ->where('downstream_id', $worklistId)
                    ->value('id');
                if ($itemId) {
                    return (int) $itemId;
                }
            }
        }

        return null;
    }
}
