<?php
namespace Components\Resources\Controllers;

use Components\Form\Engine;

/**
 * BaseController providing reusable helpers for resource CRUD controllers.
 */
abstract class BaseController
{
    /**
     * Prefer GET over POST for a parameter. If neither exists, return default.
     * Optionally apply mh_* filter type when fetching.
     *
     * @param string $name
     * @param mixed $default
     * @param string|null $filter e.g. 'string', 'key', 'int'
     * @return mixed
     */
    protected static function getParam(string $name, $default = null, ?string $filter = null)
    {
        $hasGet = (\mh_get($name, null) !== null);
        $hasPost = (\mh_post($name, null) !== null);
        if ($hasGet) {
            return $filter !== null ? \mh_get($name, $default, $filter) : \mh_get($name, $default);
        }
        if ($hasPost) {
            // mh_post does not support filter type cleaning; returned raw
            return \mh_post($name, $default);
        }
        return $default;
    }

    /**
     * Parses a truthy flag string such as '1', 'true', 'on', 'yes'.
     * If the provided $raw is empty, fallback to $default.
     */
    protected static function isTruthyFlag($raw, bool $default = false): bool
    {
        $s = is_string($raw) ? strtolower(trim($raw)) : '';
        if ($s === '') { return $default; }
        return in_array($s, ['1', 'true', 'on', 'yes'], true);
    }

    /**
     * Resolve blueprint path for a module, falling back to the app blueprint.
     */
    protected static function resolveBlueprintForModule(string $module): string
    {
        $blueprintPath = null;
        try {
            if (\Components\ModuleLoader::exists($module)) {
                $blueprintPath = \Components\ModuleLoader::blueprintPath($module);
            }
        } catch (\Throwable $e) { /* ignore */ }
        if (!$blueprintPath || !is_string($blueprintPath) || !file_exists($blueprintPath)) {
            $blueprintPath = BEEZUI_ROOT . '/blueprint.yaml';
        }
        return $blueprintPath;
    }

    /**
     * Extracts keys parameter from GET/POST. Supports array syntax keys[]=A&keys[]=B.
     */
    protected static function getKeysParam(): string
    {
        $keysParam = '';
        if (\mh_get('keys', null) !== null) {
            $kp = \mh_get('keys', '', 'string');
            if ($kp === '' && is_array($_GET['keys'] ?? null)) {
                $keysParam = implode(',', array_map('strval', (array)$_GET['keys']));
            } else {
                $keysParam = (string)$kp;
            }
        } elseif (\mh_post('keys', null) !== null) {
            $kp = \mh_post('keys', '', 'string');
            if ($kp === '' && is_array($_POST['keys'] ?? null)) {
                $keysParam = implode(',', array_map('strval', (array)$_POST['keys']));
            } else {
                $keysParam = (string)$kp;
            }
        }
        return $keysParam;
    }

    /**
     * Compute allowed set of field names from keys param and engine fields list.
     * Returns an associative array [FIELD_NAME => true] or null when unrestricted.
     * Note: This preserves existing behavior relying on Engine field key casing.
     */
    protected static function computeAllowedSet(string $keysParam, array $engineFields): ?array
    {
        if ($keysParam === '') { return null; }
        $req = array_filter(array_map(function ($s) { return strtoupper(trim((string)$s)); }, preg_split('/[\s,]+/', $keysParam)));
        if (empty($req)) { return null; }
        $allowed = [];
        foreach ($engineFields as $fname => $_def) {
            if (in_array($fname, $req, true)) { $allowed[$fname] = true; }
        }
        return $allowed;
    }

    /**
     * Detect whether POST contains any resource field values (excluding control params).
     */
    protected static function hasPostedFields(array $postAll): bool
    {
        foreach ($postAll as $k => $_) {
            if ($k === 'status' || $k === 'return' || $k === 'module' || $k === 'ui_open') { continue; }
            return true;
        }
        return false;
    }

