<?php

namespace Storeplugin\PDFInvoice\Framework\Utils;

defined('ABSPATH') || exit;

/**
 * Shared helpers for field state normalization.
 */
final class FieldState
{
    /**
     * Field types that can honor readonly as a lock state.
     */
    private const READONLY_LOCKABLE_TYPES = [
        'text',
        'textarea',
        'email',
        'tel',
        'number',
        'file_upload',
        'icon_picker',
        'wysiwyg',
        'repeater',
    ];

    /**
     * Normalize boolean-like config flags.
     *
     * @param mixed $value
     * @return bool
     */
    public static function isTruthy($value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if (is_int($value) || is_float($value)) {
            return (int) $value === 1;
        }

        if (!is_string($value)) {
            return false;
        }

        return in_array(strtolower(trim($value)), ['1', 'true', 'yes', 'on'], true);
    }

    /**
     * Determine whether a field should be treated as immutable.
     *
     * Disabled always locks. Readonly only locks for field types that support
     * readonly semantics in this framework.
     *
     * @param array $field
     * @return bool
     */
    public static function isLocked(array $field): bool
    {
        if (self::isTruthy($field['disabled'] ?? false)) {
            return true;
        }

        if (!self::isTruthy($field['readonly'] ?? false)) {
            return false;
        }

        $type = (string) ($field['type'] ?? 'text');

        return self::isReadonlyLockable($type);
    }

    /**
     * Determine whether a field type supports readonly lock semantics.
     *
     * @param string $type
     * @return bool
     */
    public static function isReadonlyLockable(string $type): bool
    {
        return in_array($type, self::READONLY_LOCKABLE_TYPES, true);
    }

    /**
     * Apply a repeater's lock state to one of its child field definitions.
     *
     * This mirrors the render-time repeater behavior so save-time sanitization
     * can reason about the same effective field state.
     *
     * @param array $subField
     * @param bool  $repeaterDisabled
     * @param bool  $repeaterReadonly
     * @return array
     */
    public static function applyRepeaterLockToSubField(
        array $subField,
        bool $repeaterDisabled,
        bool $repeaterReadonly
    ): array {
        if ($repeaterDisabled) {
            $subField['disabled'] = true;
            return $subField;
        }

        if (!$repeaterReadonly || self::isTruthy($subField['disabled'] ?? false)) {
            return $subField;
        }

        $type = (string) ($subField['type'] ?? 'text');

        if (self::isReadonlyLockable($type)) {
            $subField['readonly'] = true;
            return $subField;
        }

        $subField['disabled'] = true;

        return $subField;
    }

    /**
     * Resolve a field's implicit rendered value when no explicit current/default value exists.
     *
     * @param array $field
     * @return array{has_value:bool,value:mixed}
     */
    public static function getImplicitValue(array $field): array
    {
        $type = (string) ($field['type'] ?? 'text');

        switch ($type) {
            case 'checkbox':
            case 'switch':
                return [
                    'has_value' => true,
                    'value' => isset($field['options']) && is_array($field['options']) ? [] : '0',
                ];

            case 'repeater':
                return ['has_value' => true, 'value' => []];

            case 'color':
                return ['has_value' => true, 'value' => '#7c3aed'];

            case 'range':
                return ['has_value' => true, 'value' => $field['min'] ?? 0];

            case 'select':
                $options = self::resolveImplicitOptionDefinitions($field);
                $disabledLookup = self::extractDisabledOptionLookup($options);

                if (
                    isset($field['placeholder'])
                    && is_string($field['placeholder'])
                    && $field['placeholder'] !== ''
                    && !isset($disabledLookup[''])
                ) {
                    return ['has_value' => true, 'value' => ''];
                }

                $firstOptionValue = self::getFirstEnabledOptionValue($options);
                return $firstOptionValue !== null
                    ? ['has_value' => true, 'value' => $firstOptionValue]
                    : ['has_value' => false, 'value' => null];

            case 'select2':
                if (self::isSelect2Multi($field['support'] ?? [])) {
                    return ['has_value' => true, 'value' => []];
                }

                $options = self::resolveImplicitOptionDefinitions($field);
                $disabledLookup = self::extractDisabledOptionLookup($options);

                if (
                    isset($field['placeholder'])
                    && is_string($field['placeholder'])
                    && $field['placeholder'] !== ''
                    && !isset($disabledLookup[''])
                ) {
                    return ['has_value' => true, 'value' => ''];
                }

                $firstOptionValue = self::getFirstEnabledOptionValue($options);
                return $firstOptionValue !== null
                    ? ['has_value' => true, 'value' => $firstOptionValue]
                    : ['has_value' => false, 'value' => null];

            default:
                return ['has_value' => false, 'value' => null];
        }
    }

