<?php
namespace Components\Common;

/**
 * Request helper to safely access superglobals with validation.
 *
 * This class centralizes access to $_GET, $_POST, $_REQUEST, $_SERVER and $_SESSION
 * and reuses the existing mh_clean_input() validator for robust sanitization.
 *
 * Static API by design to keep usage simple: Request::get('page'), Request::input('term'), etc.
 */
class Request
{
    /**
     * Read from $_GET with validation.
     * @param string $name
     * @param mixed $fallback
     * @param string $type
     * @return mixed
     */
    public static function get(string $name, $fallback = null, string $type = 'string')
    {
        if (!isset($_GET[$name])) {
            return $fallback;
        }
        return self::clean($_GET[$name], $fallback, $type, 'get', $name);
    }

    /**
     * Read from $_POST with optional nested path using '->' or '.' separators.
     * @param string $name
     * @param mixed $fallback
     * @param string|null $type If null, returns raw value without "mh_clean_input".
     * @return mixed
     */
    public static function post(string $name, $fallback = null, ?string $type = null)
    {
        $raw = self::nestedLookup($_POST ?? [], $name);
        if ($raw === null) {
            return $fallback;
        }
        if ($type === null) {
            return $raw;
        }
        return self::clean($raw, $fallback, $type, 'post', $name);
    }

    /**
     * Read from $_REQUEST with validation (no nested path support as PHP flattens it).
     * @param string $name
     * @param mixed $fallback
     * @param string|null $type
     * @return mixed
     */
    public static function request(string $name, $fallback = null, ?string $type = null)
    {
        if (!isset($_REQUEST[$name])) {
            return $fallback;
        }
        $raw = $_REQUEST[$name];
        if ($type === null) {
            return $raw;
        }
        return self::clean($raw, $fallback, $type, 'request', $name);
    }

    /**
     * Read an input parameter from POST, then fallback to GET if not present.
     * @param string $name
     * @param mixed $fallback
     * @param string|null $type
     * @return mixed
     */
    public static function input(string $name, $fallback = null, ?string $type = null)
    {
        $fromPost = self::post($name, null, $type);
        if ($fromPost !== null) {
            return $fromPost;
        }
        // For GET we only support flat lookup (consistent with mh_get)
        if (isset($_GET[$name])) {
            return $type === null ? $_GET[$name] : self::clean($_GET[$name], $fallback, $type, 'get', $name);
        }
        return $fallback;
    }

    /**
     * Read a boolean flag from request (POST first, then GET).
     * Accepts: 1,true,yes,on and 0,false,no,off,'' (case-insensitive).
     * @param string $name
     * @param bool $fallback
     * @param string $source One of: 'input' (default), 'get', 'post', 'request'.
     * @return bool
     */
    public static function boolean(string $name, bool $fallback = false, string $source = 'input'): bool
    {
        $type = 'bool';
        switch ($source) {
            case 'get':
                return (bool) self::get($name, $fallback, $type);
            case 'post':
                return (bool) self::post($name, $fallback, $type);
            case 'request':
                return (bool) self::request($name, $fallback, $type);
            case 'input':
            default:
                $val = self::input($name, $fallback, $type);
                return (bool) $val;
        }
    }

    /** Convenience: integer input (POST then GET by default). */
    public static function integer(string $name, int $fallback = 0, string $source = 'input'): int
    {
        $type = 'int';
        switch ($source) {
            case 'get': return (int) self::get($name, $fallback, $type);
            case 'post': return (int) self::post($name, $fallback, $type);
            case 'request': return (int) self::request($name, $fallback, $type);
            default: return (int) self::input($name, $fallback, $type);
        }
    }

    /** Convenience: string input (POST then GET by default). */
    public static function string(string $name, string $fallback = '', string $source = 'input'): string
    {
        $type = 'string';
        switch ($source) {
            case 'get': return (string) self::get($name, $fallback, $type);
            case 'post': return (string) self::post($name, $fallback, $type);
            case 'request': return (string) self::request($name, $fallback, $type);
            default: return (string) self::input($name, $fallback, $type);
        }
    }