    /**
     * Apply checkbox/toggle semantics to normalized values depending on partial vs full update.
     * Mirrors existing behavior: for full forms missing checkboxes become '', for partial updates
     * absent keys are left untouched (not persisted).
     *
     * @param Engine $engine
     * @param array $normalized in/out normalized submitted values
     * @param bool $isFullForm
     * @param array|null $allowedSet
     * @return array adjusted normalized array
     */
    protected static function applyCheckboxSemantics(Engine $engine, array $normalized, bool $isFullForm, ?array $allowedSet): array
    {
        $allFields = $engine->getFields();
        foreach ($allFields as $fname => $def) {
            if (!$isFullForm && is_array($allowedSet) && !isset($allowedSet[$fname])) { continue; }
            $defType = strtolower((string)($def['type'] ?? ''));
            $vType = strtolower((string)($def['validate']['type'] ?? ''));
            if (in_array($defType, ['toggle', 'checkbox'], true) || $vType === 'boolean') {
                $present = (\mh_post($fname, null) !== null);
                if ($isFullForm) {
                    $normalized[$fname] = $present ? ($def['true_value'] ?? 'on') : ($def['false_value'] ?? '');
                } else {
                    if ($present) {
                        $normalized[$fname] = $def['true_value'] ?? 'on';
                    } else {
                        if (isset($normalized[$fname])) { unset($normalized[$fname]); }
                    }
                }
            }
        }
        return $normalized;
    }

    /* ======================== High-level helpers for controllers ======================== */

    /** Determine desired return mode with default. */
    protected static function determineReturnMode(): string
    {
        $mode = (string) self::getParam('return', '', 'string');
        return $mode ?: 'config_view';
    }

    /** Resolve drawer and autoclose flags as [drawer:int, autocloseRaw:string, enabled:bool]. */
    protected static function resolveDrawerAndAutoClose(): array
    {
        $drawerFlag = (int) self::getParam('drawer', 0, 'int');
        $autoCloseRaw = (string) self::getParam('autoclose', '', 'string');
        $autoCloseEnabled = self::isTruthyFlag($autoCloseRaw, $drawerFlag ? true : false);
        return [$drawerFlag, $autoCloseRaw, $autoCloseEnabled];
    }

    /** Adjust return mode when only status was posted. */
    protected static function finalizeReturnModeForStatus(string $returnMode, bool $hasPostedFields): string
    {
        if (!$hasPostedFields && (\mh_post('status', null) !== null) && !in_array($returnMode, ['module_card','open_state'], true)) {
            return 'module_card';
        }
        return $returnMode;
    }

    /** Convenience: Engine for module. */
    protected static function getEngineForModule(string $module): Engine
    {
        try {
            $paths = \Components\ModuleLoader::mergedBlueprintPaths($module);
            $vals = \Components\ConfigStore::getModuleConfig($module);
            return Engine::fromBlueprints($paths, is_array($vals) ? $vals : null);
        } catch (\Throwable $e) {
            return new Engine(self::resolveBlueprintForModule($module));
        }
    }

    /** Convenience: compute keys param and allowed set from an Engine. */
    protected static function getKeysAndAllowedSet(Engine $engine): array
    {
        $keysParam = self::getKeysParam();
        $allowedSet = self::computeAllowedSet($keysParam, (array)$engine->getFields());
        return [$keysParam, $allowedSet];
    }

    protected static function persistCommonParamsToGet(): void
    {
        if (\mh_get('drawer', null) !== null || \mh_post('drawer', null) !== null) {
            $_GET['drawer'] = (int) self::getParam('drawer', 0, 'int');
        }
        if (\mh_get('return', null) !== null || \mh_post('return', null) !== null) {
            $_GET['return'] = (string) self::getParam('return', '', 'string');
        }
        if (\mh_get('autoclose', null) !== null || \mh_post('autoclose', null) !== null) {
            $_GET['autoclose'] = (string) self::getParam('autoclose', '', 'string');
        }
        if (\mh_get('target', null) !== null || \mh_post('target', null) !== null) {
            $_GET['target'] = (string) self::getParam('target', '', 'key');
        }
    }

}
