<?php

declare(strict_types=1);

namespace BotGuard\Services\Detection;

use BotGuard\Core\Request;

/**
 * Normalized container of every signal the engine can reason about.
 *
 * Two halves:
 *   - "server" signals derived from the HTTP request (UA, headers, IP, referer)
 *   - "client" signals POSTed by the verification page's JavaScript collectors
 *     (fingerprint, behavior, timing, cookies, honeypot, captcha)
 *
 * It is deliberately framework-light: modules receive a SignalBag and nothing
 * else, so they can be unit-tested with plain arrays via SignalBag::fromArrays().
 */
final class SignalBag
{
    /**
     * @param array<string, mixed> $server Server-derived signals.
     * @param array<string, mixed> $client Client-collected signals.
     */
    public function __construct(
        private array $server,
        private array $client,
    ) {
    }

    /**
     * Build from the live request plus the decoded client payload.
     *
     * @param array<string, mixed> $clientPayload
     */
    public static function fromRequest(Request $request, array $clientPayload): self
    {
        return new self(
            [
                'ip'         => $request->ip(),
                'method'     => $request->method(),
                'user_agent' => $request->userAgent(),
                'referer'    => $request->referer(),
                'headers'    => $request->headers(),
            ],
            $clientPayload,
        );
    }

    /**
     * Build directly from arrays (used by tests and the CLI simulator).
     *
     * @param array<string, mixed> $server
     * @param array<string, mixed> $client
     */
    public static function fromArrays(array $server, array $client): self
    {
        return new self($server, $client);
    }

    // --- Server-side accessors -------------------------------------------

    public function ip(): string
    {
        return (string) ($this->server['ip'] ?? '0.0.0.0');
    }

    public function userAgent(): string
    {
        return (string) ($this->server['user_agent'] ?? '');
    }

    public function referer(): string
    {
        return (string) ($this->server['referer'] ?? '');
    }

    public function header(string $name): ?string
    {
        $headers = $this->server['headers'] ?? [];

        return $headers[strtolower($name)] ?? null;
    }

    public function hasHeader(string $name): bool
    {
        return $this->header($name) !== null;
    }

    /**
     * @return array<string, string>
     */
    public function headers(): array
    {
        return (array) ($this->server['headers'] ?? []);
    }

    // --- Client-side accessors -------------------------------------------

    /**
     * Read a client signal using dot notation, e.g. client('timing.time_on_page').
     */
    public function client(string $key, mixed $default = null): mixed
    {
        $value = $this->client;

        foreach (explode('.', $key) as $segment) {
            if (! is_array($value) || ! array_key_exists($segment, $value)) {
                return $default;
            }
            $value = $value[$segment];
        }

        return $value;
    }

    public function clientBool(string $key, bool $default = false): bool
    {
        $value = $this->client($key);

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

        return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $default;
    }

    public function clientInt(string $key, int $default = 0): int
    {
        $value = $this->client($key);

        return is_numeric($value) ? (int) $value : $default;
    }

    public function clientFloat(string $key, float $default = 0.0): float
    {
        $value = $this->client($key);

        return is_numeric($value) ? (float) $value : $default;
    }

    /**
     * @return array<int|string, mixed>
     */
    public function clientArray(string $key): array
    {
        $value = $this->client($key);

        return is_array($value) ? $value : [];
    }

    public function hasClientData(): bool
    {
        return $this->client !== [];
    }

    /**
     * @return array<string, mixed>
     */
    public function allClient(): array
    {
        return $this->client;
    }
}
