<?php

namespace Modules\LabCore\Entities\Services\Astm;

/**
 * ASTM E1394 / CLSI LIS2-A2 record-content parser.
 *
 * This is the *content* layer: it takes an already-assembled message (the set
 * of records, CR/LF separated) and turns it into a structured tree. The
 * *transport* layer (ASTM E1381 — ENQ/ACK/STX framing/checksum) is the job of
 * the standalone LabBridge Agent and is intentionally not implemented here.
 *
 * Record hierarchy:  H → ( P → ( O → (R | C)* )* )* → L     plus stray Q records.
 *
 * Field delimiters are declared by the H record (`H|\^&|...`) and detected at
 * parse time, so the parser adapts to each analyzer's delimiter set.
 *
 * Records are returned as 0-indexed field arrays where index 0 is the record
 * type — so ASTM "field N" (1-based) is array index N-1.
 */
class AstmRecordParser
{
    private string $fieldDelim     = '|';
    private string $repeatDelim    = '\\';
    private string $componentDelim = '^';
    private string $escapeDelim    = '&';

    /**
     * Parse a full ASTM message into a structured tree.
     *
     * @return array{
     *   header: array, terminator: array, queries: array<int,array>,
     *   patients: array<int,array{patient: array, orders: array<int,array{order: array, results: array<int,array>, comments: array<int,array>}>}>
     * }
     */
    public function parseMessage(string $text): array
    {
        $text  = str_replace(["\r\n", "\n"], "\r", $text);
        $lines = array_values(array_filter(
            array_map('trim', explode("\r", $text)),
            fn($l) => $l !== ''
        ));

        $message = ['header' => [], 'patients' => [], 'queries' => [], 'terminator' => []];
        $pi = -1; // current patient index
        $oi = -1; // current order index within that patient

        foreach ($lines as $line) {
            $type = strtoupper(substr($line, 0, 1));

            if ($type === 'H') {
                $this->detectDelimiters($line);
                $message['header'] = $this->splitFields($line);
                continue;
            }

            $fields = $this->splitFields($line);

            switch ($type) {
                case 'P':
                    $message['patients'][] = ['patient' => $fields, 'orders' => []];
                    $pi = count($message['patients']) - 1;
                    $oi = -1;
                    break;

                case 'O':
                    if ($pi < 0) {
                        $message['patients'][] = ['patient' => [], 'orders' => []];
                        $pi = 0;
                    }
                    $message['patients'][$pi]['orders'][] = ['order' => $fields, 'results' => [], 'comments' => []];
                    $oi = count($message['patients'][$pi]['orders']) - 1;
                    break;

                case 'R':
                    [$pi, $oi] = $this->ensureOrder($message, $pi, $oi);
                    $message['patients'][$pi]['orders'][$oi]['results'][] = $fields;
                    break;

                case 'C':
                    if ($pi >= 0 && $oi >= 0) {
                        $message['patients'][$pi]['orders'][$oi]['comments'][] = $fields;
                    }
                    break;

                case 'Q':
                    $message['queries'][] = $fields;
                    break;

                case 'L':
                    $message['terminator'] = $fields;
                    break;
            }
        }

        return $message;
    }

    /**
     * Guarantee a patient + order slot exists for an orphan R record.
     *
     * @return array{0:int,1:int}
     */
    private function ensureOrder(array &$message, int $pi, int $oi): array
    {
        if ($pi < 0) {
            $message['patients'][] = ['patient' => [], 'orders' => []];
            $pi = 0;
        }
        if ($oi < 0) {
            $message['patients'][$pi]['orders'][] = ['order' => [], 'results' => [], 'comments' => []];
            $oi = 0;
        }
        return [$pi, $oi];
    }

    /**
     * Detect the delimiter set from the H record (`H|\^&|...`).
     */
    private function detectDelimiters(string $h): void
    {
        if (strlen($h) < 2) {
            return;
        }
        $this->fieldDelim = $h[1];

        $rest   = substr($h, 2);
        $end    = strpos($rest, $this->fieldDelim);
        $delims = $end === false ? $rest : substr($rest, 0, $end);

        if (strlen($delims) >= 1) $this->repeatDelim    = $delims[0];
        if (strlen($delims) >= 2) $this->componentDelim = $delims[1];
        if (strlen($delims) >= 3) $this->escapeDelim    = $delims[2];
    }

    /**
     * Split a record into its 0-indexed field array (index 0 = record type).
     */
    public function splitFields(string $record): array
    {
        return explode($this->fieldDelim, $record);
    }

    /**
     * Components of a field (1-based accessor).
     */
    public function component(string $field, int $n): string
    {
        $parts = explode($this->componentDelim, $field);
        return trim($parts[$n - 1] ?? '');
    }

    /**
     * All components of a field.
     *
     * @return array<int,string>
     */
    public function components(string $field): array
    {
        return array_map('trim', explode($this->componentDelim, $field));
    }

    /**
     * Repeated values within a field.
     *
     * @return array<int,string>
     */
    public function repeats(string $field): array
    {
        return array_map('trim', explode($this->repeatDelim, $field));
    }

    /**
     * Safe 1-based ASTM field accessor ("field 3" = $fields index 2).
     */
    public static function field(array $fields, int $n): string
    {
        return trim((string) ($fields[$n - 1] ?? ''));
    }

    public function delimiters(): array
    {
        return [
            'field'     => $this->fieldDelim,
            'repeat'    => $this->repeatDelim,
            'component' => $this->componentDelim,
            'escape'    => $this->escapeDelim,
        ];
    }
}
