<?php

namespace Modules\NX600Lab\Entities\Services;

/**
 * HL7 2.5.1 MLLP Parser for NX600 Type 4/C
 *
 * MLLP (Minimal Lower Layer Protocol) frame:
 *   <SB> HL7_message <EB><CR>
 *
 * SB = 0x0B (Vertical Tab)
 * EB = 0x1C (File Separator)
 * CR = 0x0D (Carriage Return)
 *
 * NX600 sends LAB-29 transaction (ORU^R01 message) with test results.
 * The PC sends an ACK message back.
 */
class Hl7MllpParser
{
    const MLLP_SB     = "\x0B"; // Start Block
    const MLLP_EB     = "\x1C"; // End Block
    const MLLP_CR     = "\x0D"; // Carriage Return
    const HL7_SEP     = '|';
    const HL7_COMP    = '^';
    const HL7_REP     = '~';
    const HL7_ESC     = '\\';
    const HL7_SUB     = '&';
    const HL7_SEG_SEP = "\r"; // Segment separator in HL7 is CR

    /**
     * Extract a complete MLLP message from a buffer.
     * Returns [message_string|null, remaining_buffer].
     */
    public function extractMessage(string $buffer): array
    {
        $sbPos = strpos($buffer, self::MLLP_SB);
        if ($sbPos === false) {
            return [null, $buffer];
        }

        $ebPos = strpos($buffer, self::MLLP_EB, $sbPos);
        if ($ebPos === false) {
            return [null, $buffer]; // incomplete
        }

        // Message content is between SB and EB
        $message = substr($buffer, $sbPos + 1, $ebPos - $sbPos - 1);
        $remaining = substr($buffer, $ebPos + 2); // skip EB + CR

        return [$message, $remaining];
    }

    /**
     * Build an MLLP-wrapped HL7 ACK message.
     */
    public function buildAck(string $originalMsh, bool $success = true): string
    {
        $mshFields = explode('|', $originalMsh);
        $sendingApp   = $mshFields[2] ?? '';
        $sendingFac   = $mshFields[3] ?? '';
        $receivingApp = $mshFields[4] ?? '';
        $receivingFac = $mshFields[5] ?? '';
        $msgId        = $mshFields[9] ?? '';
        $ackCode      = $success ? 'AA' : 'AE';

        $now = date('YmdHis');
        $ack = "MSH|^~\\&|{$receivingApp}|{$receivingFac}|{$sendingApp}|{$sendingFac}|{$now}||ACK|ACK{$now}|P|2.5.1\r";
        $ack .= "MSA|{$ackCode}|{$msgId}\r";

        return self::MLLP_SB . $ack . self::MLLP_EB . self::MLLP_CR;
    }

    /**
     * Parse an HL7 2.5.1 ORU^R01 message into structured data.
     * Returns array with patient info and OBX result segments.
     */
    public function parseOru(string $hl7Message): array
    {
        $segments = explode(self::HL7_SEG_SEP, trim($hl7Message));
        $parsed = [
            'message_type' => '',
            'message_id'   => '',
            'patient'      => [],
            'order'        => [],
            'results'      => [],
            'raw_msh'      => '',
        ];

        $currentOrder = [];

        foreach ($segments as $segment) {
            if (empty(trim($segment))) {
                continue;
            }

            $fields = explode('|', $segment);
            $segId = $fields[0] ?? '';

            switch ($segId) {
                case 'MSH':
                    $parsed['raw_msh'] = $segment;
                    $parsed['message_type'] = $fields[8] ?? '';
                    $parsed['message_id'] = $fields[9] ?? '';
                    break;

                case 'PID':
                    // PID-3: Patient ID, PID-5: Name, PID-7: DOB, PID-8: Sex
                    $parsed['patient'] = [
                        'patient_id'   => $this->extractField($fields, 3),
                        'patient_name' => $this->parsePatientName($fields[5] ?? ''),
                        'dob'          => $this->extractField($fields, 7),
                        'sex'          => $this->parseSex($fields[8] ?? ''),
                    ];
                    break;

                case 'OBR':
                    // OBR-3: Filler Order Number (Sample No), OBR-7: Date/Time, OBR-25: Status
                    $currentOrder = [
                        'order_id'      => $this->extractField($fields, 2),
                        'sample_no'     => $this->extractField($fields, 3),
                        'test_date'     => $this->parseHl7DateTime($fields[7] ?? ''),
                        'test_time'     => $this->parseHl7Time($fields[7] ?? ''),
                        'result_status' => $this->extractField($fields, 25),
                    ];
                    break;

                case 'OBX':
                    // OBX-3: Test Identifier, OBX-5: Value, OBX-6: Units,
                    // OBX-7: Reference Range, OBX-8: Abnormal Flags
                    $testId = $this->extractField($fields, 3);
                    $testName = $this->parseObxIdentifier($testId);
                    $refRange = $this->extractField($fields, 7);
                    [$refLo, $refHi] = $this->parseReferenceRange($refRange);
                    $abnormalFlag = $this->extractField($fields, 8);

                    $parsed['results'][] = [
                        'test_name'             => $testName,
                        'base_name'             => $testName,
                        'sample_suffix'         => '',
                        'equality'              => '=',
                        'result'                => $this->extractField($fields, 5),
                        'unit'                  => $this->extractField($fields, 6),
                        'dilution_factor'       => 1,
                        'ref_lower'             => $refLo,
                        'ref_upper'             => $refHi,
                        'raw_warning'           => str_pad($abnormalFlag, 11),
                        'warn_high'             => str_contains($abnormalFlag, 'H'),
                        'warn_low'              => str_contains($abnormalFlag, 'L'),
                        'warn_out_of_range'     => false,
                        'warn_slide_expired'    => false,
                        'warn_temp_error'       => false,
                        'warn_photometer_error' => false,
                        'warn_high_value'       => false,
                        'warn_calc_error'       => false,
                        'warn_unspotted'        => false,
                        'is_abnormal'           => !empty(trim($abnormalFlag)),
                    ];
                    break;
            }
        }

        $parsed['order'] = $currentOrder;
        return $parsed;
    }

    private function extractField(array $fields, int $index): string
    {
        return trim($fields[$index] ?? '');
    }

    private function parsePatientName(string $nameField): string
    {
        $parts = explode('^', $nameField);
        return trim(($parts[1] ?? '') . ' ' . ($parts[0] ?? ''));
    }

    private function parseSex(string $sex): int
    {
        return match (strtoupper($sex)) {
            'M' => 0,
            'F' => 1,
            default => 9,
        };
    }

    private function parseObxIdentifier(string $id): string
    {
        $parts = explode('^', $id);
        return trim($parts[1] ?? $parts[0] ?? '');
    }

    private function parseHl7DateTime(string $dt): string
    {
        if (strlen($dt) >= 8) {
            return substr($dt, 0, 4) . '-' . substr($dt, 4, 2) . '-' . substr($dt, 6, 2);
        }
        return date('Y-m-d');
    }

    private function parseHl7Time(string $dt): string
    {
        if (strlen($dt) >= 12) {
            return substr($dt, 8, 2) . ':' . substr($dt, 10, 2);
        }
        return date('H:i');
    }

    private function parseReferenceRange(string $range): array
    {
        if (str_contains($range, '-')) {
            $parts = explode('-', $range, 2);
            return [trim($parts[0]), trim($parts[1])];
        }
        return ['', ''];
    }
}
