<?php

namespace Modules\NX600Lab\Console;

use Illuminate\Console\Command;
use Modules\NX600Lab\Entities\NxCommunicationLog;
use Modules\NX600Lab\Entities\NxDeviceConnection;
use Modules\NX600Lab\Entities\NxDeviceError;
use Modules\NX600Lab\Entities\NxTestResult;
use Modules\NX600Lab\Entities\NxTestSession;
use Modules\NX600Lab\Entities\NxWorklist;
use Modules\NX600Lab\Entities\Services\Nx600Protocol;
use Modules\NX600Lab\Entities\Services\VetClinicSyncService;

/**
 * NX600 analyzer TCP communication server (Types 1, 2, 3, A, B).
 *
 * Run with: php artisan nx600lab:tcp-server
 */
class Nx600TcpServer extends Command
{
    protected $signature = 'nx600lab:tcp-server
                            {--host= : IP address to listen on}
                            {--port= : TCP port to listen on}
                            {--type=1 : Communication type (1,2,3,A,B)}
                            {--no-bcc : Disable BCC verification (Type 3)}
                            {--debug : Show raw hex dump of all received bytes}';

    protected $description = 'Start the NX600 analyzer TCP communication server';

    private Nx600Protocol $protocol;
    private bool $running = true;
    private bool $debug = false;

    public function handle(): int
    {
        $this->protocol = new Nx600Protocol();

        $host     = $this->option('host') ?: config('nx600lab.tcp_host', '0.0.0.0');
        $port     = (int) ($this->option('port') ?: config('nx600lab.tcp_port', 65535));
        $commType = $this->option('type') ?: config('nx600lab.comm_type', '1');
        $hasBcc   = !$this->option('no-bcc') && $commType !== '3';
        $this->debug = (bool) $this->option('debug');

        $this->banner();
        $this->info("  Host      : {$host}");
        $this->info("  Port      : {$port}");
        $this->info("  Comm Type : {$commType}");
        $this->info("  BCC Check : " . ($hasBcc ? 'Yes' : 'No (Type 3)'));
        $this->info("  Debug     : " . ($this->debug ? 'ON (raw hex)' : 'Off'));
        $this->separator();

        $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if ($server === false) {
            $msg = "Failed to create socket: " . socket_strerror(socket_last_error());
            $this->error($msg);
            NxCommunicationLog::error($msg, 'system');
            return self::FAILURE;
        }

        socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);

        if (!socket_bind($server, $host, $port)) {
            $msg = "Failed to bind to {$host}:{$port} — " . socket_strerror(socket_last_error($server));
            $this->error($msg);
            NxCommunicationLog::error($msg, 'system');
            socket_close($server);
            return self::FAILURE;
        }

        if (!socket_listen($server, 5)) {
            $msg = "Failed to listen: " . socket_strerror(socket_last_error($server));
            $this->error($msg);
            NxCommunicationLog::error($msg, 'system');
            socket_close($server);
            return self::FAILURE;
        }

        $this->info("Server listening on {$host}:{$port}");
        $this->info("  Waiting for NX600 device connection...");
        $this->line("  Press Ctrl+C to stop.");
        NxCommunicationLog::info("TCP server started on {$host}:{$port} (Type {$commType})", 'system');

        socket_set_nonblock($server);

        $clients     = [];
        $buffers     = [];
        $connections = [];

