<?php

namespace App\Services\FlowTester;

/**
 * 조건 표현식 평가기
 *
 * 플로우 스텝의 condition 필드를 평가하여 실행 여부를 결정합니다.
 *
 * 지원하는 조건 타입:
 * 1. 문자열 표현식: "{{step.field}} == 'value'"
 * 2. 비교 객체: {"left": "{{step.field}}", "op": "==", "right": "value"}
 * 3. 논리 연산: {"and": [...]} / {"or": [...]} / {"not": {...}}
 * 4. 존재 확인: {"exists": "{{step.field}}"} / {"notExists": ...}
 * 5. 스텝 결과: {"stepResult": "step_id", "is": "success|failure|skipped"}
 * 6. 값 포함: {"left": "{{step.field}}", "op": "in", "right": ["a", "b"]}
 *
 * 사용 예시:
 * - 로그인 성공 시만 실행: {"stepResult": "login", "is": "success"}
 * - 관리자만 실행: "{{login.response.role}} == 'admin'"
 * - 금액 조건: {"left": "{{order.total}}", "op": ">", "right": 100000}
 * - 복합 조건: {"and": [{"stepResult": "login", "is": "success"}, "{{login.role}} == 'admin'"]}
 */
class ConditionEvaluator
{
    private VariableBinder $binder;

    /**
     * 스텝 실행 결과 맵 (stepId => ['success' => bool, 'status' => int, 'skipped' => bool])
     */
    private array $stepResults = [];

    /**
     * 평가 로그
     */
    private array $evaluationLog = [];

    public function __construct(?VariableBinder $binder = null)
    {
        $this->binder = $binder ?? new VariableBinder;
    }

    /**
     * VariableBinder 설정
     */
    public function setBinder(VariableBinder $binder): void
    {
        $this->binder = $binder;
    }

    /**
     * 스텝 결과 등록
     *
     * @param  string  $stepId  스텝 ID
     * @param  array  $result  스텝 실행 결과
     */
    public function setStepResult(string $stepId, array $result): void
    {
        $this->stepResults[$stepId] = [
            'success' => $result['success'] ?? false,
            'status' => $result['response']['status'] ?? 0,
            'skipped' => $result['skipped'] ?? false,
            'executed' => ! ($result['skipped'] ?? false),
            'response' => $result['response']['body'] ?? null,
            'extracted' => $result['extracted'] ?? [],
        ];
    }

    /**
     * 조건 평가
     *
     * @param  mixed  $condition  조건 (문자열 또는 배열)
     * @return array ['passed' => bool, 'reason' => string, 'evaluated' => string]
     */
    public function evaluate(mixed $condition): array
    {
        $this->evaluationLog = [];

        if ($condition === null || $condition === true) {
            return $this->result(true, 'No condition (always run)');
        }

        if ($condition === false) {
            return $this->result(false, 'Condition is explicitly false');
        }

        try {
            if (is_string($condition)) {
                return $this->evaluateStringExpression($condition);
            }

            if (is_array($condition)) {
                return $this->evaluateArrayCondition($condition);
            }

            return $this->result(false, 'Invalid condition type: '.gettype($condition));
        } catch (\Exception $e) {
            return $this->result(false, 'Evaluation error: '.$e->getMessage());
        }
    }

