<?php

namespace Components\Common;

/**
 * Utility to determine if a blueprint field/item is applicable to the current environment.
 */
class FieldApplicability
{
    /**
     * @param array $def Field or item definition that may contain 'applicable' or 'applicable_if'
     * @return bool
     */
    public static function isApplicable(array $def): bool
    {
        $cond = $def['applicable'] ?? ($def['applicable_if'] ?? null);
        if (!$cond) {
            return true;
        }

        return self::evaluate($cond, $def);
    }

    /**
     * Recursively evaluate applicability conditions.
     *
     * @param mixed $cond The condition(s) to evaluate
     * @param array $def The full field/item definition for context
     * @return bool
     */
    protected static function evaluate($cond, array $def): bool
    {
        // 1. Closures or direct callables (if provided programmatically)
        if ($cond instanceof \Closure) {
            return (bool)$cond($def);
        }

        // 2. Simple string: function name or Class::method
        if (is_string($cond)) {
            if (strpos($cond, '::') !== false) {
                return (bool)self::callMethod($cond, $def);
            }
            if (function_exists($cond)) {
                return (bool)call_user_func($cond, $def);
            }
            return true; // Non-callable string? Treat as applicable.
        }

        if (!is_array($cond)) {
            return true;
        }

        // 3. Array-based callable [Class, Method]
        if (isset($cond[0], $cond[1]) && count($cond) === 2 && is_string($cond[0]) && is_string($cond[1])) {
            if (is_callable($cond)) {
                return (bool)call_user_func($cond, $def);
            }
        }

        // 4. Logical Enclosures / Groups
        // List of conditions: OR by default (matching FieldVisibility)
        if (isset($cond[0]) && is_array($cond[0])) {
            foreach ($cond as $sub) {
                if (self::evaluate($sub, $def)) {
                    return true;
                }
            }
            return false;
        }

        if (isset($cond['any_of']) || isset($cond['or'])) {
            $subList = $cond['any_of'] ?? $cond['or'];
            foreach ((array)$subList as $sub) {
                if (self::evaluate($sub, $def)) {
                    return true;
                }
            }
            return false;
        }

        if (isset($cond['all_of']) || isset($cond['and'])) {
            $subList = $cond['all_of'] ?? $cond['and'];
            foreach ((array)$subList as $sub) {
                if (!self::evaluate($sub, $def)) {
                    return false;
                }
            }
            return true;
        }

        if (isset($cond['not'])) {
            return !self::evaluate($cond['not'], $def);
        }

        // 5. Explicit Callback/Method
        if (isset($cond['callback'])) {
            return self::evaluate($cond['callback'], $def);
        }
        if (isset($cond['method'])) {
            return self::evaluate($cond['method'], $def);
        }

        // 6. Built-in property checks
        // Platform restriction: { platform: 'zencart' } or { platform: ['zencart', 'xtc'] }
        if (isset($cond['platform'])) {
            $platform = $cond['platform'];
            $currentPlatform = mh_cfg('MH_PLATFORM');
            if (is_array($platform)) {
                if (!in_array($currentPlatform, $platform)) {
                    return false;
                }
            } else {
                if ($currentPlatform !== (string)$platform) {
                    return false;
                }
            }
        }

        // SaaS restriction: { saas: true } or { saas: false }
        if (isset($cond['saas'])) {
            $saasReq = (bool)$cond['saas'];
            $isSaas = (bool)mh_saas();
            if ($isSaas !== $saasReq) {
                return false;
            }
        }

        return true;
    }

    /**
     * Safely call a Class::method string.
     * Instantiates the class if it's not a static method.
     *
     * @param string $source Format: "ClassName::methodName"
     * @param array $def Context passed to the method
     * @return bool
     */
    protected static function callMethod(string $source, array $def): bool
    {
        if (strpos($source, '::') === false) {
            return true;
        }

        [$class, $method] = explode('::', $source, 2);
        $class = trim($class);
        $method = trim($method);

        if ($class === '' || $method === '' || !class_exists($class) || !method_exists($class, $method)) {
            return true;
        }

        try {
            $ref = new \ReflectionMethod($class, $method);
            if ($ref->isStatic()) {
                return (bool)($ref->getNumberOfParameters() >= 1 ? $ref->invoke(null, $def) : $ref->invoke(null));
            }

            // Non-static: try to instantiate with no arguments
            $obj = @new $class();
            return (bool)($ref->getNumberOfParameters() >= 1 ? $ref->invoke($obj, $def) : $ref->invoke($obj));
        } catch (\Throwable $e) {
            // Fallback to simple call if reflection fails
            return (bool)@call_user_func([$class, $method], $def);
        }
    }
}