        while ($this->running) {
            $client = @socket_accept($server);
            if ($client !== false) {
                $peerIp   = '';
                $peerPort = 0;
                socket_getpeername($client, $peerIp, $peerPort);
                $clientId = spl_object_id($client);

                $this->separator();
                $this->info("[+] NEW CONNECTION from {$peerIp}:{$peerPort}");

                socket_set_nonblock($client);
                $clients[$clientId] = $client;
                $buffers[$clientId] = '';

                $conn = NxDeviceConnection::create([
                    'device_ip'    => $peerIp,
                    'device_port'  => $peerPort,
                    'comm_type'    => $commType,
                    'status'       => 'connected',
                    'connected_at' => now(),
                ]);
                $connections[$clientId] = $conn->id;

                NxCommunicationLog::info(
                    "Device connected from {$peerIp}:{$peerPort}",
                    'connection',
                    ['port' => $peerPort, 'comm_type' => $commType],
                    $peerIp
                );
            }

            foreach ($clients as $clientId => $client) {
                $data = @socket_read($client, 4096, PHP_BINARY_READ);

                if ($data === false) {
                    $err = socket_last_error($client);
                    if ($err !== 0 && $err !== SOCKET_EAGAIN && $err !== SOCKET_EWOULDBLOCK) {
                        $this->disconnectClient($clientId, $clients, $buffers, $connections);
                    }
                    continue;
                }

                if ($data === '') {
                    $this->disconnectClient($clientId, $clients, $buffers, $connections);
                    continue;
                }

                if ($this->debug) {
                    $this->line('  [RAW<-] ' . strlen($data) . ' bytes: ' . strtoupper(bin2hex($data)));
                    $this->line('  [TXT<-] ' . preg_replace('/[^\x20-\x7E]/', '.', $data));
                }

                $buffers[$clientId] .= $data;

                [$frames, $buffers[$clientId]] = $this->protocol->extractFrames($buffers[$clientId]);

                foreach ($frames as $rawFrame) {
                    $this->processFrame($rawFrame, $client, $commType, $hasBcc, $clientId, $connections);
                }
            }

            usleep(10000);
        }

