<?php

namespace Modules\NX600Lab\Console;

use Illuminate\Console\Command;
use Modules\NX600Lab\Entities\Services\Nx600Protocol;

/**
 * NX600 Device Simulator.
 *
 * Acts as the NX600 analyzer (TCP CLIENT) to test the server without real
 * hardware. Run alongside the server: php artisan nx600lab:simulate
 *
 * The "full" and "loop" scenarios pull the real worklist first and report
 * results under the worklist's own sample number + patient data — exactly
 * as a real analyzer does — so results link back to the worklist (and pet).
 */
class Nx600Simulator extends Command
{
    protected $signature = 'nx600lab:simulate
                            {--host=127.0.0.1 : Server host}
                            {--port= : Server port}
                            {--type=1 : Communication type (1,2,A,B)}
                            {--scenario=menu : Scenario: menu|worklist|result|error|full|loop}
                            {--sample= : Sample number (auto-generated if omitted)}
                            {--patient= : Patient name}
                            {--abnormal : Include abnormal results in test data}
                            {--delay=1 : Seconds between steps}';

    protected $description = 'Simulate the NX600 analyzer device (TCP client) for testing';

    private Nx600Protocol $protocol;
    private bool $hasBcc = true;

    // Sample chemistry tests with reference ranges.
    private array $normalTests = [
        ['name' => 'GLU-P',  'result' => '95.2', 'unit' => 'mg/dL', 'lo' => '70.0', 'hi' => '143.', 'warn' => '           '],
        ['name' => 'BUN-P',  'result' => '18.4', 'unit' => 'mg/dL', 'lo' => '7.00', 'hi' => '27.0', 'warn' => '           '],
        ['name' => 'CRE-P',  'result' => '0.80', 'unit' => 'mg/dL', 'lo' => '0.30', 'hi' => '1.20', 'warn' => '           '],
        ['name' => 'TP-P',   'result' => '6.80', 'unit' => 'g/dL',  'lo' => '5.20', 'hi' => '8.20', 'warn' => '           '],
        ['name' => 'ALB-P',  'result' => '3.20', 'unit' => 'g/dL',  'lo' => '2.20', 'hi' => '3.90', 'warn' => '           '],
        ['name' => 'GOT-P',  'result' => '28.0', 'unit' => 'U/L',   'lo' => '0.00', 'hi' => '50.0', 'warn' => '           '],
        ['name' => 'GPT-P',  'result' => '35.0', 'unit' => 'U/L',   'lo' => '0.00', 'hi' => '50.0', 'warn' => '           '],
        ['name' => 'ALP-P',  'result' => '88.0', 'unit' => 'U/L',   'lo' => '23.0', 'hi' => '212.', 'warn' => '           '],
        ['name' => 'TCHO-P', 'result' => '185.', 'unit' => 'mg/dL', 'lo' => '110.', 'hi' => '320.', 'warn' => '           '],
        ['name' => 'TG-P',   'result' => '95.0', 'unit' => 'mg/dL', 'lo' => '29.0', 'hi' => '291.', 'warn' => '           '],
        ['name' => 'TBIL-P', 'result' => '0.40', 'unit' => 'mg/dL', 'lo' => '0.00', 'hi' => '0.90', 'warn' => '           '],
        ['name' => 'Ca-P',   'result' => '9.80', 'unit' => 'mg/dL', 'lo' => '7.90', 'hi' => '12.0', 'warn' => '           '],
    ];