    /**
     * 문자열 표현식 평가
     *
     * 형식: "{{step.field}} op value"
     * 예: "{{login.success}} == true"
     *     "{{order.total}} > 100000"
     *     "{{user.role}} != 'guest'"
     */
    private function evaluateStringExpression(string $expression): array
    {
        // 변수 바인딩 적용
        $boundExpression = $this->binder->bind($expression);
        $this->log("Expression: {$expression} → {$boundExpression}");

        // 연산자 패턴 매칭
        $operators = ['===', '!==', '==', '!=', '>=', '<=', '>', '<', ' contains ', ' in ', ' matches '];

        foreach ($operators as $op) {
            $trimmedOp = trim($op);
            if (str_contains($boundExpression, $op)) {
                $parts = explode($op, $boundExpression, 2);
                if (count($parts) === 2) {
                    $left = $this->parseValue(trim($parts[0]));
                    $right = $this->parseValue(trim($parts[1]));

                    $result = $this->compare($left, $trimmedOp, $right);
                    $this->log("Compare: {$left} {$trimmedOp} {$right} = ".($result ? 'true' : 'false'));

                    return $this->result($result, "{$expression} → ".($result ? 'true' : 'false'));
                }
            }
        }

        // 연산자가 없으면 truthy 체크
        $value = $this->parseValue($boundExpression);
        $result = $this->isTruthy($value);

        return $this->result($result, "Truthy check: {$boundExpression} → ".($result ? 'true' : 'false'));
    }

    /**
     * 배열 조건 평가
     */
    private function evaluateArrayCondition(array $condition): array
    {
        // 1. 논리 연산자: and, or, not
        if (isset($condition['and'])) {
            return $this->evaluateAnd($condition['and']);
        }

        if (isset($condition['or'])) {
            return $this->evaluateOr($condition['or']);
        }

        if (isset($condition['not'])) {
            $inner = $this->evaluate($condition['not']);

            return $this->result(! $inner['passed'], "NOT ({$inner['reason']})");
        }

        // 2. 스텝 결과 조건
        if (isset($condition['stepResult'])) {
            return $this->evaluateStepResult($condition);
        }

        // 3. 존재 확인
        if (isset($condition['exists'])) {
            return $this->evaluateExists($condition['exists'], true);
        }

        if (isset($condition['notExists'])) {
            return $this->evaluateExists($condition['notExists'], false);
        }

        // 4. 빈 값 확인
        if (isset($condition['isEmpty'])) {
            return $this->evaluateEmpty($condition['isEmpty'], true);
        }

        if (isset($condition['isNotEmpty'])) {
            return $this->evaluateEmpty($condition['isNotEmpty'], false);
        }

        // 5. 비교 연산 객체: {left, op, right}
        if (isset($condition['left']) && isset($condition['op'])) {
            return $this->evaluateComparisonObject($condition);
        }

        // 6. 타입 체크
        if (isset($condition['isType'])) {
            return $this->evaluateType($condition['value'] ?? '', $condition['isType']);
        }

        // 7. 모든 조건 (all - and의 별칭)
        if (isset($condition['all'])) {
            return $this->evaluateAnd($condition['all']);
        }

        // 8. 하나라도 (any - or의 별칭)
        if (isset($condition['any'])) {
            return $this->evaluateOr($condition['any']);
        }

        return $this->result(false, 'Unknown condition structure: '.json_encode($condition));
    }

    /**
     * AND 조건 평가
     */
    private function evaluateAnd(array $conditions): array
    {
        $reasons = [];

        foreach ($conditions as $i => $cond) {
            $result = $this->evaluate($cond);
            $reasons[] = "[{$i}] ".($result['passed'] ? '✓' : '✗').' '.$result['reason'];

            if (! $result['passed']) {
                return $this->result(false, 'AND failed: '.implode(', ', $reasons));
            }
        }

        return $this->result(true, 'AND passed: '.implode(', ', $reasons));
    }

    /**
     * OR 조건 평가
     */
    private function evaluateOr(array $conditions): array
    {
        $reasons = [];

        foreach ($conditions as $i => $cond) {
            $result = $this->evaluate($cond);
            $reasons[] = "[{$i}] ".($result['passed'] ? '✓' : '✗').' '.$result['reason'];

            if ($result['passed']) {
                return $this->result(true, 'OR passed: '.implode(', ', $reasons));
            }
        }

        return $this->result(false, 'OR failed: '.implode(', ', $reasons));
    }