        socket_close($server);
        NxCommunicationLog::info("TCP server stopped.", 'system');
        $this->line('');
        $this->info("Server stopped.");
        return self::SUCCESS;
    }

    private function processFrame(
        string $rawFrame,
        mixed $client,
        string $commType,
        bool $hasBcc,
        int $clientId,
        array &$connections
    ): void {
        $peerIp = '';
        @socket_getpeername($client, $peerIp);

        $frame = $commType === '3'
            ? $this->protocol->parseType3Frame($rawFrame)
            : $this->protocol->parseFrame($rawFrame, $hasBcc);

        if (!$frame) {
            $msg = "Failed to parse frame (raw: " . bin2hex($rawFrame) . ")";
            $this->error("  [!] {$msg}");
            NxCommunicationLog::error($msg, 'system', ['raw' => bin2hex($rawFrame)], $peerIp);
            return;
        }

        if (isset($frame['bcc_error'])) {
            $msg = "BCC mismatch — expected 0x" . dechex($frame['bcc_calculated']) . ", got 0x" . dechex($frame['bcc_received']);
            $this->error("  [!] {$msg}");
            NxCommunicationLog::error($msg, 'system', $frame, $peerIp);
            return;
        }

        $command  = $frame['command'];
        $cmdNames = ['I' => 'WORKLIST REQUEST', 'W' => 'SAMPLE INFO REQUEST', 'S' => 'TEST START', 'R' => 'TEST RESULT', 'E' => 'DEVICE ERROR'];
        $cmdLabel = $cmdNames[$command] ?? "UNKNOWN ({$command})";
        $this->line("  <- CMD [{$command}] {$cmdLabel}  |  from {$peerIp}");

        match ($command) {
            Nx600Protocol::CMD_WORKLIST_INDEX => $this->handleWorklistIndex($frame, $client, $peerIp),
            Nx600Protocol::CMD_SAMPLE_INFO    => $this->handleSampleInfo($frame, $client, $peerIp),
            Nx600Protocol::CMD_TEST_START     => $this->handleTestStart($frame, $commType, $peerIp),
            Nx600Protocol::CMD_TEST_RESULT    => $this->handleTestResult($frame, $commType, $peerIp, $clientId, $connections),
            Nx600Protocol::CMD_ERROR          => $this->handleError($frame, $peerIp),
            default => $this->handleUnknown($command, $frame, $peerIp),
        };
    }

    private function handleWorklistIndex(array $frame, mixed $client, string $peerIp): void
    {
        $req = $this->protocol->parseWorklistIndexRequest($frame);

        $query = NxWorklist::query()->where('is_sent', false)->orderBy('created_at');
        if ($req['sample_no']) {
            $query->where('sample_no', '>=', $req['sample_no']);
        }
        $entries = $query->limit(min($req['count'], 99))->get();

        $count = count($entries);
        $this->line("     Pending entries found: {$count}");

        $replyData = $entries->map(fn($e) => ['sample_no' => $e->sample_no])->toArray();
        $reply = $this->protocol->buildWorklistIndexReply($replyData);
        $this->sendReply($client, $reply, 'I-reply');

        @socket_shutdown($client, 1);

        if ($count > 0) {
            NxCommunicationLog::success(
                "Worklist index sent: {$count} pending entr" . ($count === 1 ? 'y' : 'ies'),
                'worklist',
                ['count' => $count, 'samples' => $entries->pluck('sample_no')->toArray()],
                $peerIp
            );
        } else {
            NxCommunicationLog::info("Worklist requested — no pending entries", 'worklist', [], $peerIp);
        }
    }

    private function handleSampleInfo(array $frame, mixed $client, string $peerIp): void
    {
        $req   = $this->protocol->parseSampleInfoRequest($frame);
        $sno   = $req['sample_no'];
        $entry = NxWorklist::where('sample_no', $sno)->first();

        if (!$entry) {
            $reply = $this->protocol->buildSampleInfoReply([
                'sample_no' => $sno, 'patient_id' => '', 'patient_name' => '',
                'species' => 0, 'sex' => 9, 'age' => 999, 'test_names' => [],
            ]);
            NxCommunicationLog::warning("Sample '{$sno}' not in worklist — empty W-reply sent", 'worklist', [], $peerIp, $sno);
        } else {
            $reply = $this->protocol->buildSampleInfoReply([
                'sample_no'    => $entry->sample_no,
                'patient_id'   => $entry->patient_id   ?? '',
                'patient_name' => $entry->patient_name ?? '',
                'species'      => $entry->species ?? 0,
                'sex'          => $entry->sex ?? 9,
                'age'          => $entry->age ?? 999,
                'test_names'   => $entry->test_names ?? [],
            ]);
        }

        $this->sendReply($client, $reply, 'W-reply');
        @socket_shutdown($client, 1);

        if ($entry) {
            $entry->update(['is_sent' => true, 'sent_at' => now()]);
            NxCommunicationLog::success(
                "Sample info sent for '{$sno}' — marked as sent",
                'worklist',
                ['patient_name' => $entry->patient_name, 'patient_id' => $entry->patient_id],
                $peerIp,
                $sno
            );
        }
    }

    private function handleTestStart(array $frame, string $commType, string $peerIp): void
    {
        $data = $this->protocol->parseTestStart($frame, $commType);
        $sno  = $data['sample_no'];

        $this->line("     Test STARTED — sample {$sno}");
        NxCommunicationLog::info("Test started for sample '{$sno}'", 'result', $data, $peerIp, $sno);
    }

    private function handleTestResult(
        array $frame,
        string $commType,
        string $peerIp,
        int $clientId,
        array &$connections
    ): void {
        $data = $commType === '3'
            ? $this->protocol->parseTestResultType3($frame)
            : $this->protocol->parseTestResult($frame, $commType);

        $sno      = $data['sample_no'];
        $numTests = count($data['tests']);

        $this->line("     Test RESULT — sample {$sno}, {$numTests} parameter(s)");

        $session = NxTestSession::updateOrCreate(
            ['sample_no' => $data['sample_no'], 'test_date' => $data['test_date']],
            [
                'patient_id'      => $data['patient_id'] ?: null,
                'patient_name'    => $data['patient_name'] ?: null,
                'test_condition'  => $data['test_condition'],
                'test_date'       => $data['test_date'],
                'test_time'       => $data['test_time'],
                'sample_type'     => $data['sample_type'] ?? null,
                'species'         => $data['species'],
                'sex'             => $data['sex'],
                'age'             => $data['age'],
                'sample_position' => $data['sample_position'],
                'operator_id'     => $data['operator_id'] ?? null,
                'emergency_test'  => $data['emergency_test'] ?? false,
                'analyzer_id'     => $data['analyzer_id'] ?? null,
                'slide_lot'       => $data['slide_lot'] ?? null,
                'valid_term'      => $data['valid_term'] ?? null,
                'comm_type'       => $commType,
                'device_ip'       => $peerIp,
            ]
        );

        $abnormalCount = 0;
        $stored = 0;
        foreach ($data['tests'] as $t) {
            NxTestResult::updateOrCreate(
                ['test_session_id' => $session->id, 'test_name' => $t['test_name']],
                [
                    'base_name'             => $t['base_name'],
                    'sample_suffix'         => $t['sample_suffix'],
                    'equality'              => $t['equality'],
                    'result'                => $t['result'],
                    'unit'                  => $t['unit'],
                    'dilution_factor'       => $t['dilution_factor'],
                    'ref_lower'             => $t['ref_lower'],
                    'ref_upper'             => $t['ref_upper'],
                    'raw_warning'           => $t['raw_warning'],
                    'warn_high'             => $t['warn_high'],
                    'warn_low'              => $t['warn_low'],
                    'warn_out_of_range'     => $t['warn_out_of_range'],
                    'warn_slide_expired'    => $t['warn_slide_expired'],
                    'warn_temp_error'       => $t['warn_temp_error'],
                    'warn_photometer_error' => $t['warn_photometer_error'],
                    'warn_high_value'       => $t['warn_high_value'],
                    'warn_calc_error'       => $t['warn_calc_error'],
                    'warn_unspotted'        => $t['warn_unspotted'],
                    'is_abnormal'           => $t['is_abnormal'],
                ]
            );

            if ($t['is_abnormal']) {
                $abnormalCount++;
            }
            $stored++;
        }

        // Inherit pet linkage from the matching worklist entry (synced mode).
        VetClinicSyncService::applyWorklistLinkage($session);

        if ($abnormalCount > 0) {
            $this->warn("     Stored {$stored} results — {$abnormalCount} ABNORMAL parameter(s)");
        } else {
            $this->info("     Stored {$stored} results — all within normal range");
        }

        if (isset($connections[$clientId])) {
            NxDeviceConnection::where('id', $connections[$clientId])->increment('results_stored', $stored);
            NxDeviceConnection::where('id', $connections[$clientId])->increment('messages_received');
        }

        $level = $abnormalCount > 0 ? 'warning' : 'success';
        NxCommunicationLog::$level(
            "Results received for '{$sno}': {$stored} parameters" . ($abnormalCount > 0 ? ", {$abnormalCount} abnormal" : ", all normal"),
            'result',
            ['sample_no' => $sno, 'stored' => $stored, 'abnormal_count' => $abnormalCount, 'session_id' => $session->id],
            $peerIp,
            $sno
        );
    }

    private function handleError(array $frame, string $peerIp): void
    {
        $data = $this->protocol->parseError($frame);

        $this->error("     DEVICE ERROR #{$data['error_number']} at {$data['error_date']} {$data['error_time']}");

        NxDeviceError::create([
            'error_date'   => $data['error_date'],
            'error_time'   => $data['error_time'],
            'error_number' => $data['error_number'],
            'added_info'   => $data['added_info'],
            'device_ip'    => $peerIp,
            'comm_type'    => null,
        ]);

        NxCommunicationLog::error(
            "Device error #{$data['error_number']} at {$data['error_date']} {$data['error_time']}",
            'error',
            $data,
            $peerIp
        );
    }

    private function handleUnknown(string $command, array $frame, string $peerIp): void
    {
        $msg = "Unknown command '{$command}' received";
        $this->warn("     [!] {$msg}");
        NxCommunicationLog::warning($msg, 'system', ['params' => array_slice($frame['params'], 0, 5)], $peerIp);
    }

    private function sendReply(mixed $client, string $reply, string $label): void
    {
        $sent = @socket_write($client, $reply, strlen($reply));
        if ($sent === false) {
            $this->error("  [!] Failed to send {$label}");
        } else {
            $this->line("     -> Sent {$label} ({$sent} bytes)");
            if ($this->debug) {
                $this->line('  [TX-RAW] ' . strtoupper(bin2hex($reply)));
            }
        }
    }

    private function disconnectClient(
        int $clientId,
        array &$clients,
        array &$buffers,
        array &$connections
    ): void {
        $peerIp = '';
        @socket_getpeername($clients[$clientId], $peerIp);
        @socket_close($clients[$clientId]);

        $this->line("[-] Connection closed ({$peerIp})");

        if (isset($connections[$clientId])) {
            NxDeviceConnection::where('id', $connections[$clientId])->update([
                'status'          => 'disconnected',
                'disconnected_at' => now(),
            ]);
        }

        NxCommunicationLog::info("Device disconnected ({$peerIp})", 'connection', [], $peerIp);

        unset($clients[$clientId], $buffers[$clientId], $connections[$clientId]);
    }

    private function separator(): void
    {
        $this->line('  ' . str_repeat('-', 60));
    }

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