    private array $abnormalTests = [
        ['name' => 'GLU-P',  'result' => '248.', 'unit' => 'mg/dL', 'lo' => '70.0', 'hi' => '143.', 'warn' => 'H          '],
        ['name' => 'BUN-P',  'result' => '62.0', 'unit' => 'mg/dL', 'lo' => '7.00', 'hi' => '27.0', 'warn' => 'H          '],
        ['name' => 'CRE-P',  'result' => '3.60', 'unit' => 'mg/dL', 'lo' => '0.30', 'hi' => '1.20', 'warn' => 'H          '],
        ['name' => 'TP-P',   'result' => '4.10', 'unit' => 'g/dL',  'lo' => '5.20', 'hi' => '8.20', 'warn' => 'L          '],
        ['name' => 'ALB-P',  'result' => '1.80', 'unit' => 'g/dL',  'lo' => '2.20', 'hi' => '3.90', 'warn' => 'L          '],
        ['name' => 'GOT-P',  'result' => '312.', 'unit' => 'U/L',   'lo' => '0.00', 'hi' => '50.0', 'warn' => 'H          '],
        ['name' => 'GPT-P',  'result' => '189.', 'unit' => 'U/L',   'lo' => '0.00', 'hi' => '50.0', 'warn' => 'H          '],
        ['name' => 'ALP-P',  'result' => '88.0', 'unit' => 'U/L',   'lo' => '23.0', 'hi' => '212.', 'warn' => '           '],
        ['name' => 'TCHO-P', 'result' => '420.', 'unit' => 'mg/dL', 'lo' => '110.', 'hi' => '320.', 'warn' => 'H          '],
        ['name' => 'TG-P',   'result' => '95.0', 'unit' => 'mg/dL', 'lo' => '29.0', 'hi' => '291.', 'warn' => '           '],
        ['name' => 'TBIL-P', 'result' => '4.20', 'unit' => 'mg/dL', 'lo' => '0.00', 'hi' => '0.90', 'warn' => 'H          '],
        ['name' => 'Ca-P',   'result' => '5.10', 'unit' => 'mg/dL', 'lo' => '7.90', 'hi' => '12.0', 'warn' => 'L          '],
    ];

    public function handle(): int
    {
        $this->protocol = new Nx600Protocol();
        $this->hasBcc   = $this->option('type') !== '3';

        $host     = $this->option('host');
        $port     = (int) ($this->option('port') ?: config('nx600lab.tcp_port', 65535));
        $scenario = $this->option('scenario');

        $this->banner();
        $this->info("  Target    : {$host}:{$port}");
        $this->info("  Comm Type : " . $this->option('type'));
        $this->line('');

        if ($scenario === 'menu') {
            $scenario = $this->ask(
                "  Choose scenario:\n" .
                "    [1] worklist  — Send Command I + W (request worklist)\n" .
                "    [2] result    — Send Command S + R (test result)\n" .
                "    [3] error     — Send Command E (device error)\n" .
                "    [4] full      — Worklist -> result (links to the pet)\n" .
                "    [5] loop      — Repeat full flow every N seconds\n" .
                "  Enter choice",
                '4'
            );
            $scenario = match ($scenario) {
                '1' => 'worklist',
                '2' => 'result',
                '3' => 'error',
                '5' => 'loop',
                default => 'full',
            };
        }

        $delay = max(0, (int) $this->option('delay'));

        match ($scenario) {
            'worklist' => $this->scenarioWorklist($host, $port),
            'result'   => $this->scenarioResult($host, $port),
            'error'    => $this->scenarioError($host, $port),
            'loop'     => $this->scenarioLoop($host, $port, $delay),
            default    => $this->scenarioFull($host, $port, $delay),
        };

        return self::SUCCESS;
    }

    // ----- SCENARIOS ---------------------------------------------------------

