<?php

namespace modules\selfserve\classes;

require_once WD . '/modules/selfserve/interfaces/selfserve_condition_evaluator_i.php';
require_once WD . '/modules/selfserve/helpers/selfserve_condition_rule_object_type.php';
require_once WD . '/modules/selfserve/helpers/selfserve_condition_rule_type.php';
require_once WD . '/modules/selfserve/helpers/selfserve_task_gate_type.php';

use modules\selfserve\helpers\selfserve_condition_rule_object_type;
use modules\selfserve\helpers\selfserve_condition_rule_type;
use modules\selfserve\helpers\selfserve_task_gate_type;
use modules\selfserve\interfaces\selfserve_condition_evaluator_i;

class selfserve_condition_evaluator implements selfserve_condition_evaluator_i
{
    private const V2_OPERATORS = [
        'IS_TRUE',
        'IS_FALSE',
        'IS_SET',
        'IS_TRUE_OR_NOT_SET',
        'IS_FALSE_OR_NOT_SET',
    ];

    public function evaluate(array $conditions, array $rules, array $answers): array
    {
        $rulesByConditionId = [];
        foreach ($rules as $rule) {
            $conditionId = (int)($rule['condition_id'] ?? 0);
            if ($conditionId <= 0) {
                continue;
            }
            $rulesByConditionId[$conditionId][] = $rule;
        }

        $results = [];
        $resolving = [];
        $resolver = function (int $conditionId) use (&$resolver, &$results, &$resolving, $rulesByConditionId, $answers): bool {
            if (array_key_exists($conditionId, $results)) {
                return $results[$conditionId];
            }
            if (isset($resolving[$conditionId])) {
                return false;
            }

            $resolving[$conditionId] = true;
            $conditionRules = $rulesByConditionId[$conditionId] ?? [];
            if ($conditionRules === []) {
                unset($resolving[$conditionId]);
                $results[$conditionId] = false;
                return false;
            }

            $orRules = [];
            $andRules = [];
            foreach ($conditionRules as $rule) {
                $ruleType = selfserve_condition_rule_type::tryFrom((string)($rule['type'] ?? ''));
                if ($ruleType === selfserve_condition_rule_type::IS_TRUE_OR_ANY_TRUE) {
                    $orRules[] = $rule;
                    continue;
                }
                $andRules[] = $rule;
            }

            $andSatisfied = true;
            foreach ($andRules as $rule) {
                if (!$this->isRuleSatisfied($rule, $answers, $resolver)) {
                    $andSatisfied = false;
                    break;
                }
            }

            $orSatisfied = true;
            if ($orRules !== []) {
                $orSatisfied = false;
                foreach ($orRules as $rule) {
                    if ($this->isRuleSatisfied($rule, $answers, $resolver)) {
                        $orSatisfied = true;
                        break;
                    }
                }
            }

            unset($resolving[$conditionId]);
            $results[$conditionId] = $andSatisfied && $orSatisfied;
            return $results[$conditionId];
        };

        foreach ($conditions as $condition) {
            $conditionId = (int)($condition['id'] ?? 0);
            if ($conditionId <= 0) {
                continue;
            }
            $resolver($conditionId);
        }

        return $results;
    }

    public function evaluateExpressions(array $conditions, array $answers): array
    {
        return $this->evaluateExpressionsWithTrace($conditions, $answers)['results'];
    }

