<?php

namespace App\Services;

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\ParsedExpression;

class RuleExpressionService
{
    protected ExpressionLanguage $language;

    /** @var array<string, ParsedExpression> */
    protected array $parsedCache = [];

    public function __construct()
    {
        $this->language = new ExpressionLanguage();
    }

    public function evaluate(string $expr, array $context = [])
    {
        $expr = trim($expr);
        if ($expr === '') {
            return null;
        }

        $names = array_keys($context);
        sort($names);
        $cacheKey = md5($expr . '|' . implode(',', $names));

        if (!isset($this->parsedCache[$cacheKey])) {
            $this->parsedCache[$cacheKey] = $this->language->parse($expr, $names);
        }

        return $this->language->evaluate($this->parsedCache[$cacheKey], $context);
    }

    public function evaluateBoolean(?string $expr, array $context = []): bool
    {
        if ($expr === null || trim($expr) === '') {
            return true;
        }

        return (bool) $this->evaluate($expr, $context);
    }

    /**
     * Compile DSL expression to engine-compatible JSON structure.
     *
     * Supported DSL (canonical):
     * - if(condition; then-actions; else-actions)
     * - sex('F'|'M') -> patient["Sex"] == 'F'
     * - priority('R'|'S'|'U') -> order["Priority"] == 'S'
     * - age > 18 -> age > 18
     * - requested('CODE') -> requested('CODE') (resolved at runtime)
     * - result_set('TestSiteCode', value)
     * - result_set(value) (deprecated, uses current context TestSiteID)
     * - test_insert('CODE')
     * - test_delete('CODE')
     * - comment_insert('text')
     * - nothing
     * - Multi-action: result_set(0.5):test_insert('CODE'):comment_insert('text')
     *
     * Backward compatible aliases:
     * - set_result -> result_set
     * - insert -> test_insert
     * - add_comment -> comment_insert
     *
     * @param string $expr The raw DSL expression
     * @return array The compiled structure with actions
     * @throws \InvalidArgumentException If DSL is invalid
     */
    public function compile(string $expr): array
    {
        $expr = trim($expr);
        if ($expr === '') {
            return [];
        }

        $inner = $this->extractIfInner($expr);
        if ($inner === null) {
            // Allow callers to pass without if(...) wrapper
            if (substr_count($expr, ';') >= 2) {
                $inner = $expr;
            } else {
                throw new \InvalidArgumentException('Invalid DSL: expected "if(condition; then; else)" format');
            }
        }

        $parts = $this->splitTopLevel($inner, ';', 3);
        if (count($parts) !== 3) {
            // Fallback: legacy ternary syntax
            $ternary = $this->convertTopLevelTernaryToSemicolon($inner);
            if ($ternary !== null) {
                $parts = $this->splitTopLevel($ternary, ';', 3);
            }
        }

        if (count($parts) !== 3) {
            throw new \InvalidArgumentException('Invalid DSL: expected exactly 3 parts "condition; then; else"');
        }

        $condition = trim($parts[0]);
        $thenAction = trim($parts[1]);
        $elseAction = trim($parts[2]);

        // Compile condition
        $compiledCondition = $this->compileCondition($condition);

        // Compile actions (supports multi-action with : separator)
        $thenActions = $this->compileMultiAction($thenAction);
        $elseActions = $this->compileMultiAction($elseAction);

        // Build valueExpr for backward compatibility
        $thenValueExpr = $this->buildValueExpr($thenActions);
        $elseValueExpr = $this->buildValueExpr($elseActions);

        $valueExpr = "({$compiledCondition}) ? {$thenValueExpr} : {$elseValueExpr}";

        return [
            'conditionExpr' => $compiledCondition,
            'valueExpr' => $valueExpr,
            'then' => $thenActions,
            'else' => $elseActions,
        ];
    }

