<?php

namespace Modules\AbaxisLab\Console;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Modules\LabCore\Entities\LabAgent;

/**
 * Abaxis VetScan device simulator.
 *
 * Generates a realistic ASTM E1394 message and delivers it one of three ways:
 *   --mode=print : print the message (parser/debug aid)
 *   --mode=api   : POST it to the cloud LabCore agent API, as a LabBridge
 *                  Agent would (auto-provisions a "simulator" agent)
 *   --mode=tcp   : act as a real analyzer — connect to a TCP host and run the
 *                  ASTM E1381 ENQ/ACK/STX handshake (pair with the LabBridge
 *                  Agent's listener once Phase 3 is built)
 *
 * Run with: php artisan abaxis:simulate
 */
class AbaxisSimulator extends Command
{
    protected $signature = 'abaxis:simulate
                            {--mode=print : print | api | tcp}
                            {--scenario=chemistry : chemistry | hematology | abnormal}
                            {--sample= : Sample number (auto-generated if omitted)}
                            {--patient=Buddy : Patient name}
                            {--species=Canine : Patient species}
                            {--sex=M : Patient sex (M/F/U)}
                            {--model=VS2 : VetScan model in the H record}
                            {--host=127.0.0.1 : TCP host (mode=tcp)}
                            {--port=9600 : TCP port (mode=tcp)}
                            {--url= : Cloud base URL (mode=api); defaults to config(app.url)}
                            {--token= : Agent API token (mode=api); auto-provisioned if omitted}';

    protected $description = 'Simulate an Abaxis VetScan analyzer (ASTM E1394) for testing';

    private const STX = "\x02";
    private const ETX = "\x03";
    private const EOT = "\x04";
    private const ENQ = "\x05";
    private const ACK = "\x06";
    private const CR  = "\x0D";
    private const LF  = "\x0A";

    public function handle(): int
    {
        $message = $this->buildMessage();

        return match ($this->option('mode')) {
            'api'   => $this->sendApi($message),
            'tcp'   => $this->sendTcp($message),
            default => $this->printMessage($message),
        };
    }

    /**
     * Build a full ASTM E1394 message (records joined by CR).
     */
    private function buildMessage(): string
    {
        $sample   = $this->option('sample') ?: 'SIM' . random_int(10000, 99999);
        $patient  = $this->option('patient');
        $species  = $this->option('species');
        $sex      = strtoupper($this->option('sex'));
        $model    = $this->option('model');
        $scenario = $this->option('scenario');
        $ts       = now()->format('YmdHis');

        $analytes = $this->analytes($scenario);
        $panel    = $scenario === 'hematology' ? 'CBC' : 'CHEM';

        $records = [];
        // H — message header, declaring delimiters \^&
        $records[] = "H|\\^&|||VetScan {$model}^1.0|||||||P|1|{$ts}";
        // P — patient: P.3 id, P.6 name, P.8 dob, P.9 sex, P.10 species
        $records[] = "P|1|{$sample}|||{$patient}||20200101|{$sex}|{$species}";
        // O — order: O.3 specimen id, O.5 universal test id
        $records[] = "O|1|{$sample}||^^^{$panel}|R|{$ts}";
        // R — one per analyte
        $seq = 1;
        foreach ($analytes as $a) {
            $records[] = "R|{$seq}|^^^{$a['code']}|{$a['value']}|{$a['unit']}|{$a['lo']}^{$a['hi']}|{$a['flag']}||F||{$ts}";
            $seq++;
        }
        // L — terminator
        $records[] = "L|1|N";

        return implode(self::CR, $records);
    }

    /**
     * Analyte panels with reference ranges and abnormal flags.
     *
     * @return array<int,array{code:string,value:string,unit:string,lo:string,hi:string,flag:string}>
     */
    private function analytes(string $scenario): array
    {
        $chemistry = [
            ['GLU',  '95.2', 'mg/dL', '70',  '143', ''],
            ['BUN',  '18.4', 'mg/dL', '7',   '27',  ''],
            ['CRE',  '0.9',  'mg/dL', '0.3', '1.2', ''],
            ['TP',   '6.8',  'g/dL',  '5.2', '8.2', ''],
            ['ALB',  '3.2',  'g/dL',  '2.2', '3.9', ''],
            ['ALT',  '45',   'U/L',   '10',  '100', ''],
            ['ALP',  '88',   'U/L',   '23',  '212', ''],
            ['Ca',   '9.8',  'mg/dL', '7.9', '12',  ''],
        ];

        $abnormal = [
            ['GLU',  '284',  'mg/dL', '70',  '143', 'H'],
            ['BUN',  '65',   'mg/dL', '7',   '27',  'H'],
            ['CRE',  '3.6',  'mg/dL', '0.3', '1.2', 'H'],
            ['TP',   '4.1',  'g/dL',  '5.2', '8.2', 'L'],
            ['ALB',  '1.8',  'g/dL',  '2.2', '3.9', 'L'],
            ['ALT',  '256',  'U/L',   '10',  '100', 'H'],
            ['ALP',  '88',   'U/L',   '23',  '212', ''],
            ['Ca',   '9.8',  'mg/dL', '7.9', '12',  ''],
        ];

        $hematology = [
            ['WBC',  '11.2', '10^9/L',  '6',   '17',  ''],
            ['RBC',  '6.8',  '10^12/L', '5.5', '8.5', ''],
            ['HGB',  '15.2', 'g/dL',    '12',  '18',  ''],
            ['HCT',  '45',   '%',       '37',  '55',  ''],
            ['PLT',  '320',  '10^9/L',  '200', '500', ''],
        ];

        $set = match ($scenario) {
            'hematology' => $hematology,
            'abnormal'   => $abnormal,
            default      => $chemistry,
        };

        return array_map(fn($a) => [
            'code' => $a[0], 'value' => $a[1], 'unit' => $a[2],
            'lo'   => $a[3], 'hi'    => $a[4], 'flag' => $a[5],
        ], $set);
    }

