<?php
// components/Common/FieldVisibility.php
namespace Components\Common;

/**
 * Common visibility evaluation used by Form Engine and Assistant Flow.
 * Supports the following visible_if structure on a field/item definition:
 *   visible_if:
 *     field: <controller_field_id>
 *     any_of: ["a", "b"]   # alias: in
 *     eq: "a"
 *     gt: 1
 *     gte: 2
 *
 *   visible_if:
 *     all_of:
 *       - { field: <controller_field_id>, ... }
 *       - { field: <controller_field_id>, ... }
 *   # all_of evaluates with AND across all listed sub-conditions
 *
 *   visible_if:
 *     - { field: <controller_field_id>, ... }
 *     - { field: <controller_field_id>, ... }
 *   # list form evaluates with OR across listed sub-conditions (legacy behavior)
 *
 * Also supports mode-based visibility for create/edit flows via the following flags on a field:
 *   - visible_on: "create" | "edit" | "both" | "view" (default: both)
 *     Supports comma-separated list, e.g. "edit, view" to hide on create.
 *   - edit_only: true (shorthand for visible_on: edit)
 *   - create_only: true (shorthand for visible_on: create)
 *
 * Evaluation uses the submitted/answered values map. For controller fields that are
 * boolean-like (toggle/checkbox), the value is normalized to 'on' when truthy.
 */
class FieldVisibility
{
    /**
     * Quick helper to parse truthy values commonly used in configs.
     */
    protected static function truthy($v): bool
    {
        if ($v === true || $v === 1) return true;
        $s = strtolower(trim((string)$v));
        return in_array($s, ['1','true','on','yes','y'], true);
    }

    /**
     * Determine whether a value should count as "set" for visibility conditions.
     *
     * @param mixed $value
     */
    protected static function hasValue($value): bool
    {
        if (is_array($value)) {
            foreach ($value as $item) {
                if (self::hasValue($item)) {
                    return true;
                }
            }
            return false;
        }

        if ($value === null) {
            return false;
        }

        if (is_bool($value)) {
            return $value;
        }

        return trim((string)$value) !== '';
    }

    /**
     * Determine visibility restrictions based purely on mode (create/edit/view).
     * If mode is empty, returns true (backward compatible).
     */
    public static function isVisibleByMode(array $def, ?string $mode): bool
    {
        $mode = strtolower((string)$mode);
        if ($mode === '') return true;
        if ($mode === 'new' || $mode === 'create') { $mode = 'create'; }
        // Accept 'edit' and 'view' as-is

        $visibleOn = strtolower((string)($def['visible_on'] ?? ''));
        $editOnly = array_key_exists('edit_only', $def) ? self::truthy($def['edit_only']) : false;
        $createOnly = array_key_exists('create_only', $def) ? self::truthy($def['create_only']) : false;

        if ($visibleOn !== '') {
            $allowed = array_map('trim', explode(',', $visibleOn));
            if (in_array('both', $allowed)) return true;
            if (in_array($mode, $allowed)) return true;
            return false;
        }
        if ($editOnly) { return $mode === 'edit'; }
        if ($createOnly) { return $mode === 'create'; }
        return true;
    }