    /**
     * Read from $_SERVER in a safe way.
     * @param string $name
     * @param mixed $fallback
     * @return mixed
     */
    public static function server(string $name, $fallback = null)
    {
        return $_SERVER[$name] ?? $fallback;
    }

    /**
     * Access $_SESSION with optional nested path using '->' or '.'.
     * Mirrors the behavior of mh_session().
     * @param string $name
     * @param mixed $fallback
     * @return mixed
     */
    public static function session(string $name, $fallback = null)
    {
        if (!isset($_SESSION)) return $fallback;
        if (strpos($name, '->') !== false || strpos($name, '.') !== false) {
            $delim = strpos($name, '->') !== false ? '->' : '.';
            $parts = explode($delim, $name);
            $cursor = $_SESSION;
            foreach ($parts as $p) {
                if (is_array($cursor) && array_key_exists($p, $cursor)) {
                    $cursor = $cursor[$p];
                } else {
                    return $fallback;
                }
            }
            return $cursor;
        }
        return array_key_exists($name, $_SESSION) ? $_SESSION[$name] : $fallback;
    }

    /** Check if a key exists in POST then GET (input) or a specific source. */
    public static function has(string $name, string $source = 'input'): bool
    {
        switch ($source) {
            case 'get': return isset($_GET[$name]);
            case 'post': return isset($_POST[$name]);
            case 'request': return isset($_REQUEST[$name]);
            default: return isset($_POST[$name]) || isset($_GET[$name]);
        }
    }

    /** Return the raw superglobal array for a given source. */
    public static function all(string $source = 'request'): array
    {
        switch ($source) {
            case 'get': return $_GET ?? [];
            case 'post': return $_POST ?? [];
            case 'server': return $_SERVER ?? [];
            case 'session': return $_SESSION ?? [];
            default: return $_REQUEST ?? [];
        }
    }

    /**
     * Lookup helper to support nested form names in POST, like "user->name" or "user.name".
     * @param array $data
     * @param string $name
     * @return mixed
     */
    private static function nestedLookup(array $data, string $name)
    {
        if ($name === '') return null;
        if (strpos($name, '->') === false && strpos($name, '.') === false) {
            return $data[$name] ?? null;
        }
        $delim = strpos($name, '->') !== false ? '->' : '.';
        $parts = explode($delim, $name);
        $cursor = $data;
        foreach ($parts as $p) {
            if (is_array($cursor) && array_key_exists($p, $cursor)) {
                $cursor = $cursor[$p];
            } else {
                return null;
            }
        }
        return $cursor;
    }