    private function printMessage(string $message): int
    {
        $this->info('Abaxis VetScan — simulated ASTM E1394 message');
        $this->line(str_repeat('-', 60));
        // Show CR-separated records on their own lines.
        foreach (explode(self::CR, $message) as $record) {
            $this->line('  ' . $record);
        }
        $this->line(str_repeat('-', 60));
        $this->info(strlen($message) . ' bytes, ' . substr_count($message, self::CR) . ' record separators');
        return self::SUCCESS;
    }

    private function sendApi(string $message): int
    {
        $base  = rtrim($this->option('url') ?: (string) config('app.url'), '/');
        $token = $this->option('token') ?: $this->simulatorAgentToken();
        $uuid  = (string) Str::uuid();

        $this->info("POST {$base}/api/lab/results  (uuid {$uuid})");

        $response = Http::withToken($token)
            ->acceptJson()
            ->post("{$base}/api/lab/results", [
                'uuid'        => $uuid,
                'vendor'      => 'abaxis',
                'raw_message' => $message,
                'received_at' => now()->toIso8601String(),
            ]);

        $this->line('  HTTP ' . $response->status());
        $this->line('  ' . $response->body());

        return $response->successful() ? self::SUCCESS : self::FAILURE;
    }

    /**
     * Auto-provision (or reuse) a "simulator" LabBridge agent and return its
     * token. The agent is attached to the first business so ingested sessions
     * are visible in the business-scoped UI.
     */
    private function simulatorAgentToken(): string
    {
        $businessId = \App\Business::orderBy('id')->value('id');

        $agent = LabAgent::firstOrCreate(
            ['code' => 'simulator'],
            ['name' => 'Abaxis Simulator', 'api_token' => LabAgent::freshToken(), 'status' => 'active']
        );
        if (!$agent->business_id && $businessId) {
            $agent->update(['business_id' => $businessId]);
        }

        $this->line("  Using simulator agent #{$agent->id} ({$agent->code}), business #{$agent->business_id}");
        return $agent->api_token;
    }

    /**
     * Connect as a real analyzer and transmit via ASTM E1381 (ENQ/ACK/STX).
     */
    private function sendTcp(string $message): int
    {
        $host = $this->option('host');
        $port = (int) $this->option('port');

        $this->info("Connecting to {$host}:{$port} (ASTM E1381)...");
        $sock = @fsockopen($host, $port, $errno, $errstr, 5);
        if (!$sock) {
            $this->error("  Connection failed: {$errstr} ({$errno})");
            $this->warn('  The LabBridge Agent (Phase 3) must be listening for mode=tcp.');
            return self::FAILURE;
        }
        stream_set_timeout($sock, 5);

        // Establishment phase.
        fwrite($sock, self::ENQ);
        if (fread($sock, 1) !== self::ACK) {
            $this->error('  No ACK to ENQ — aborting.');
            fclose($sock);
            return self::FAILURE;
        }

        // One ASTM frame per record.
        $fn = 1;
        foreach (explode(self::CR, $message) as $record) {
            $frame = $this->buildFrame($fn, $record . self::CR);
            fwrite($sock, $frame);
            $ack = fread($sock, 1);
            if ($ack !== self::ACK) {
                $this->error("  Frame {$fn} not ACKed — aborting.");
                fclose($sock);
                return self::FAILURE;
            }
            $this->line("  Frame {$fn} sent + ACKed");
            $fn = $fn >= 7 ? 0 : $fn + 1;
        }

        fwrite($sock, self::EOT);
        fclose($sock);
        $this->info('  Transmission complete (EOT sent).');
        return self::SUCCESS;
    }

    /**
     * Build an ASTM E1381 frame: STX FN text ETX C1 C2 CR LF.
     * Checksum = sum of bytes from FN through ETX inclusive, mod 256, hex.
     */
    private function buildFrame(int $fn, string $text): string
    {
        $body     = $fn . $text . self::ETX;
        $checksum = 0;
        for ($i = 0; $i < strlen($body); $i++) {
            $checksum += ord($body[$i]);
        }
        $checksum %= 256;
        return self::STX . $body . strtoupper(str_pad(dechex($checksum), 2, '0', STR_PAD_LEFT)) . self::CR . self::LF;
    }
}
