<?php

namespace App\Services;

use App\Models\Rule\RuleDefModel;
use App\Models\Test\TestDefSiteModel;

class RuleEngineService
{
    protected RuleDefModel $ruleDefModel;
    protected RuleExpressionService $expr;

    public function __construct()
    {
        $this->ruleDefModel = new RuleDefModel();
        $this->expr = new RuleExpressionService();
    }

    /**
     * Run rules for an event.
     *
     * Expected context keys:
     * - order: array (must include InternalOID)
     * - patient: array (optional, includes Sex, Age, etc.)
     * - tests: array (optional, patres rows)
     *
     * @param string $eventCode The event that triggered rule execution
     * @param array $context Context data for rule evaluation
     * @return void
     */
    public function run(string $eventCode, array $context = []): void
    {
        $order = $context['order'] ?? null;
        $testSiteID = $context['testSiteID'] ?? null;

        if (is_array($order) && isset($order['TestSiteID']) && $testSiteID === null) {
            $testSiteID = is_numeric($order['TestSiteID']) ? (int) $order['TestSiteID'] : null;
        }

        $rules = $this->ruleDefModel->getActiveByEvent($eventCode, $testSiteID);
        if (empty($rules)) {
            return;
        }

        foreach ($rules as $rule) {
            $rid = (int) ($rule['RuleID'] ?? 0);
            if ($rid === 0) {
                continue;
            }

            try {
                // Rules must have compiled expressions
                $compiled = null;
                if (!empty($rule['ConditionExprCompiled'])) {
                    $compiled = json_decode($rule['ConditionExprCompiled'], true);
                }

                if (empty($compiled) || !is_array($compiled)) {
                    log_message('warning', 'Rule ' . $rid . ' has no compiled expression, skipping');
                    continue;
                }

                // Evaluate condition
                $conditionExpr = $compiled['conditionExpr'] ?? 'true';
                $matches = $this->evaluateCondition($conditionExpr, $context);
                
                if (!$matches) {
                    // Execute else actions
                    $actions = $compiled['else'] ?? [];
                } else {
                    // Execute then actions
                    $actions = $compiled['then'] ?? [];
                }

                // Execute all actions
                foreach ($actions as $action) {
                    $this->executeAction($action, $context);
                }
            } catch (\Throwable $e) {
                log_message('error', 'Rule engine error (RuleID=' . $rid . '): ' . $e->getMessage());
                continue;
            }
        }
    }

    /**
     * Evaluate a condition expression
     * Handles special functions like requested() by querying the database
     */
    private function evaluateCondition(string $conditionExpr, array $context): bool
    {
        // Handle requested() function(s) by querying database
        $conditionExpr = preg_replace_callback(
            '/requested\s*\(\s*["\']([^"\']+)["\']\s*\)/i',
            function (array $m) use ($context) {
                $testCode = $m[1] ?? '';
                if ($testCode === '') {
                    return 'false';
                }
                return $this->isTestRequested($testCode, $context) ? 'true' : 'false';
            },
            $conditionExpr
        );

        return $this->expr->evaluateBoolean($conditionExpr, $context);
    }

    /**
     * Check if a test was requested for the current order
     */
    private function isTestRequested(string $testCode, array $context): bool
    {
        $order = $context['order'] ?? null;
        if (!is_array($order) || empty($order['InternalOID'])) {
            return false;
        }

        $internalOID = (int) $order['InternalOID'];
        $db = \Config\Database::connect();

        // Query patres to check if test with given code exists
        $result = $db->table('patres')
            ->select('patres.*, testdefsite.TestSiteCode')
            ->join('testdefsite', 'testdefsite.TestSiteID = patres.TestSiteID', 'inner')
            ->where('patres.OrderID', $internalOID)
            ->where('testdefsite.TestSiteCode', $testCode)
            ->where('patres.DelDate', null)
            ->where('testdefsite.EndDate', null)
            ->get()
            ->getRow();

        return $result !== null;
    }