    public function evaluateExpressionsWithTrace(array $conditions, array $answers): array
    {
        $conditionsById = [];
        foreach ($conditions as $condition) {
            $conditionId = (int)($condition['id'] ?? 0);
            if ($conditionId > 0) {
                $conditionsById[$conditionId] = $condition;
            }
        }

        $results = [];
        $trace = [];
        $resolving = [];

        $resolver = function (int $conditionId) use (&$resolver, &$results, &$trace, &$resolving, $conditionsById, $answers): bool {
            if (array_key_exists($conditionId, $results)) {
                return $results[$conditionId];
            }
            if (isset($resolving[$conditionId])) {
                $results[$conditionId] = false;
                $trace[$conditionId] = [
                    'type' => 'cycle',
                    'condition_id' => $conditionId,
                    'result' => false,
                    'reason' => 'Condition dependency cycle detected.',
                ];
                return false;
            }

            $condition = $conditionsById[$conditionId] ?? null;
            if (!is_array($condition)) {
                $results[$conditionId] = false;
                $trace[$conditionId] = [
                    'type' => 'missing_condition',
                    'condition_id' => $conditionId,
                    'result' => false,
                    'reason' => 'Condition was not found.',
                ];
                return false;
            }

            $expression = is_array($condition['expression'] ?? null)
                ? (array)$condition['expression']
                : $this->emptyExpression();

            $resolving[$conditionId] = true;
            $evaluated = $this->evaluateExpressionNode($expression, $answers, $resolver);
            unset($resolving[$conditionId]);

            $results[$conditionId] = (bool)($evaluated['result'] ?? false);
            $previousTrace = $trace[$conditionId] ?? null;
            $trace[$conditionId] = [
                'type' => 'condition',
                'condition_id' => $conditionId,
                'result' => $results[$conditionId],
                'expression' => $evaluated,
            ];
            if (is_array($previousTrace) && ($previousTrace['type'] ?? null) === 'cycle') {
                $trace[$conditionId]['cycle'] = $previousTrace;
            }
            return $results[$conditionId];
        };

        foreach (array_keys($conditionsById) as $conditionId) {
            $resolver((int)$conditionId);
        }

        return [
            'results' => $results,
            'trace' => $trace,
        ];
    }

    public function taskGateSatisfied(?int $gateId, array $conditionResults, array $answers): bool
    {
        if ($gateId === null || $gateId <= 0) {
            return true;
        }
        if (array_key_exists($gateId, $conditionResults)) {
            return $conditionResults[$gateId] === true;
        }
        return ($answers[$gateId] ?? null) === true;
    }

    public function taskGateSatisfiedTyped(string $gateType, ?int $gateRefId, array $conditionResults, array $answers): bool
    {
        $typed = selfserve_task_gate_type::tryFrom(strtoupper(trim($gateType)));
        if ($typed === null) {
            return $this->taskGateSatisfied($gateRefId, $conditionResults, $answers);
        }

        return match ($typed) {
            selfserve_task_gate_type::ALWAYS => true,
            selfserve_task_gate_type::CONDITION => ($gateRefId !== null && $gateRefId > 0) ? (($conditionResults[$gateRefId] ?? false) === true) : false,
            selfserve_task_gate_type::QUESTION => ($gateRefId !== null && $gateRefId > 0) ? (($answers[$gateRefId] ?? null) === true) : false,
        };
    }

    /**
     * @param array<string,mixed> $rule
     * @param array<int,bool|null> $answers
     * @param callable(int):bool $conditionResolver
     * @return bool
     */
    private function isRuleSatisfied(array $rule, array $answers, callable $conditionResolver): bool
    {
        $ruleType = selfserve_condition_rule_type::tryFrom((string)($rule['type'] ?? ''));
        if ($ruleType === null) {
            return false;
        }

        $objectType = selfserve_condition_rule_object_type::tryFrom((string)($rule['object_type'] ?? ''));
        if ($objectType === null) {
            return false;
        }

        $objectId = (int)($rule['object_id'] ?? 0);
        $value = match ($objectType) {
            selfserve_condition_rule_object_type::QUESTION => ($answers[$objectId] ?? null),
            selfserve_condition_rule_object_type::CONDITION => $conditionResolver($objectId),
        };

        return match ($ruleType) {
            selfserve_condition_rule_type::IS_TRUE,
            selfserve_condition_rule_type::IS_TRUE_OR_ANY_TRUE => $value === true,
            selfserve_condition_rule_type::IS_FALSE => $value === false,
            selfserve_condition_rule_type::IS_SET => $value !== null,
            selfserve_condition_rule_type::IS_TRUE_OR_NOT_SET => $value === true || $value === null,
            selfserve_condition_rule_type::IS_FALSE_OR_NOT_SET => $value === false || $value === null,
        };
    }

