<?php

declare(strict_types=1);

namespace BotGuard\Services\Detection\Modules;

use BotGuard\Services\Detection\Contracts\DetectionModuleInterface;
use BotGuard\Services\Detection\ModuleResult;

/**
 * Base class with a small check-accumulator to keep modules terse.
 *
 * Most modules express their logic as a list of boolean "checks" where passing
 * a check is human-like. fromChecks() turns that list into a weighted [0,1]
 * score plus the passed/failed label arrays the analytics UI displays.
 */
abstract class AbstractModule implements DetectionModuleInterface
{
    /** @var array<int, array{label:string, human:bool, weight:float}> */
    private array $checks = [];

    /** @var array<string, mixed> */
    private array $detail = [];

    /**
     * Register a check. $human=true means this observation looks human.
     */
    protected function check(string $label, bool $human, float $weight = 1.0): void
    {
        $this->checks[] = ['label' => $label, 'human' => $human, 'weight' => $weight];
    }

    /**
     * Attach a raw value for display/debugging.
     */
    protected function detail(string $key, mixed $value): void
    {
        $this->detail[$key] = $value;
    }

    /**
     * Build a ModuleResult from the accumulated checks.
     *
     * When there are no checks (e.g. a module had no data to work with) the
     * caller should pass an explicit $fallbackScore instead.
     */
    protected function result(?float $fallbackScore = null): ModuleResult
    {
        $passed = [];
        $failed = [];
        $earned = 0.0;
        $total = 0.0;

        foreach ($this->checks as $check) {
            $total += $check['weight'];
            if ($check['human']) {
                $earned += $check['weight'];
                $passed[] = $check['label'];
            } else {
                $failed[] = $check['label'];
            }
        }

        $score = $total > 0
            ? $earned / $total
            : ($fallbackScore ?? 0.5);

        $result = ModuleResult::make($this->key(), $score, $passed, $failed, $this->detail);

        // Reset accumulators so a reused instance stays clean.
        $this->checks = [];
        $this->detail = [];

        return $result;
    }

    /**
     * Directly build a result from a continuous score (bypassing checks).
     *
     * @param string[] $passed
     * @param string[] $failed
     * @param array<string,mixed> $detail
     */
    protected function scored(float $score, array $passed = [], array $failed = [], array $detail = []): ModuleResult
    {
        $this->checks = [];
        $this->detail = [];

        return ModuleResult::make($this->key(), $score, $passed, $failed, $detail);
    }
}