    /**
     * Request the worklist (Command I) and fetch each entry's sample info
     * (Command W). Returns the parsed worklist entries.
     *
     * @return array<int,array{sample_no:string,patient_id:string,patient_name:string,species:string,sex:string,age:string}>
     */
    private function scenarioWorklist(string $host, int $port): array
    {
        $this->separator('WORKLIST REQUEST (Command I -> W)');

        $this->step('1', 'Connecting to server...');
        $sock = $this->connect($host, $port);
        if (!$sock) return [];

        $this->step('2', 'Sending Command I (worklist index request)');
        $iFrame = $this->buildFrame('I', ['', '10']);
        $this->sendRaw($sock, $iFrame, 'Command I');

        $reply = $this->receiveAll($sock);
        socket_close($sock);

        if (!$reply) {
            $this->error('  No reply received for Command I.');
            return [];
        }

        $parsed = $this->protocol->parseFrame($reply, $this->hasBcc);
        if (!$parsed || ($parsed['command'] ?? null) !== 'I') {
            $this->error('  Invalid I-reply: ' . bin2hex($reply));
            return [];
        }

        $count   = (int) trim($parsed['params'][0] ?? '0');
        $samples = array_map('trim', array_slice($parsed['params'], 1));
        $this->info("  I-reply: {$count} entr" . ($count === 1 ? 'y' : 'ies') . " in worklist");

        if ($count === 0) {
            $this->line('  No pending entries.');
            return [];
        }

        $entries = [];
        foreach ($samples as $i => $sampleNo) {
            if ($sampleNo === '') continue;
            $step = 3 + $i;

            $this->step((string) $step, "Reconnecting for Command W -> sample '{$sampleNo}'");
            $sock = $this->connect($host, $port);
            if (!$sock) return $entries;

            $wFrame = $this->buildFrame('W', [str_pad($sampleNo, 13)]);
            $this->sendRaw($sock, $wFrame, "Command W [{$sampleNo}]");

            $wReply = $this->receiveAll($sock);
            socket_close($sock);

            if (!$wReply) {
                $this->warn("  No W-reply for sample '{$sampleNo}'");
                continue;
            }

            $wParsed = $this->protocol->parseFrame($wReply, $this->hasBcc);
            if (!$wParsed || ($wParsed['command'] ?? null) !== 'W') {
                $this->warn("  Invalid W-reply: " . bin2hex($wReply));
                continue;
            }

            $p = $wParsed['params'];
            $entry = [
                'sample_no'    => trim($p[0] ?? $sampleNo),
                'patient_id'   => trim($p[1] ?? ''),
                'patient_name' => trim($p[2] ?? ''),
                'species'      => trim($p[3] ?? '0'),
                'sex'          => trim($p[4] ?? '9'),
                'age'          => trim($p[5] ?? '999'),
            ];
            $entries[] = $entry;

            $this->info("  W-reply for '{$sampleNo}':");
            $this->line("       Patient ID   : " . ($entry['patient_id'] ?: '(none)'));
            $this->line("       Patient Name : " . ($entry['patient_name'] ?: '(none)'));
            $this->line("       Species code : " . $entry['species']);
            $this->line("       Sex / Age    : " . $entry['sex'] . ' / ' . $entry['age']);
        }

        return $entries;
    }

    /**
     * Send a test result (Command S + R). When $entry is supplied (from the
     * worklist) the result carries that sample number and patient data, so it
     * links back to the worklist entry and its pet.
     */
    private function scenarioResult(string $host, int $port, ?array $entry = null): void
    {
        $sampleNo = $entry['sample_no'] ?? ($this->option('sample') ?: ('SIM' . now()->format('dHis')));
        $abnormal = (bool) $this->option('abnormal');
        $tests    = $abnormal ? $this->abnormalTests : $this->normalTests;

        $info = [
            'patient_id'   => $entry['patient_id']   ?? 'SIM001',
            'patient_name' => $entry['patient_name'] ?? ($this->option('patient') ?: 'SimPatient'),
            'species'      => $entry['species']      ?? '14',
            'sex'          => $entry['sex']          ?? '0',
            'age'          => $entry['age']          ?? '3',
        ];

        $this->separator('TEST RESULT (Command S -> R)');
        $this->line("  Sample  : {$sampleNo}");
        $this->line("  Patient : {$info['patient_name']} [{$info['patient_id']}]");
        $this->line("  Profile : " . ($abnormal ? 'ABNORMAL' : 'Normal'));
        $this->line('');

        $this->step('1', 'Connecting to server...');
        $sock = $this->connect($host, $port);
        if (!$sock) return;

        $this->step('2', 'Sending Command S (test start notification)');
        $sFrame = $this->buildCommandS($sampleNo, $info);
        $this->sendRaw($sock, $sFrame, 'Command S');
        socket_close($sock);
        sleep(1);

        $this->step('3', 'Reconnecting for Command R (test results)...');
        $sock = $this->connect($host, $port);
        if (!$sock) return;

        $this->step('4', 'Sending Command R (' . count($tests) . ' parameters)');
        $rFrame = $this->buildCommandR($sampleNo, $info, $tests);
        $this->sendRaw($sock, $rFrame, 'Command R');
        socket_close($sock);

        $this->info('');
        $this->info('  Test result transmitted successfully.');
        $this->line('  Check the server log and Test Results page in the web UI.');
    }