    /**
     * @param array<string,mixed> $node
     * @param array<int,bool|null> $answers
     * @param callable(int):bool $conditionResolver
     * @return array<string,mixed>
     */
    private function evaluateExpressionNode(array $node, array $answers, callable $conditionResolver): array
    {
        $type = strtolower((string)($node['type'] ?? $node['kind'] ?? 'group'));
        if ($type === 'predicate') {
            return $this->evaluateExpressionPredicate($node, $answers, $conditionResolver);
        }
        if (in_array($type, ['branch', 'if', 'if_else'], true)) {
            return $this->evaluateBranchExpression($node, $answers, $conditionResolver);
        }
        if ($type === 'case') {
            return $this->evaluateCaseExpression($node, $answers, $conditionResolver);
        }

        $operator = strtoupper((string)($node['operator'] ?? $node['mode'] ?? 'ALL'));
        if (!in_array($operator, ['ALL', 'ANY'], true)) {
            $operator = 'ALL';
        }

        $children = is_array($node['children'] ?? null) ? array_values((array)$node['children']) : [];
        if ($children === []) {
            return [
                'type' => 'group',
                'operator' => $operator,
                'result' => false,
                'children' => [],
                'reason' => 'Group has no predicates.',
            ];
        }

        $childTraces = [];
        foreach ($children as $child) {
            if (!is_array($child)) {
                continue;
            }
            $childTraces[] = $this->evaluateExpressionNode((array)$child, $answers, $conditionResolver);
        }

        if ($childTraces === []) {
            $result = false;
        } elseif ($operator === 'ANY') {
            $result = count(array_filter($childTraces, static fn(array $child): bool => ($child['result'] ?? false) === true)) > 0;
        } else {
            $result = count(array_filter($childTraces, static fn(array $child): bool => ($child['result'] ?? false) !== true)) === 0;
        }

        return [
            'type' => 'group',
            'operator' => $operator,
            'result' => $result,
            'children' => $childTraces,
            'reason' => $result ? 'Group passed.' : 'Group did not pass.',
        ];
    }

    /**
     * @param array<string,mixed> $node
     * @param array<int,bool|null> $answers
     * @param callable(int):bool $conditionResolver
     * @return array<string,mixed>
     */
    private function evaluateExpressionPredicate(array $node, array $answers, callable $conditionResolver): array
    {
        $subjectType = strtolower((string)($node['subject_type'] ?? $node['object_type'] ?? ''));
        $subjectId = (int)($node['subject_id'] ?? $node['object_id'] ?? 0);
        $operator = strtoupper((string)($node['operator'] ?? $node['rule_type'] ?? ''));

        if (!in_array($operator, self::V2_OPERATORS, true)) {
            return [
                'type' => 'predicate',
                'subject_type' => $subjectType,
                'subject_id' => $subjectId,
                'operator' => $operator,
                'actual_value' => null,
                'result' => false,
                'reason' => 'Unsupported predicate operator.',
            ];
        }

        $actual = $this->expressionSubjectValue($subjectType, $subjectId, $answers, $conditionResolver);
        $result = match ($operator) {
            'IS_TRUE' => $actual === true,
            'IS_FALSE' => $actual === false,
            'IS_SET' => $actual !== null,
            'IS_TRUE_OR_NOT_SET' => $actual === true || $actual === null,
            'IS_FALSE_OR_NOT_SET' => $actual === false || $actual === null,
            default => false,
        };

        return [
            'type' => 'predicate',
            'subject_type' => $subjectType,
            'subject_id' => $subjectId,
            'operator' => $operator,
            'actual_value' => $actual,
            'result' => $result,
            'reason' => $result ? 'Predicate passed.' : 'Predicate did not pass.',
        ];
    }