    /**
     * Compile DSL condition to ExpressionLanguage expression
     * Supports: sex(), priority(), age, requested(), &&, ||, (), comparison operators
     */
    private function compileCondition(string $condition): string
    {
        $condition = trim($condition);
        if ($condition === '') {
            return 'true';
        }

        $tokens = $this->tokenize($condition);
        $out = '';
        $count = count($tokens);

        for ($i = 0; $i < $count; $i++) {
            $t = $tokens[$i];
            $type = $t['type'];
            $val = $t['value'];

            if ($type === 'op' && $val === '&&') {
                $out .= ' and ';
                continue;
            }
            if ($type === 'op' && $val === '||') {
                $out .= ' or ';
                continue;
            }

            if ($type === 'ident') {
                $lower = strtolower($val);

                // sex('M')
                if ($lower === 'sex') {
                    $arg = $this->parseSingleStringArg($tokens, $i + 1);
                    if ($arg !== null) {
                        $out .= 'patient["Sex"] == "' . addslashes($arg['value']) . '"';
                        $i = $arg['endIndex'];
                        continue;
                    }
                }

                // priority('S')
                if ($lower === 'priority') {
                    $arg = $this->parseSingleStringArg($tokens, $i + 1);
                    if ($arg !== null) {
                        $out .= 'order["Priority"] == "' . addslashes($arg['value']) . '"';
                        $i = $arg['endIndex'];
                        continue;
                    }
                }

                // requested('CODE')
                if ($lower === 'requested') {
                    $arg = $this->parseSingleStringArg($tokens, $i + 1);
                    if ($arg !== null) {
                        $out .= 'requested("' . addslashes($arg['value']) . '")';
                        $i = $arg['endIndex'];
                        continue;
                    }
                }

                // age >= 18
                if ($lower === 'age') {
                    $op = $tokens[$i + 1] ?? null;
                    $num = $tokens[$i + 2] ?? null;
                    if (($op['type'] ?? '') === 'op' && in_array($op['value'], ['<', '>', '<=', '>=', '==', '!='], true) && ($num['type'] ?? '') === 'number') {
                        $out .= 'age ' . $op['value'] . ' ' . $num['value'];
                        $i += 2;
                        continue;
                    }
                }
            }

            $out .= $val;
        }

        return trim($out);
    }

    /**
     * Compile multi-action string (separated by :)
     * Returns array of action objects
     */
    private function compileMultiAction(string $actionStr): array
    {
        $actionStr = trim($actionStr);

        // Split by : separator (top-level only)
        $actions = [];

        $parts = $this->splitTopLevel($actionStr, ':');

        foreach ($parts as $part) {
            $action = $this->compileSingleAction(trim($part));
            if ($action !== null) {
                $actions[] = $action;
            }
        }

        return $actions;
    }

    /**
     * Compile a single action
     */
    private function compileSingleAction(string $action): ?array
    {
        $action = trim($action);

        // nothing - no operation
        if (strcasecmp($action, 'nothing') === 0) {
            return [
                'type' => 'NO_OP',
                'value' => null,
            ];
        }

        // result_set(value) or result_set('CODE', value) [aliases: set_result]
        if (preg_match('/^(result_set|set_result)\s*\(\s*(.+)\s*\)$/is', $action, $m)) {
            $args = $this->splitTopLevel($m[2], ',', 2);
            if (empty($args)) {
                throw new \InvalidArgumentException('result_set requires a value expression');
            }

            $testCode = null;
            if (count($args) === 2) {
                $testCode = $this->unquoteStringArgument($args[0]);
                if ($testCode === null) {
                    throw new \InvalidArgumentException('result_set test code must be a quoted string');
                }
            }

            $value = trim($args[count($args) - 1]);

            // Check if it's a number
            if (is_numeric($value)) {
                return [
                    'type' => 'RESULT_SET',
                    'value' => strpos($value, '.') !== false ? (float) $value : (int) $value,
                    'valueExpr' => $value,
                    'testCode' => $testCode,
                ];
            }

            // Check if it's a quoted string (single or double quotes)
            if (preg_match('/^["\'](.+)["\']$/s', $value, $vm)) {
                return [
                    'type' => 'RESULT_SET',
                    'value' => $vm[1],
                    'valueExpr' => '"' . addslashes($vm[1]) . '"',
                    'testCode' => $testCode,
                ];
            }

            // Complex expression
            return [
                'type' => 'RESULT_SET',
                'valueExpr' => $value,
                'testCode' => $testCode,
            ];
        }

        // test_insert('CODE') [aliases: insert]
        if (preg_match('/^(test_insert|insert)\s*\(\s*["\']([^"\']+)["\']\s*\)$/i', $action, $m)) {
            return [
                'type' => 'TEST_INSERT',
                'testCode' => $m[2],
            ];
        }

        // test_delete('CODE')
        if (preg_match('/^test_delete\s*\(\s*["\']([^"\']+)["\']\s*\)$/i', $action, $m)) {
            return [
                'type' => 'TEST_DELETE',
                'testCode' => $m[1],
            ];
        }

        // comment_insert('text') [aliases: add_comment]
        if (preg_match('/^(comment_insert|add_comment)\s*\(\s*["\'](.+?)["\']\s*\)$/is', $action, $m)) {
            return [
                'type' => 'COMMENT_INSERT',
                'comment' => $m[2],
            ];
        }

        throw new \InvalidArgumentException('Unknown action: ' . $action);
    }

