<?php

namespace Modules\IdexxLab\Entities\Drivers;

use Modules\IdexxLab\Entities\IdexxModelCatalog;
use Modules\LabCore\Entities\Contracts\LabDeviceDriver;
use Modules\LabCore\Entities\Services\Astm\AstmRecordParser;

/**
 * IDEXX VetLab analyzer driver for the LabCore engine.
 *
 * In-clinic IDEXX instruments (Catalyst, ProCyte, SediVue, Coag Dx …) report
 * through the IDEXX VetLab Station. When the station is bridged to a practice
 * management system / LIS it transmits ASTM E1394 (LIS2-A2) — the path this
 * driver parses, via LabCore's shared {@see AstmRecordParser}. (The station
 * can also emit HL7; that variant is declared in commProtocols() and handled
 * by the LabBridge Agent / a future HL7 parser.)
 *
 * IDEXX-specific note: IDEXX SDMA (a renal biomarker) is included in the
 * analyte catalog — it is one of the most clinically useful IDEXX-only results.
 *
 * Field positions follow ASTM E1394; confirm against a real VetLab Station
 * capture before field deployment (some firmware places species/breed in
 * vendor-specific manufacturer records).
 */
class IdexxDriver implements LabDeviceDriver
{
    public function vendor(): string
    {
        return 'idexx';
    }

    public function label(): string
    {
        return 'IDEXX VetLab';
    }

    public function models(): array
    {
        return IdexxModelCatalog::labels();
    }

    public function commProtocols(): array
    {
        // ASTM E1394/LIS2-A2 is implemented; the VetLab Station can also be
        // configured for HL7, normalized by the agent.
        return ['astm-e1394', 'idexx-vetlab-hl7'];
    }

    public function connectionTypes(): array
    {
        return ['tcp', 'serial', 'serial2eth'];
    }

    /**
     * IDEXX analyte catalog (Catalyst chemistry + electrolytes + ProCyte
     * hematology). Reference ranges are left null — they are species-specific
     * and the analyzer transmits them per result; per-business overrides can be
     * added in lab_analyte_map.
     */
    public function defaultAnalyteMap(): array
    {
        $chemistry = [
            ['GLU',  'Glucose', 'mg/dL'],
            ['BUN',  'Urea Nitrogen', 'mg/dL'],
            ['CREA', 'Creatinine', 'mg/dL'],
            ['SDMA', 'Symmetric Dimethylarginine (IDEXX SDMA)', 'µg/dL'],
            ['PHOS', 'Phosphorus', 'mg/dL'],
            ['TP',   'Total Protein', 'g/dL'],
            ['ALB',  'Albumin', 'g/dL'],
            ['GLOB', 'Globulin', 'g/dL'],
            ['ALT',  'Alanine Aminotransferase', 'U/L'],
            ['ALKP', 'Alkaline Phosphatase', 'U/L'],
            ['GGT',  'Gamma-Glutamyl Transferase', 'U/L'],
            ['TBIL', 'Total Bilirubin', 'mg/dL'],
            ['CHOL', 'Cholesterol', 'mg/dL'],
            ['AMYL', 'Amylase', 'U/L'],
            ['LIPA', 'Lipase', 'U/L'],
            ['Ca',   'Calcium', 'mg/dL'],
            ['TT4',  'Total Thyroxine', 'µg/dL'],
        ];

        $electrolytes = [
            ['Na',   'Sodium', 'mmol/L'],
            ['K',    'Potassium', 'mmol/L'],
            ['Cl',   'Chloride', 'mmol/L'],
            ['tCO2', 'Total CO2', 'mmol/L'],
        ];

        $hematology = [
            ['WBC',  'White Blood Cells', '10^9/L'],
            ['RBC',  'Red Blood Cells', '10^12/L'],
            ['HGB',  'Hemoglobin', 'g/dL'],
            ['HCT',  'Hematocrit', '%'],
            ['MCV',  'Mean Corpuscular Volume', 'fL'],
            ['MCH',  'Mean Corpuscular Hemoglobin', 'pg'],
            ['MCHC', 'Mean Corpuscular Hemoglobin Concentration', 'g/dL'],
            ['RDW',  'Red Cell Distribution Width', '%'],
            ['RETIC','Reticulocytes', 'K/µL'],
            ['PLT',  'Platelets', '10^9/L'],
            ['MPV',  'Mean Platelet Volume', 'fL'],
            ['NEU',  'Neutrophils', '10^9/L'],
            ['LYM',  'Lymphocytes', '10^9/L'],
            ['MONO', 'Monocytes', '10^9/L'],
            ['EOS',  'Eosinophils', '10^9/L'],
            ['BASO', 'Basophils', '10^9/L'],
        ];

        $rows = [];
        $order = 0;
        foreach ([['chemistry', $chemistry], ['electrolytes', $electrolytes], ['hematology', $hematology]] as [$category, $set]) {
            foreach ($set as [$code, $name, $unit]) {
                $rows[] = [
                    'analyte_code'   => $code,
                    'canonical_name' => $name,
                    'display_name'   => $name,
                    'unit'           => $unit,
                    'species'        => null,
                    'ref_lower'      => null,
                    'ref_upper'      => null,
                    'category'       => $category,
                    'sort_order'     => $order++,
                ];
            }
        }
        return $rows;
    }