    /**
     * 스텝 결과 조건 평가
     *
     * {"stepResult": "login", "is": "success|failure|skipped|executed"}
     */
    private function evaluateStepResult(array $condition): array
    {
        $stepId = $condition['stepResult'];
        $expected = $condition['is'] ?? 'success';

        if (! isset($this->stepResults[$stepId])) {
            return $this->result(false, "Step '{$stepId}' has not been executed yet");
        }

        $stepResult = $this->stepResults[$stepId];

        $passed = match ($expected) {
            'success' => $stepResult['success'] === true,
            'failure', 'failed' => $stepResult['success'] === false && ! $stepResult['skipped'],
            'skipped' => $stepResult['skipped'] === true,
            'executed' => $stepResult['executed'] === true,
            default => false,
        };

        $actual = $stepResult['skipped'] ? 'skipped' : ($stepResult['success'] ? 'success' : 'failure');

        return $this->result(
            $passed,
            "Step '{$stepId}' is {$actual}, expected: {$expected}"
        );
    }

    /**
     * 존재 확인 평가
     */
    private function evaluateExists(string $path, bool $shouldExist): array
    {
        $value = $this->binder->bind($path);

        // 바인딩 후에도 변수 패턴이 남아있으면 존재하지 않는 것
        $exists = ! str_contains($value, '{{') && $value !== '' && $value !== null;

        $passed = $shouldExist ? $exists : ! $exists;
        $status = $shouldExist ? 'exists' : 'notExists';

        return $this->result($passed, "{$path} {$status}: ".($exists ? 'yes' : 'no'));
    }

    /**
     * 빈 값 확인 평가
     */
    private function evaluateEmpty(string $path, bool $shouldBeEmpty): array
    {
        $value = $this->binder->bind($path);
        $parsedValue = $this->parseValue($value);

        $isEmpty = empty($parsedValue) || $parsedValue === '' || $parsedValue === [] || $parsedValue === null;

        $passed = $shouldBeEmpty ? $isEmpty : ! $isEmpty;

        return $this->result($passed, "{$path} isEmpty: ".($isEmpty ? 'yes' : 'no'));
    }

    /**
     * 비교 연산 객체 평가
     *
     * {"left": "{{step.field}}", "op": "==", "right": "value"}
     */
    private function evaluateComparisonObject(array $condition): array
    {
        $left = $this->binder->bind($condition['left'] ?? '');
        $op = $condition['op'] ?? '==';
        $right = $condition['right'] ?? null;

        // right도 변수일 수 있음
        if (is_string($right) && str_contains($right, '{{')) {
            $right = $this->binder->bind($right);
        }

        $leftValue = $this->parseValue($left);
        $rightValue = is_string($right) ? $this->parseValue($right) : $right;

        $result = $this->compare($leftValue, $op, $rightValue);

        $leftDisplay = is_array($leftValue) ? json_encode($leftValue) : $leftValue;
        $rightDisplay = is_array($rightValue) ? json_encode($rightValue) : $rightValue;

        return $this->result($result, "{$leftDisplay} {$op} {$rightDisplay} → ".($result ? 'true' : 'false'));
    }

    /**
     * 타입 체크 평가
     */
    private function evaluateType(string $path, string $expectedType): array
    {
        $value = $this->binder->bind($path);
        $parsedValue = $this->parseValue($value);

        $actualType = gettype($parsedValue);

        $passed = match ($expectedType) {
            'number', 'numeric' => is_numeric($parsedValue),
            'integer', 'int' => is_int($parsedValue) || (is_string($parsedValue) && ctype_digit($parsedValue)),
            'string' => is_string($parsedValue),
            'array' => is_array($parsedValue),
            'object' => is_object($parsedValue) || (is_array($parsedValue) && ! array_is_list($parsedValue)),
            'boolean', 'bool' => is_bool($parsedValue) || in_array(strtolower((string) $parsedValue), ['true', 'false']),
            'null' => $parsedValue === null || strtolower((string) $parsedValue) === 'null',
            default => $actualType === $expectedType,
        };

        return $this->result($passed, "{$path} isType {$expectedType}: actual={$actualType}");
    }