    /**
     * Build valueExpr string from array of actions (for backward compatibility)
     */
    private function buildValueExpr(array $actions): string
    {
        if (empty($actions)) {
            return 'null';
        }

        // Use first SET_RESULT action's value, or null
        foreach ($actions as $action) {
            if (($action['type'] ?? '') === 'RESULT_SET' || ($action['type'] ?? '') === 'SET_RESULT') {
                if (isset($action['valueExpr'])) {
                    return $action['valueExpr'];
                }
                if (isset($action['value'])) {
                    if (is_string($action['value'])) {
                        return '"' . addslashes($action['value']) . '"';
                    }
                    return json_encode($action['value']);
                }
            }
        }

        return 'null';
    }

    /**
     * Parse and split multi-rule expressions (comma-separated)
     * Returns array of individual rule expressions
     */
    public function parseMultiRule(string $expr): array
    {
        $expr = trim($expr);
        if ($expr === '') {
            return [];
        }

        $rules = [];
        $depth = 0;
        $current = '';
        $len = strlen($expr);

        for ($i = 0; $i < $len; $i++) {
            $char = $expr[$i];

            if ($char === '(') {
                $depth++;
            } elseif ($char === ')') {
                $depth--;
            }

            if ($char === ',' && $depth === 0) {
                $trimmed = trim($current);
                if ($trimmed !== '') {
                    $rules[] = $trimmed;
                }
                $current = '';
            } else {
                $current .= $char;
            }
        }

        // Add last rule
        $trimmed = trim($current);
        if ($trimmed !== '') {
            $rules[] = $trimmed;
        }

        return $rules;
    }

    private function extractIfInner(string $expr): ?string
    {
        $expr = trim($expr);
        if (!preg_match('/^if\s*\(/i', $expr)) {
            return null;
        }

        $pos = stripos($expr, '(');
        if ($pos === false) {
            return null;
        }

        $start = $pos + 1;
        $end = $this->findMatchingParen($expr, $pos);
        if ($end === null) {
            throw new \InvalidArgumentException('Invalid DSL: unbalanced parentheses in if(...)');
        }

        // Only allow trailing whitespace after the closing paren
        if (trim(substr($expr, $end + 1)) !== '') {
            throw new \InvalidArgumentException('Invalid DSL: unexpected trailing characters after if(...)');
        }

        return trim(substr($expr, $start, $end - $start));
    }

    private function findMatchingParen(string $s, int $openIndex): ?int
    {
        $len = strlen($s);
        $depth = 0;
        $inSingle = false;
        $inDouble = false;

        for ($i = $openIndex; $i < $len; $i++) {
            $ch = $s[$i];

            if ($ch === '\\') {
                $i++;
                continue;
            }
            if (!$inDouble && $ch === "'") {
                $inSingle = !$inSingle;
                continue;
            }
            if (!$inSingle && $ch === '"') {
                $inDouble = !$inDouble;
                continue;
            }
            if ($inSingle || $inDouble) {
                continue;
            }

            if ($ch === '(') {
                $depth++;
                continue;
            }
            if ($ch === ')') {
                $depth--;
                if ($depth === 0) {
                    return $i;
                }
            }
        }
        return null;
    }

    /**
     * Split string by a delimiter only when not inside quotes/parentheses.
     * If $limit is provided, splits into at most $limit parts.
     */
    private function splitTopLevel(string $s, string $delimiter, ?int $limit = null): array
    {
        $s = (string) $s;
        $len = strlen($s);
        $depth = 0;
        $inSingle = false;
        $inDouble = false;
        $parts = [];
        $buf = '';

        for ($i = 0; $i < $len; $i++) {
            $ch = $s[$i];

            if ($ch === '\\') {
                $buf .= $ch;
                if ($i + 1 < $len) {
                    $buf .= $s[$i + 1];
                    $i++;
                }
                continue;
            }

            if (!$inDouble && $ch === "'") {
                $inSingle = !$inSingle;
                $buf .= $ch;
                continue;
            }
            if (!$inSingle && $ch === '"') {
                $inDouble = !$inDouble;
                $buf .= $ch;
                continue;
            }

            if (!$inSingle && !$inDouble) {
                if ($ch === '(') {
                    $depth++;
                } elseif ($ch === ')') {
                    $depth = max(0, $depth - 1);
                }

                if ($depth === 0 && $ch === $delimiter) {
                    $parts[] = trim($buf);
                    $buf = '';
                    if ($limit !== null && count($parts) >= ($limit - 1)) {
                        $buf .= substr($s, $i + 1);
                        $i = $len;
                    }
                    continue;
                }
            }

            $buf .= $ch;
        }

        $parts[] = trim($buf);
        $parts = array_values(array_filter($parts, static fn ($p) => $p !== ''));
        return $parts;
    }