    /**
     * @param array<string,mixed> $node
     * @param array<int,bool|null> $answers
     * @param callable(int):bool $conditionResolver
     * @return array<string,mixed>
     */
    private function evaluateBranchExpression(array $node, array $answers, callable $conditionResolver): array
    {
        $branches = is_array($node['branches'] ?? null) ? array_values((array)$node['branches']) : [];
        if ($branches === []) {
            return [
                'type' => 'branch',
                'operator' => 'IF_ELSE',
                'result' => false,
                'branches' => [],
                'reason' => 'Branch has no clauses.',
            ];
        }

        $branchTraces = [];
        foreach ($branches as $index => $branch) {
            if (!is_array($branch)) {
                $branchTraces[] = [
                    'index' => $index,
                    'kind' => 'invalid',
                    'matched' => false,
                    'result' => false,
                    'reason' => 'Branch clause is invalid.',
                ];
                continue;
            }

            $kind = strtolower((string)($branch['kind'] ?? $branch['type'] ?? $branch['operator'] ?? ($index === 0 ? 'if' : 'else_if')));
            $isElse = (bool)($branch['else'] ?? false) || in_array($kind, ['else', 'default'], true);
            $whenTrace = null;
            $matched = $isElse;
            if (!$isElse) {
                $when = is_array($branch['when'] ?? null) ? (array)$branch['when'] : $this->emptyExpression();
                $whenTrace = $this->evaluateExpressionNode($when, $answers, $conditionResolver);
                $matched = (bool)($whenTrace['result'] ?? false);
            }

            if (!$matched) {
                $branchTraces[] = [
                    'index' => $index,
                    'kind' => $isElse ? 'else' : $kind,
                    'matched' => false,
                    'result' => false,
                    'when' => $whenTrace,
                    'reason' => $isElse ? 'Else branch was not reached.' : 'Branch condition did not pass.',
                ];
                continue;
            }

            $then = is_array($branch['then'] ?? null)
                ? (array)$branch['then']
                : (is_array($branch['result_expression'] ?? null) ? (array)$branch['result_expression'] : $this->emptyExpression());
            $thenTrace = $this->evaluateExpressionNode($then, $answers, $conditionResolver);
            $result = (bool)($thenTrace['result'] ?? false);
            $branchTraces[] = [
                'index' => $index,
                'kind' => $isElse ? 'else' : $kind,
                'matched' => true,
                'result' => $result,
                'when' => $whenTrace,
                'then' => $thenTrace,
                'reason' => $result ? 'Branch matched and passed.' : 'Branch matched and did not pass.',
            ];

            return [
                'type' => 'branch',
                'operator' => 'IF_ELSE',
                'result' => $result,
                'selected_index' => $index,
                'branches' => $branchTraces,
                'reason' => $result ? 'Selected branch passed.' : 'Selected branch did not pass.',
            ];
        }

        if (is_array($node['default'] ?? null)) {
            $defaultTrace = $this->evaluateExpressionNode((array)$node['default'], $answers, $conditionResolver);
            $result = (bool)($defaultTrace['result'] ?? false);
            return [
                'type' => 'branch',
                'operator' => 'IF_ELSE',
                'result' => $result,
                'branches' => $branchTraces,
                'default' => $defaultTrace,
                'reason' => $result ? 'Default branch passed.' : 'Default branch did not pass.',
            ];
        }

        return [
            'type' => 'branch',
            'operator' => 'IF_ELSE',
            'result' => false,
            'branches' => $branchTraces,
            'reason' => 'No branch matched.',
        ];
    }