    /**
     * 값 비교
     */
    private function compare(mixed $left, string $op, mixed $right): bool
    {
        return match ($op) {
            '==', '=' => $left == $right,
            '===' => $left === $right,
            '!=', '<>' => $left != $right,
            '!==' => $left !== $right,
            '>' => is_numeric($left) && is_numeric($right) && $left > $right,
            '>=' => is_numeric($left) && is_numeric($right) && $left >= $right,
            '<' => is_numeric($left) && is_numeric($right) && $left < $right,
            '<=' => is_numeric($left) && is_numeric($right) && $left <= $right,
            'contains' => $this->contains($left, $right),
            'notContains', 'not_contains' => ! $this->contains($left, $right),
            'in' => $this->in($left, $right),
            'notIn', 'not_in' => ! $this->in($left, $right),
            'startsWith', 'starts_with' => is_string($left) && is_string($right) && str_starts_with($left, $right),
            'endsWith', 'ends_with' => is_string($left) && is_string($right) && str_ends_with($left, $right),
            'matches', 'regex' => is_string($left) && is_string($right) && preg_match($right, $left),
            default => false,
        };
    }

    /**
     * 포함 여부 확인
     */
    private function contains(mixed $haystack, mixed $needle): bool
    {
        if (is_string($haystack)) {
            return str_contains($haystack, (string) $needle);
        }

        if (is_array($haystack)) {
            return in_array($needle, $haystack);
        }

        return false;
    }

    /**
     * 배열 내 포함 여부 확인
     */
    private function in(mixed $needle, mixed $haystack): bool
    {
        if (! is_array($haystack)) {
            return false;
        }

        return in_array($needle, $haystack);
    }

    /**
     * 문자열 값 파싱
     */
    private function parseValue(string $value): mixed
    {
        $trimmed = trim($value);

        // 따옴표로 감싸진 문자열
        if ((str_starts_with($trimmed, "'") && str_ends_with($trimmed, "'")) ||
            (str_starts_with($trimmed, '"') && str_ends_with($trimmed, '"'))) {
            return substr($trimmed, 1, -1);
        }

        // boolean
        if (strtolower($trimmed) === 'true') {
            return true;
        }
        if (strtolower($trimmed) === 'false') {
            return false;
        }

        // null
        if (strtolower($trimmed) === 'null') {
            return null;
        }

        // 숫자
        if (is_numeric($trimmed)) {
            return str_contains($trimmed, '.') ? (float) $trimmed : (int) $trimmed;
        }

        // JSON 배열/객체
        if ((str_starts_with($trimmed, '[') && str_ends_with($trimmed, ']')) ||
            (str_starts_with($trimmed, '{') && str_ends_with($trimmed, '}'))) {
            $decoded = json_decode($trimmed, true);
            if (json_last_error() === JSON_ERROR_NONE) {
                return $decoded;
            }
        }

        return $trimmed;
    }

    /**
     * Truthy 체크
     */
    private function isTruthy(mixed $value): bool
    {
        if ($value === null || $value === false || $value === '' || $value === 0 || $value === '0') {
            return false;
        }

        if (is_array($value) && empty($value)) {
            return false;
        }

        return true;
    }

    /**
     * 결과 생성
     */
    private function result(bool $passed, string $reason): array
    {
        return [
            'passed' => $passed,
            'reason' => $reason,
            'log' => $this->evaluationLog,
        ];
    }

    /**
     * 평가 로그 추가
     */
    private function log(string $message): void
    {
        $this->evaluationLog[] = $message;
    }

    /**
     * 상태 초기화
     */
    public function reset(): void
    {
        $this->stepResults = [];
        $this->evaluationLog = [];
    }

    /**
     * 스텝 결과 조회
     */
    public function getStepResults(): array
    {
        return $this->stepResults;
    }
}