    /**
     * Convert a top-level ternary (condition ? then : else) into semicolon form.
     */
    private function convertTopLevelTernaryToSemicolon(string $s): ?string
    {
        $len = strlen($s);
        $depth = 0;
        $inSingle = false;
        $inDouble = false;
        $qPos = null;
        $cPos = null;

        for ($i = 0; $i < $len; $i++) {
            $ch = $s[$i];
            if ($ch === '\\') {
                $i++;
                continue;
            }
            if (!$inDouble && $ch === "'") {
                $inSingle = !$inSingle;
                continue;
            }
            if (!$inSingle && $ch === '"') {
                $inDouble = !$inDouble;
                continue;
            }
            if ($inSingle || $inDouble) {
                continue;
            }
            if ($ch === '(') {
                $depth++;
                continue;
            }
            if ($ch === ')') {
                $depth = max(0, $depth - 1);
                continue;
            }
            if ($depth === 0 && $ch === '?' && $qPos === null) {
                $qPos = $i;
                continue;
            }
            if ($depth === 0 && $ch === ':' && $qPos !== null) {
                $cPos = $i;
                break;
            }
        }

        if ($qPos === null || $cPos === null) {
            return null;
        }

        $cond = trim(substr($s, 0, $qPos));
        $then = trim(substr($s, $qPos + 1, $cPos - $qPos - 1));
        $else = trim(substr($s, $cPos + 1));

        if ($cond === '' || $then === '' || $else === '') {
            return null;
        }

        return $cond . '; ' . $then . '; ' . $else;
    }

    /**
     * Tokenize a condition for lightweight transforms.
     * Returns tokens with keys: type (ident|number|string|op|punct|other), value.
     */
    private function tokenize(string $s): array
    {
        $len = strlen($s);
        $tokens = [];

        for ($i = 0; $i < $len; $i++) {
            $ch = $s[$i];

            if (ctype_space($ch)) {
                continue;
            }

            $two = ($i + 1 < $len) ? $ch . $s[$i + 1] : '';
            if (in_array($two, ['&&', '||', '>=', '<=', '==', '!='], true)) {
                $tokens[] = ['type' => 'op', 'value' => $two];
                $i++;
                continue;
            }

            if (in_array($ch, ['>', '<', '(', ')', '[', ']', ',', '+', '-', '*', '/', '%', '!', '.', ':', '?'], true)) {
                $tokens[] = ['type' => ($ch === '>' || $ch === '<' || $ch === '!' ? 'op' : 'punct'), 'value' => $ch];
                continue;
            }

            if ($ch === '"' || $ch === "'") {
                $quote = $ch;
                $buf = $quote;
                $i++;
                for (; $i < $len; $i++) {
                    $c = $s[$i];
                    $buf .= $c;
                    if ($c === '\\' && $i + 1 < $len) {
                        $buf .= $s[$i + 1];
                        $i++;
                        continue;
                    }
                    if ($c === $quote) {
                        break;
                    }
                }
                $tokens[] = ['type' => 'string', 'value' => $buf];
                continue;
            }

            if (ctype_digit($ch)) {
                $buf = $ch;
                while ($i + 1 < $len && (ctype_digit($s[$i + 1]) || $s[$i + 1] === '.')) {
                    $buf .= $s[$i + 1];
                    $i++;
                }
                $tokens[] = ['type' => 'number', 'value' => $buf];
                continue;
            }

            if (ctype_alpha($ch) || $ch === '_') {
                $buf = $ch;
                while ($i + 1 < $len && (ctype_alnum($s[$i + 1]) || $s[$i + 1] === '_')) {
                    $buf .= $s[$i + 1];
                    $i++;
                }
                $tokens[] = ['type' => 'ident', 'value' => $buf];
                continue;
            }

            $tokens[] = ['type' => 'other', 'value' => $ch];
        }

        return $tokens;
    }

    /**
     * Parse a single quoted string argument from tokens starting at $start.
     * Expects: '(' string ')'. Returns ['value' => string, 'endIndex' => int] or null.
     */
    private function parseSingleStringArg(array $tokens, int $start): ?array
    {
        $t0 = $tokens[$start] ?? null;
        $t1 = $tokens[$start + 1] ?? null;
        $t2 = $tokens[$start + 2] ?? null;
        if (($t0['value'] ?? null) !== '(') {
            return null;
        }
        if (($t1['type'] ?? null) !== 'string') {
            return null;
        }
        if (($t2['value'] ?? null) !== ')') {
            return null;
        }

        $raw = $t1['value'];
        $quote = $raw[0] ?? '';
        if ($quote !== '"' && $quote !== "'") {
            return null;
        }
        $val = substr($raw, 1, -1);

        return [
            'value' => $val,
            'endIndex' => $start + 2,
        ];
    }

    private function unquoteStringArgument(string $value): ?string
    {
        $value = trim($value);
        if ($value === '') {
            return null;
        }

        $quote = $value[0];
        if ($quote !== '"' && $quote !== "'") {
            return null;
        }

        if (substr($value, -1) !== $quote) {
            return null;
        }

        $content = substr($value, 1, -1);
        return stripcslashes($content);
    }
}