    /**
     * Execute an action based on its type
     */
    protected function executeAction(array $action, array $context): void
    {
        $type = strtoupper((string) ($action['type'] ?? ''));

        switch ($type) {
            case 'RESULT_SET':
            case 'SET_RESULT': // legacy
                $this->executeSetResult($action, $context);
                break;
            case 'TEST_INSERT':
            case 'INSERT_TEST': // legacy
                $this->executeInsertTest($action, $context);
                break;
            case 'TEST_DELETE':
                $this->executeDeleteTest($action, $context);
                break;
            case 'COMMENT_INSERT':
            case 'ADD_COMMENT': // legacy
                $this->executeAddComment($action, $context);
                break;
            case 'NO_OP':
                // Do nothing
                break;
            default:
                log_message('warning', 'Unknown action type: ' . $type);
                break;
        }
    }

    /**
     * Execute SET_RESULT action
     */
    protected function executeSetResult(array $action, array $context): void
    {
        $order = $context['order'] ?? null;
        if (!is_array($order) || empty($order['InternalOID'])) {
            throw new \Exception('SET_RESULT requires context.order.InternalOID');
        }

        $internalOID = (int) $order['InternalOID'];
        $testSiteID = $context['testSiteID'] ?? null;

        if ($testSiteID === null && isset($order['TestSiteID'])) {
            $testSiteID = is_numeric($order['TestSiteID']) ? (int) $order['TestSiteID'] : null;
        }

        $testCode = $action['testCode'] ?? null;
        if ($testCode !== null) {
            $resolvedId = $this->resolveTestSiteIdByCode($testCode);
            if ($resolvedId === null) {
                throw new \Exception('SET_RESULT unknown test code: ' . $testCode);
            }
            $testSiteID = $resolvedId;
        }

        if ($testSiteID === null) {
            throw new \Exception('SET_RESULT requires testSiteID');
        }

        // Get the value
        if (isset($action['valueExpr']) && is_string($action['valueExpr'])) {
            $value = $this->expr->evaluate($action['valueExpr'], $context);
        } else {
            $value = $action['value'] ?? null;
        }

        $testSiteCode = $testCode ?? $this->resolveTestSiteCode($testSiteID);

        $db = \Config\Database::connect();

        // Check if patres row exists
        $patres = $db->table('patres')
            ->where('OrderID', $internalOID)
            ->where('TestSiteID', $testSiteID)
            ->where('DelDate', null)
            ->get()
            ->getRowArray();

        if ($patres) {
            // Update existing result
            $ok = $db->table('patres')
                ->where('OrderID', $internalOID)
                ->where('TestSiteID', $testSiteID)
                ->where('DelDate', null)
                ->update(['Result' => $value]);
        } else {
            // Insert new result row
            $ok = $db->table('patres')->insert([
                'OrderID' => $internalOID,
                'TestSiteID' => $testSiteID,
                'TestSiteCode' => $testSiteCode,
                'Result' => $value,
                'CreateDate' => date('Y-m-d H:i:s'),
            ]);
        }

        if ($ok === false) {
            throw new \Exception('SET_RESULT update/insert failed');
        }
    }