    private function scenarioError(string $host, int $port): void
    {
        $this->separator('DEVICE ERROR (Command E)');

        $errorNo = $this->ask('  Error code to send', 'W0175');

        $this->step('1', 'Connecting to server...');
        $sock = $this->connect($host, $port);
        if (!$sock) return;

        $this->step('2', "Sending Command E (error #{$errorNo})");
        $eFrame = $this->buildCommandE($errorNo);
        $this->sendRaw($sock, $eFrame, 'Command E');
        socket_close($sock);

        $this->info("  Device error #{$errorNo} sent.");
    }

    private function scenarioFull(string $host, int $port, int $delay): void
    {
        $this->separator('FULL FLOW (Worklist -> Test Start -> Results)');

        $entries = $this->scenarioWorklist($host, $port);
        if ($delay) sleep($delay);
        $this->line('');

        // Pick the worklist entry to report results for. Mimics a real analyzer
        // running a sample it pulled from the worklist, so results link to the pet.
        $entry  = null;
        $forced = $this->option('sample');

        if (!empty($entries)) {
            if ($forced) {
                foreach ($entries as $e) {
                    if ($e['sample_no'] === $forced) { $entry = $e; break; }
                }
            } else {
                $entry = $entries[0];
            }
        }

        if ($entry) {
            $this->info("  Reporting results for worklist sample '{$entry['sample_no']}' ({$entry['patient_name']}).");
        } else {
            $this->warn('  No matching worklist entry — results will not link to a pet.');
        }

        $this->scenarioResult($host, $port, $entry);
    }

    private function scenarioLoop(string $host, int $port, int $delay): void
    {
        $interval = max(3, (int) $this->ask('  Loop interval in seconds', '10'));
        $this->info("  Running loop every {$interval}s. Press Ctrl+C to stop.");

        $run = 1;
        while (true) {
            $this->line('');
            $this->separator("LOOP RUN #{$run}  —  " . now()->format('H:i:s'));

            $this->input->setOption('abnormal', $run % 3 === 0);

            $entries = $this->scenarioWorklist($host, $port);
            sleep(1);
            $this->scenarioResult($host, $port, !empty($entries) ? $entries[0] : null);

            $run++;
            $this->line("  Sleeping {$interval}s before next run...");
            sleep($interval);
        }
    }

    // ----- FRAME BUILDERS ----------------------------------------------------

    private function buildFrame(string $cmd, array $params): string
    {
        return $this->protocol->buildFrame($cmd, $params, $this->hasBcc);
    }

    private function buildCommandS(string $sampleNo, array $info): string
    {
        return $this->buildFrame('S', [
            'NORMAL',
            now()->format('Y-m-d'),
            now()->format('H:i'),
            str_pad($sampleNo, 13),
            str_pad((string) ($info['patient_id'] ?? ''), 13),
            str_pad((string) ($info['patient_name'] ?? ''), 13),
            '01',
        ]);
    }