    /**
     * @param array<string,mixed> $node
     * @param array<int,bool|null> $answers
     * @param callable(int):bool $conditionResolver
     * @return array<string,mixed>
     */
    private function evaluateCaseExpression(array $node, array $answers, callable $conditionResolver): array
    {
        $subjectType = strtolower((string)($node['subject_type'] ?? $node['object_type'] ?? ''));
        $subjectId = (int)($node['subject_id'] ?? $node['object_id'] ?? 0);
        $actual = $this->expressionSubjectValue($subjectType, $subjectId, $answers, $conditionResolver);
        $cases = is_array($node['cases'] ?? null) ? array_values((array)$node['cases']) : [];

        if (!in_array($subjectType, ['question', 'condition'], true) || $subjectId <= 0) {
            return [
                'type' => 'case',
                'operator' => 'CASE',
                'subject_type' => $subjectType,
                'subject_id' => $subjectId,
                'actual_value' => null,
                'result' => false,
                'cases' => [],
                'reason' => 'Case subject is invalid.',
            ];
        }

        $caseTraces = [];
        foreach ($cases as $index => $case) {
            if (!is_array($case)) {
                $caseTraces[] = [
                    'index' => $index,
                    'matched' => false,
                    'result' => false,
                    'reason' => 'Case clause is invalid.',
                ];
                continue;
            }

            $expected = $case['value'] ?? null;
            $matched = $this->caseValueMatches($expected, $actual);
            if (!$matched) {
                $caseTraces[] = [
                    'index' => $index,
                    'value' => $expected,
                    'matched' => false,
                    'result' => false,
                    'reason' => 'Case value did not match.',
                ];
                continue;
            }

            $then = is_array($case['then'] ?? null)
                ? (array)$case['then']
                : (is_array($case['result_expression'] ?? null) ? (array)$case['result_expression'] : $this->emptyExpression());
            $thenTrace = $this->evaluateExpressionNode($then, $answers, $conditionResolver);
            $result = (bool)($thenTrace['result'] ?? false);
            $caseTraces[] = [
                'index' => $index,
                'value' => $expected,
                'matched' => true,
                'result' => $result,
                'then' => $thenTrace,
                'reason' => $result ? 'Case matched and passed.' : 'Case matched and did not pass.',
            ];

            return [
                'type' => 'case',
                'operator' => 'CASE',
                'subject_type' => $subjectType,
                'subject_id' => $subjectId,
                'actual_value' => $actual,
                'result' => $result,
                'selected_index' => $index,
                'cases' => $caseTraces,
                'reason' => $result ? 'Selected case passed.' : 'Selected case did not pass.',
            ];
        }

        if (is_array($node['default'] ?? null)) {
            $defaultTrace = $this->evaluateExpressionNode((array)$node['default'], $answers, $conditionResolver);
            $result = (bool)($defaultTrace['result'] ?? false);
            return [
                'type' => 'case',
                'operator' => 'CASE',
                'subject_type' => $subjectType,
                'subject_id' => $subjectId,
                'actual_value' => $actual,
                'result' => $result,
                'cases' => $caseTraces,
                'default' => $defaultTrace,
                'reason' => $result ? 'Default case passed.' : 'Default case did not pass.',
            ];
        }

        return [
            'type' => 'case',
            'operator' => 'CASE',
            'subject_type' => $subjectType,
            'subject_id' => $subjectId,
            'actual_value' => $actual,
            'result' => false,
            'cases' => $caseTraces,
            'reason' => 'No case matched.',
        ];
    }

    /**
     * @param array<int,bool|null> $answers
     * @param callable(int):bool $conditionResolver
     */
    private function expressionSubjectValue(string $subjectType, int $subjectId, array $answers, callable $conditionResolver): ?bool
    {
        if ($subjectType === 'condition') {
            return $subjectId > 0 ? $conditionResolver($subjectId) : null;
        }
        if ($subjectType === 'question') {
            return $answers[$subjectId] ?? null;
        }
        return null;
    }

    private function caseValueMatches(mixed $expected, ?bool $actual): bool
    {
        if (is_string($expected)) {
            $normalized = strtolower(trim($expected));
            return match ($normalized) {
                'true', '1', 'yes' => $actual === true,
                'false', '0', 'no' => $actual === false,
                'null', 'unset', 'not_set', 'unanswered' => $actual === null,
                'set' => $actual !== null,
                'any', '*' => true,
                default => false,
            };
        }

        return $expected === $actual;
    }

    /**
     * @return array<string,mixed>
     */
    private function emptyExpression(): array
    {
        return [
            'type' => 'group',
            'operator' => 'ALL',
            'children' => [],
        ];
    }
}