    /**
     * @param array $def Field or item definition that may contain 'visible_if'
     * @param array $submitted Map of submitted/answered values
     * @param array $fieldMeta Optional map of field meta keyed by id (must contain 'type' when available)
     */
    public static function isVisible(array $def, array $submitted, array $fieldMeta = []): bool
    {
        // Mode-based gate first
        $mode = (string)($submitted['__mode'] ?? '');
        if (!self::isVisibleByMode($def, $mode)) return false;

        $cond = $def['visible_if'] ?? null;
        if (!$cond || !is_array($cond)) return true; // no condition -> visible

        // Support explicit AND groups: visible_if: { all_of: [ {field:...}, {field:...} ] }
        $allOf = null;
        if (isset($cond['all_of']) && is_array($cond['all_of'])) {
            $allOf = $cond['all_of'];
        } elseif (isset($cond['and']) && is_array($cond['and'])) {
            $allOf = $cond['and'];
        }
        if ($allOf !== null) {
            foreach ($allOf as $subCond) {
                if (!is_array($subCond)) continue;
                if (!self::isVisible(array_merge($def, ['visible_if' => $subCond]), $submitted, $fieldMeta)) {
                    return false;
                }
            }
            return true;
        }

        // Support array of OR'd conditions: visible_if: [ {field:..., eq:...}, {field:..., eq:...} ]
        if (isset($cond[0]) && is_array($cond[0])) {
            foreach ($cond as $subCond) {
                if (is_array($subCond) && self::isVisible(array_merge($def, ['visible_if' => $subCond]), $submitted, $fieldMeta)) {
                    return true;
                }
            }
            return false;
        }

        $controller = (string)($cond['field'] ?? '');
        if ($controller === '') return true; // invalid rule -> visible
        $raw = $submitted[$controller] ?? '';

        // Try to detect controller type for normalization
        $ctrlDef = $fieldMeta[$controller] ?? [];
        $ctrlType = strtolower((string)($ctrlDef['type'] ?? ''));

        // Normalize checkbox/toggle truthy values to 'on'
        if (in_array($ctrlType, ['toggle', 'checkbox'], true)) {
            $value = ($raw === 'on' || $raw === '1' || $raw === 'true' || $raw === true) ? 'on' : '';
        } else {
            // For arrays (multi-select) we will compare membership for any_of/in; for eq we cast to string
            $value = $raw;
        }

        // any_of / in
        $any = [];
        $anyRaw = null;
        if (isset($cond['any_of']) && is_array($cond['any_of'])) {
            $any = array_map('strval', $cond['any_of']);
            $anyRaw = $cond['any_of'];
        } elseif (isset($cond['in']) && is_array($cond['in'])) {
            $any = array_map('strval', $cond['in']);
            $anyRaw = $cond['in'];
        }
        if (!empty($any)) {
            // Special handling for toggle/checkbox so boolean lists work, e.g. any_of: [true]
            if (in_array($ctrlType, ['toggle', 'checkbox'], true)) {
                $allowed = array_map(function ($x) { return self::truthy($x); }, (array)$anyRaw);
                $valBool = ($value === 'on');
                if (in_array($valBool, $allowed, true)) return true;
            }

            if (is_array($value)) {
                foreach ($value as $v) {
                    if (in_array((string)$v, $any, true)) return true;
                }
            } else {
                if (in_array((string)$value, $any, true)) return true;
            }
        }

        // eq
        if (array_key_exists('eq', $cond)) {
            // Toggle/checkbox: compare as boolean truthiness
            if (in_array($ctrlType, ['toggle', 'checkbox'], true)) {
                $targetBool = self::truthy($cond['eq']);
                $valBool = ($value === 'on');
                if ($valBool === $targetBool) return true;
            }

            if (is_array($value)) {
                if (in_array((string)$cond['eq'], array_map('strval', $value), true)) return true;
            } else {
                if ((string)$value === (string)$cond['eq']) return true;
            }
        }

        // neq
        if (array_key_exists('neq', $cond)) {
            // Toggle/checkbox: compare as boolean truthiness
            if (in_array($ctrlType, ['toggle', 'checkbox'], true)) {
                $targetBool = self::truthy($cond['neq']);
                $valBool = ($value === 'on');
                if ($valBool !== $targetBool) return true;
            }

            if (is_array($value)) {
                if (!in_array((string)$cond['neq'], array_map('strval', $value), true)) return true;
            } else {
                if ((string)$value !== (string)$cond['neq']) return true;
            }
        }

        // has_value / not_empty
        if (array_key_exists('has_value', $cond) || array_key_exists('not_empty', $cond)) {
            $expect = array_key_exists('has_value', $cond) ? self::truthy($cond['has_value']) : self::truthy($cond['not_empty']);
            $actual = self::hasValue($value);
            if ($actual === $expect) return true;
        }

        // gt/gte numeric comparisons
        $num = null;
        if (!is_array($value) && $value !== '' && is_numeric($value)) $num = (float)$value;
        if ($num !== null) {
            if (isset($cond['gt']) && is_numeric($cond['gt']) && $num > (float)$cond['gt']) return true;
            if (isset($cond['gte']) && is_numeric($cond['gte']) && $num >= (float)$cond['gte']) return true;
        }

        return false; // none matched
    }
}