    /**
     * Clean and validate an input value according to $type.
     * Supported $type: string, int, bool, hex, uuid, email, slug
     * Array forms: array:string, array:int, array:bool, array:hex, array:uuid, array:email, array:slug
     * Special aliases: email_string, hex_string (decide by content)
     * Moved from global mh_clean_input() to centralize in Request.
     * @param mixed $value
     * @param mixed $fallback
     * @param string $type
     * @return mixed
     * @throws \Exception
     */
    public static function cleanInput($value, $fallback = null, string $type = 'string')
    {
        // Determine array subtype
        $isArrayType = false;
        $subType = $type;
        if (is_string($type) && strncmp($type, 'array:', 6) === 0) {
            $isArrayType = true;
            $subType = substr($type, 6) ?: 'string';
        }

        if ($subType === 'email_string') {
            $subType = (is_string($value) && stristr($value, '@')) ? 'email' : 'string';
        }

        if ($subType === 'hex_string') {
            $subType = (is_string($value) && stristr($value, '-')) ? 'string' : 'hex';
        }

        // If array expected, normalize to array and validate each item
        if ($isArrayType) {
            if ($value === null || $value === '') {
                return is_array($fallback) ? $fallback : [];
            }
            if (!is_array($value)) {
                // Allow single value to be wrapped into array
                $value = [$value];
            }
            $out = [];
            foreach ($value as $k => $v) {
                $out[$k] = self::cleanInput($v, null, $subType);
            }
            return $out;
        }

        // Coerce scalar
        if (is_array($value)) {
            throw new \Exception('Expected scalar for type ' . $subType);
        }

        if (is_string($value)) {
            $value = trim($value);
        }

        // Define validation rules based on type
        $patterns = [
            'hex'   => '/^[a-f0-9]{3,32}$/',
            'int'   => '/^\d+$/',
            'string'=> '/^[ =,.:?_~@+&%#\[\]\/\-*\p{L}\p{N}]*$/u',
            'uuid'  => '/^[a-f0-9\-]{36}$/',
            'email' => '/^[\p{L}\p{N}_\.\-]+@([\p{L}\p{N}\-]+\.)*[\p{L}\p{N}\-]*$/u',
            'slug'  => '/^[a-z0-9-]+$/',
            'key'   => '/^[a-z0-9_\-]+$/',
            // bool handled separately
        ];

        // Validate input against defined pattern or specific rules
        switch ($subType) {
            case 'int':
                if ($value === '' || $value === null) return ($fallback !== null) ? (int)$fallback : 0;
                if (!preg_match($patterns['int'], (string)$value)) {
                    throw new \Exception('Invalid int: ' . $value);
                }
                // Avoid numeric overflow by casting
                return (int)$value;
            case 'bool':
                if (is_bool($value)) return $value;
                $val = is_string($value) ? strtolower($value) : $value;
                $truthy = ['1','true','yes','on'];
                $falsy  = ['0','false','no','off',''];
                if (in_array($val, $truthy, true)) return true;
                if (in_array($val, $falsy, true)) return false;
                // Also accept integers
                if ($val === 1 || $val === 0) return (bool)$val;
                throw new \Exception('Invalid bool: ' . $value);
            case 'hex':
                $v = is_string($value) ? strtolower($value) : $value;
                if (!preg_match($patterns['hex'], (string)$v)) {
                    throw new \Exception('Invalid hex: ' . $value);
                }
                return $v;
            case 'uuid':
                $v = is_string($value) ? strtolower($value) : $value;
                if (!preg_match($patterns['uuid'], (string)$v)) {
                    throw new \Exception('Invalid uuid: ' . $value);
                }
                return $v;
            case 'email':
                $v = is_string($value) ? strtolower($value) : $value;
                if (!preg_match($patterns['email'], (string)$v)) {
                    throw new \Exception('Invalid email: ' . $value);
                }
                return $v;
            case 'slug':
                if (!preg_match($patterns['slug'], (string)$value)) {
                    throw new \Exception('Invalid slug: ' . $value);
                }
                return (string)$value;
            case 'key':
                $v = is_string($value) ? strtolower($value) : $value;
                if (!preg_match($patterns['key'], (string)$v)) {
                    throw new \Exception('Invalid key: ' . $value);
                }
                return (string)$v;
            case 'string':
            default:
                if (!is_string($value)) {
                    $value = (string)$value;
                }
                if (!preg_match($patterns['string'], $value)) {
                    throw new \Exception('Invalid string: ' . $value);
                }
                return $value;
        }
    }

    /**
     * Wrap the cleanInput validator and convert exceptions into warnings + fallback.
     * @param mixed $raw
     * @param mixed $fallback
     * @param string $type
     * @param string $ctx
     * @param string $name
     * @return mixed
     */
    private static function clean($raw, $fallback, string $type, string $ctx, string $name)
    {
        try {
            return self::cleanInput($raw, $fallback, $type);
        } catch (\Throwable $e) {
            trigger_error('[Request::' . $ctx . "] invalid value for " . $name . ' (' . $type . ')', E_USER_WARNING);
            return $fallback;
        }
    }
}