    /**
     * Parse a raw ASTM message. Returns null when the message carries no
     * result records (e.g. a worklist query).
     */
    public function parse(string $raw, array $context = []): ?array
    {
        $message = (new AstmRecordParser())->parseMessage($raw);

        $hasResults = collect($message['patients'] ?? [])
            ->flatMap(fn($p) => $p['orders'] ?? [])
            ->contains(fn($o) => !empty($o['results']));

        return $hasResults ? $message : null;
    }

    /**
     * Map a parsed ASTM message onto the canonical LabCore shape.
     */
    public function normalize(array $parsed): array
    {
        $parser  = new AstmRecordParser();
        $header  = $parsed['header'] ?? [];
        $patient = $parsed['patients'][0]['patient'] ?? [];
        $order   = $parsed['patients'][0]['orders'][0]['order'] ?? [];
        $records = $parsed['patients'][0]['orders'][0]['results'] ?? [];

        $f = fn(array $rec, int $n) => AstmRecordParser::field($rec, $n);

        // Result timestamp: prefer R.13 of the first result, then H.14.
        $stamp = $this->splitTimestamp($f($records[0] ?? [], 13) ?: $f($header, 14));

        $session = [
            'vendor'         => 'idexx',
            'sample_no'      => $f($order, 3) ?: $f($order, 4),
            'patient_id'     => $f($patient, 3) ?: null,
            'patient_name'   => $this->cleanName($f($patient, 6)),
            'test_condition' => 'NORMAL',
            'test_date'      => $stamp['date'],
            'test_time'      => $stamp['time'],
            'species'        => $f($patient, 10) ?: null,   // ASTM "race" field — VetLab species (verify per capture)
            'species_code'   => null,
            'sex'            => $this->sexLabel($f($patient, 9)),
            'analyzer_id'    => $f($header, 5) ?: null,
            'source'         => 'agent',
            'meta'           => [
                'astm_sender'  => $f($header, 5),
                'order_panel'  => $f($order, 5),
                'record_count' => count($records),
            ],
        ];

        $results = [];
        foreach ($records as $r) {
            $code = $this->analyteCode($parser, $f($r, 3));
            if ($code === '') {
                continue;
            }
            $flag  = strtoupper($f($r, 7));
            [$lo, $hi] = $this->parseRefRange($f($r, 6));

            $results[] = [
                'analyte_code'    => $code,
                'analyte_name'    => $parser->component($f($r, 3), 5) ?: null,
                'base_name'       => $code,
                'sample_suffix'   => null,
                'equality'        => $this->equalityFromFlag($flag),
                'value'           => $f($r, 4),
                'unit'            => $f($r, 5) ?: null,
                'dilution_factor' => 1,
                'ref_lower'       => $lo,
                'ref_upper'       => $hi,
                'raw_flag'        => $flag ?: null,
                'flags'           => $flag !== '' ? ['astm' => $flag] : [],
                'is_abnormal'     => $this->isAbnormal($flag),
            ];
        }

        return ['session' => $session, 'results' => $results];
    }

    // -----------------------------------------------------------------------

    private function analyteCode(AstmRecordParser $parser, string $universalTestId): string
    {
        if ($universalTestId === '') {
            return '';
        }
        $comps = $parser->components($universalTestId);
        // Standard universal test ID places the code in component 4 (^^^code).
        $code = $comps[3] ?? '';
        if ($code === '') {
            foreach ($comps as $c) {
                if ($c !== '') {
                    $code = $c;
                    break;
                }
            }
        }
        return $code;
    }

    /**
     * @return array{0:?string,1:?string} [lower, upper]
     */
    private function parseRefRange(string $range): array
    {
        if ($range === '') {
            return [null, null];
        }
        if (preg_match_all('/-?\d+\.?\d*/', $range, $m) && count($m[0]) >= 2) {
            return [$m[0][0], $m[0][1]];
        }
        return [null, null];
    }

    private function isAbnormal(string $flag): bool
    {
        return $flag !== '' && !in_array($flag, ['N', 'NORM'], true);
    }

    private function equalityFromFlag(string $flag): string
    {
        return match (true) {
            str_contains($flag, '>') => '>',
            str_contains($flag, '<') => '<',
            default                  => '=',
        };
    }

    private function sexLabel(string $raw): ?string
    {
        $raw = strtoupper(trim($raw));
        return match (true) {
            $raw === ''                => null,
            str_starts_with($raw, 'M') => 'Male',
            str_starts_with($raw, 'F') => 'Female',
            str_starts_with($raw, 'U') => 'Unknown',
            default                    => $raw,
        };
    }

    private function cleanName(string $name): ?string
    {
        $name = trim(str_replace(['^', '\\'], ' ', $name));
        $name = preg_replace('/\s+/', ' ', $name);
        return $name !== '' ? $name : null;
    }

    /**
     * Split an ASTM timestamp (YYYYMMDD[HHMMSS]) into date + time parts.
     *
     * @return array{date:?string,time:?string}
     */
    private function splitTimestamp(string $ts): array
    {
        $ts = preg_replace('/\D/', '', $ts);
        if (strlen($ts) < 8) {
            return ['date' => null, 'time' => null];
        }
        $date = substr($ts, 0, 4) . '-' . substr($ts, 4, 2) . '-' . substr($ts, 6, 2);
        $time = strlen($ts) >= 12
            ? substr($ts, 8, 2) . ':' . substr($ts, 10, 2) . ':' . (substr($ts, 12, 2) ?: '00')
            : null;
        return ['date' => $date, 'time' => $time];
    }
}