    /**
     * Determine whether a select2 field is multi-select.
     *
     * @param mixed $support
     * @return bool
     */
    private static function isSelect2Multi($support): bool
    {
        if (is_string($support)) {
            return trim(strtolower($support)) === 'multi';
        }

        if (!is_array($support)) {
            return false;
        }

        foreach ($support as $key => $value) {
            if ($key === 'multi' && self::isTruthy($value)) {
                return true;
            }

            if (is_int($key) && (string) $value === 'multi') {
                return true;
            }
        }

        return false;
    }

    /**
     * Resolve the active normalized option definitions for implicit select values.
     *
     * @param array $field
     * @return array<string, array{label:string,disabled:bool,title:string}>
     */
    private static function resolveImplicitOptionDefinitions(array $field): array
    {
        $disabledLookup = self::normalizeDisabledOptionLookup($field['disabled_options'] ?? []);
        $baseOptions = isset($field['options']) && is_array($field['options']) ? $field['options'] : [];

        if (!isset($field['options_when']['map']) || !is_array($field['options_when']['map'])) {
            return self::normalizeOptionDefinitions($baseOptions, $disabledLookup);
        }

        $controllerValue = self::resolveOptionsWhenControllerValue($field);
        if (
            $controllerValue !== null
            && array_key_exists($controllerValue, $field['options_when']['map'])
            && is_array($field['options_when']['map'][$controllerValue])
        ) {
            return self::normalizeOptionDefinitions($field['options_when']['map'][$controllerValue], $disabledLookup);
        }

        if ($baseOptions !== []) {
            return self::normalizeOptionDefinitions($baseOptions, $disabledLookup);
        }

        return [];
    }

    /**
     * Normalize disabled option values into a string-keyed lookup.
     *
     * @param mixed $disabledOptions
     * @return array<string, bool>
     */
    private static function normalizeDisabledOptionLookup($disabledOptions): array
    {
        if (!is_array($disabledOptions) || $disabledOptions === []) {
            return [];
        }

        $lookup = [];
        foreach ($disabledOptions as $optionValue) {
            $lookup[(string) $optionValue] = true;
        }

        return $lookup;
    }

    /**
     * Normalize flat and rich option definitions into a common structure.
     *
     * @param array              $options
     * @param array<string, bool> $disabledLookup
     * @return array<string, array{label:string,disabled:bool,title:string}>
     */
    private static function normalizeOptionDefinitions(array $options, array $disabledLookup): array
    {
        $normalized = [];

        foreach ($options as $optionValue => $optionDefinition) {
            $optionKey = (string) $optionValue;
            $label = is_scalar($optionDefinition) ? (string) $optionDefinition : $optionKey;
            $disabled = isset($disabledLookup[$optionKey]);
            $title = '';

            if (is_array($optionDefinition)) {
                if (array_key_exists('label', $optionDefinition) && is_scalar($optionDefinition['label'])) {
                    $label = (string) $optionDefinition['label'];
                }

                if (array_key_exists('disabled', $optionDefinition)) {
                    $disabled = $disabled || self::isTruthy($optionDefinition['disabled']);
                }

                if (array_key_exists('title', $optionDefinition) && is_scalar($optionDefinition['title'])) {
                    $title = (string) $optionDefinition['title'];
                }
            }

            $normalized[$optionKey] = [
                'label' => $label,
                'disabled' => $disabled,
                'title' => $title,
            ];
        }

        return $normalized;
    }

    /**
     * Resolve the active controller value for options_when-aware implicit values.
     *
     * @param array $field
     * @return string|null
     */
    private static function resolveOptionsWhenControllerValue(array $field): ?string
    {
        if (empty($field['options_when']['field']) || !isset($field['_input_context']) || !is_array($field['_input_context'])) {
            return null;
        }

        $controllerField = \sanitize_key((string) $field['options_when']['field']);
        if ($controllerField === '' || !array_key_exists($controllerField, $field['_input_context'])) {
            return null;
        }

        $controllerValue = $field['_input_context'][$controllerField];
        if (is_array($controllerValue)) {
            return null;
        }

        return (string) $controllerValue;
    }

    /**
     * Extract disabled option keys from normalized option definitions.
     *
     * @param array<string, array{label:string,disabled:bool,title:string}> $options
     * @return array<string, bool>
     */
    private static function extractDisabledOptionLookup(array $options): array
    {
        $disabled = [];

        foreach ($options as $optionValue => $optionDefinition) {
            if (!empty($optionDefinition['disabled'])) {
                $disabled[(string) $optionValue] = true;
            }
        }

        return $disabled;
    }

    /**
     * Get the first enabled option key from a normalized select-style option map.
     *
     * @param array<string, array{label:string,disabled:bool,title:string}> $options
     * @return string|null
     */
    private static function getFirstEnabledOptionValue(array $options): ?string
    {
        foreach ($options as $value => $definition) {
            if (!empty($definition['disabled'])) {
                continue;
            }

            return (string) $value;
        }

        return null;
    }
}