    /**
     * Execute INSERT_TEST action - Insert a new test into patres
     */
    protected function executeInsertTest(array $action, array $context): void
    {
        $order = $context['order'] ?? null;
        if (!is_array($order) || empty($order['InternalOID'])) {
            throw new \Exception('INSERT_TEST requires context.order.InternalOID');
        }

        $internalOID = (int) $order['InternalOID'];
        $testCode = $action['testCode'] ?? null;

        if (empty($testCode)) {
            throw new \Exception('INSERT_TEST requires testCode');
        }

        // Look up TestSiteID from TestSiteCode
        $testDefSiteModel = new TestDefSiteModel();
        $testSite = $testDefSiteModel->where('TestSiteCode', $testCode)
            ->where('EndDate', null)
            ->first();

        if (!$testSite || empty($testSite['TestSiteID'])) {
            throw new \Exception('INSERT_TEST: Test not found with code: ' . $testCode);
        }

        $testSiteID = (int) $testSite['TestSiteID'];

        $db = \Config\Database::connect();

        // Check if test already exists (avoid duplicates)
        $existing = $db->table('patres')
            ->where('OrderID', $internalOID)
            ->where('TestSiteID', $testSiteID)
            ->where('DelDate', null)
            ->get()
            ->getRow();

        if ($existing) {
            // Test already exists, skip
            return;
        }

        // Insert new test row
        $ok = $db->table('patres')->insert([
            'OrderID' => $internalOID,
            'TestSiteID' => $testSiteID,
            'TestSiteCode' => $testCode,
            'CreateDate' => date('Y-m-d H:i:s'),
        ]);

        if ($ok === false) {
            throw new \Exception('INSERT_TEST insert failed');
        }
    }

    /**
     * Execute ADD_COMMENT action - Add a comment to the order
     */
    protected function executeAddComment(array $action, array $context): void
    {
        $order = $context['order'] ?? null;
        if (!is_array($order) || empty($order['InternalOID'])) {
            throw new \Exception('ADD_COMMENT requires context.order.InternalOID');
        }

        $internalOID = (int) $order['InternalOID'];
        $comment = $action['comment'] ?? null;

        if (empty($comment)) {
            throw new \Exception('ADD_COMMENT requires comment');
        }

        $db = \Config\Database::connect();

        // Insert comment into ordercom table
        $ok = $db->table('ordercom')->insert([
            'InternalOID' => $internalOID,
            'Comment' => $comment,
            'CreateDate' => date('Y-m-d H:i:s'),
        ]);

        if ($ok === false) {
            throw new \Exception('ADD_COMMENT insert failed');
        }
    }

    /**
     * Execute TEST_DELETE action - soft delete a test from patres
     */
    protected function executeDeleteTest(array $action, array $context): void
    {
        $order = $context['order'] ?? null;
        if (!is_array($order) || empty($order['InternalOID'])) {
            throw new \Exception('TEST_DELETE requires context.order.InternalOID');
        }

        $internalOID = (int) $order['InternalOID'];
        $testCode = $action['testCode'] ?? null;
        if (empty($testCode)) {
            throw new \Exception('TEST_DELETE requires testCode');
        }

        $testDefSiteModel = new TestDefSiteModel();
        $testSite = $testDefSiteModel->where('TestSiteCode', $testCode)
            ->where('EndDate', null)
            ->first();

        if (!$testSite || empty($testSite['TestSiteID'])) {
            // Unknown test code: no-op
            return;
        }

        $testSiteID = (int) $testSite['TestSiteID'];
        $db = \Config\Database::connect();

        // Soft delete matching patres row(s)
        $db->table('patres')
            ->where('OrderID', $internalOID)
            ->where('TestSiteID', $testSiteID)
            ->where('DelDate', null)
            ->update(['DelDate' => date('Y-m-d H:i:s')]);
    }

    private function resolveTestSiteCode(int $testSiteID): ?string
    {
        try {
            $testDefSiteModel = new TestDefSiteModel();
            $row = $testDefSiteModel->where('TestSiteID', $testSiteID)->where('EndDate', null)->first();
            return $row['TestSiteCode'] ?? null;
        } catch (\Throwable $e) {
            return null;
        }
    }

    private function resolveTestSiteIdByCode(string $testSiteCode): ?int
    {
        try {
            $testDefSiteModel = new TestDefSiteModel();
            $row = $testDefSiteModel->where('TestSiteCode', $testSiteCode)->where('EndDate', null)->first();
            if (empty($row['TestSiteID'])) {
                return null;
            }

            return (int) $row['TestSiteID'];
        } catch (\Throwable $e) {
            return null;
        }
    }
}