    private function buildCommandR(string $sampleNo, array $info, array $tests): string
    {
        $params = [
            'NORMAL',
            now()->format('Y-m-d'),
            now()->format('H:i'),
            str_pad($sampleNo, 13),
            str_pad((string) ($info['patient_id'] ?? ''), 13),
            str_pad((string) ($info['patient_name'] ?? ''), 13),
            str_pad((string) ($info['species'] ?? '0'), 2, '0', STR_PAD_LEFT),
            (string) ($info['sex'] ?? '9'),
            str_pad((string) ($info['age'] ?? '999'), 3, '0', STR_PAD_LEFT),
            '01',
            str_pad((string) count($tests), 2, '0', STR_PAD_LEFT),
        ];

        foreach ($tests as $t) {
            $result   = str_pad($t['result'], 9);
            $unit     = str_pad($t['unit'], 6);
            $params[] = str_pad($t['name'], 8);   // test name
            $params[] = '=';                       // equality
            $params[] = $result . $unit;           // result+unit combined
            $params[] = '01';                      // dilution factor
            $params[] = str_pad($t['lo'], 5);      // ref lower
            $params[] = str_pad($t['hi'], 5);      // ref upper
            $params[] = str_pad($t['warn'], 11);   // warning flags
        }

        return $this->buildFrame('R', $params);
    }

    private function buildCommandE(string $errorNo): string
    {
        return $this->buildFrame('E', [
            now()->format('Y-m-d'),
            now()->format('H:i'),
            $errorNo,
            '01',
            'Simulator',
        ]);
    }

    // ----- SOCKET HELPERS ----------------------------------------------------

    private function connect(string $host, int $port): mixed
    {
        $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if ($sock === false) {
            $this->error("  Failed to create socket: " . socket_strerror(socket_last_error()));
            return null;
        }

        socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 5, 'usec' => 0]);
        socket_set_option($sock, SOL_SOCKET, SO_SNDTIMEO, ['sec' => 5, 'usec' => 0]);

        if (!socket_connect($sock, $host, $port)) {
            $this->error("  Cannot connect to {$host}:{$port} — " . socket_strerror(socket_last_error($sock)));
            socket_close($sock);
            return null;
        }

        $this->line("  Connected to {$host}:{$port}");
        return $sock;
    }

    private function sendRaw(mixed $sock, string $frame, string $label): void
    {
        $sent = socket_write($sock, $frame, strlen($frame));
        $this->line("  -> Sent {$label} ({$sent} bytes)");
        $this->line("    [RAW] " . strtoupper(bin2hex($frame)));
    }

    private function receiveAll(mixed $sock): string
    {
        $data    = '';
        $timeout = microtime(true) + 5.0;

        while (microtime(true) < $timeout) {
            $chunk = @socket_read($sock, 4096, PHP_BINARY_READ);
            if ($chunk === false || $chunk === '') break;
            $data .= $chunk;
            if (strpos($data, Nx600Protocol::ETX) !== false) break;
        }

        if ($data !== '') {
            $this->line("  <- Received (" . strlen($data) . " bytes)");
            $this->line("    [RAW] " . strtoupper(bin2hex($data)));
        }

        return $data;
    }

    // ----- UI HELPERS --------------------------------------------------------

    private function banner(): void
    {
        $this->line('');
        $this->info('  =============================================');
        $this->info('     NX600 Lab — Device Simulator');
        $this->info('  =============================================');
        $this->line('');
    }

    private function separator(string $title = ''): void
    {
        $this->line('  ' . str_repeat('-', 58));
        if ($title) {
            $this->line("  | {$title}");
            $this->line('  ' . str_repeat('-', 58));
        }
    }

    private function step(string $num, string $desc): void
    {
        $this->line("  [Step {$num}] {$desc}");
    }
}
