<?php

declare(strict_types=1);

namespace BotGuard\Services;

use BotGuard\Core\Database;
use BotGuard\Core\Request;
use BotGuard\Helpers\UserAgentParser;
use BotGuard\Models\DetectionLog;
use BotGuard\Models\Fingerprint;
use BotGuard\Models\RedirectConfig;
use BotGuard\Models\VerificationResult;
use BotGuard\Models\VerificationSession;
use BotGuard\Services\Captcha\CaptchaManager;
use BotGuard\Services\Detection\DetectionOutcome;
use BotGuard\Services\Detection\EngineFactory;
use BotGuard\Services\Detection\SignalBag;

/**
 * Orchestrates the end-to-end verification: issues challenges, assembles the
 * signal bag (client-submitted + server-derived), runs the detection engine,
 * resolves the redirect decision, and persists the result + per-module logs.
 *
 * This is the single place the controllers call, keeping them thin.
 */
final class VerificationService
{
    private const SESSION_TTL_SECONDS = 900; // 15 minutes to complete a challenge

    public function __construct(
        private Database $db,
        private RateTracker $rateTracker,
        private CaptchaManager $captcha,
        private EngineFactory $engineFactory,
        private VerificationSession $sessions,
        private VerificationResult $results,
        private Fingerprint $fingerprints,
        private RedirectConfig $redirects,
        private DetectionLog $detectionLogs,
    ) {
    }

    /**
     * Create a verification session and issue a challenge for the /verify page.
     *
     * @return array{token:string, challenge:array<string,mixed>, provider:string, issued_at_ms:int}
     */
    public function createSession(Request $request): array
    {
        $provider = $this->captcha->provider();
        $issued = $provider->issue();
        $token = str_random(32);

        $this->sessions->create([
            'token'            => $token,
            'ip'               => $request->ip(),
            'user_agent'       => mb_substr($request->userAgent(), 0, 1024),
            'challenge'        => $provider->name(),
            'challenge_answer' => $issued['answer'],
            'status'           => 'pending',
            'expires_at'       => date('Y-m-d H:i:s', time() + self::SESSION_TTL_SECONDS),
        ]);

        return [
            'token'        => $token,
            'challenge'    => $issued['client'],
            'provider'     => $provider->name(),
            'issued_at_ms' => (int) round(microtime(true) * 1000),
        ];
    }

    /**
     * Resolve a verification: score the signals and produce a decision.
     *
     * @param array<string, mixed> $payload The full client-submitted signal bundle.
     * @return array<string, mixed> Decision payload for the client / logs.
     */
    public function verify(Request $request, array $payload): array
    {
        $token = (string) ($payload['token'] ?? '');
        $session = $token !== '' ? $this->sessions->findByToken($token) : null;

        $integrityOk = $session !== null
            && $session['status'] === 'pending'
            && strtotime((string) $session['expires_at']) >= time();

        // Verify the challenge with the same provider that issued it.
        $provider = $this->captcha->provider($session['challenge'] ?? null);
        $captchaResult = $provider->verify(
            (array) ($payload['captcha'] ?? []),
            $session['challenge_answer'] ?? null,
            $request->ip(),
        );

        // Server-derived signals.
        $rate = $this->rateTracker->hit($request->ip());
        $fpBag = (array) ($payload['fingerprint'] ?? []);
        $fpHash = (string) ($fpBag['hash'] ?? '');
        $repeatVisitor = $fpHash !== '' && $this->fingerprints->record($fpHash, $fpBag);

        // Inject the server-authoritative signals (overriding any client claims).
        $payload['rate'] = $rate;
        $payload['captcha'] = array_merge((array) ($payload['captcha'] ?? []), [
            'provider'      => $captchaResult->provider,
            'verified'      => $captchaResult->verified,
            'solve_time_ms' => $captchaResult->solveTimeMs,
            'retries'       => $captchaResult->retries,
        ]);
        $payload['session'] = [
            'duration_ms'      => (int) ($payload['timing']['time_on_page_ms'] ?? 0),
            'request_count'    => $rate['per_minute'],
            'repeat_visitor'   => $repeatVisitor,
            'integrity_ok'     => $integrityOk,
            'navigation_count' => (int) ($payload['navigation_count'] ?? 0),
        ];

        // Score.
        $signals = SignalBag::fromRequest($request, $payload);
        $outcome = $this->engineFactory->make()->evaluate($signals);

        // Resolve destination + persist.
        $decision = $this->resolveDecision($outcome);
        $this->persist($request, $payload, $outcome, $decision, $captchaResult, $fpHash);

        if ($session !== null) {
            $this->sessions->markCompleted((int) $session['id']);
        }

        return [
            'score'          => $outcome->score,
            'classification' => $outcome->classification,
            'action'         => $outcome->action,
            'redirect_url'   => $decision['redirect_url'],
            'block_message'  => $decision['block_message'],
            'breakdown'      => $outcome->breakdown(),
        ];
    }

    /**
     * Map the engine action to a concrete redirect URL / block message using the
     * platform's default redirect configuration.
     *
     * @return array{user_id:?int, redirect_url:?string, block_message:?string}
     */
    private function resolveDecision(DetectionOutcome $outcome): array
    {
        $config = $this->redirects->platformDefault();
        $primary = $config['primary_url'] ?? (rtrim((string) config('app.url'), '/') . '/success');
        $alternate = $config['alternate_url'] ?? (rtrim((string) config('app.url'), '/') . '/blocked');
        $blockMessage = $config['block_message'] ?? 'Automated traffic detected.';
        $userId = isset($config['user_id']) ? (int) $config['user_id'] : null;

        $redirectUrl = match ($outcome->action) {
            'redirect_primary', 'allow' => $primary,
            'redirect_alternate'        => $alternate,
            default                     => null, // block
        };

        return [
            'user_id'       => $userId,
            'redirect_url'  => $redirectUrl,
            'block_message' => $outcome->action === 'block' ? $blockMessage : null,
        ];
    }

    /**
     * Store the verification_results row + normalized detection_logs rows.
     *
     * @param array<string, mixed> $payload
     * @param array{user_id:?int, redirect_url:?string, block_message:?string} $decision
     */
    private function persist(
        Request $request,
        array $payload,
        DetectionOutcome $outcome,
        array $decision,
        \BotGuard\Services\Captcha\CaptchaResult $captchaResult,
        string $fpHash,
    ): void {
        $parsed = UserAgentParser::parse($request->userAgent());

        $this->db->beginTransaction();
        try {
            $resultId = $this->results->create([
                'session_token'   => (string) ($payload['token'] ?? ''),
                'user_id'         => $decision['user_id'],
                'ip'              => $request->ip(),
                'country'         => \BotGuard\Helpers\Geo::country($request->ip(), $request->header('cf-ipcountry')),
                'user_agent'      => mb_substr($request->userAgent(), 0, 1024),
                'browser'         => $parsed['browser'],
                'browser_version' => $parsed['version'],
                'os'              => $parsed['os'],
                'device_type'     => $parsed['device_type'],
                'fingerprint_hash' => $fpHash !== '' ? $fpHash : null,
                'score'           => $outcome->score,
                'classification'  => $outcome->classification,
                'decision'        => $outcome->action,
                'redirect_url'    => $decision['redirect_url'],
                'js_enabled'      => (int) (bool) ($payload['javascript']['enabled'] ?? 0),
                'cookies_enabled' => (int) (bool) ($payload['cookies']['enabled'] ?? 0),
                'captcha_result'  => $captchaResult->verified ? 'passed' : 'failed',
                'referrer_type'   => $this->referrerType($outcome),
                'breakdown'       => json_encode($outcome->breakdown(), JSON_UNESCAPED_SLASHES),
            ]);

            foreach ($outcome->results as $moduleResult) {
                $weight = $outcome->weights[$moduleResult->key] ?? 0.0;
                $this->detectionLogs->create([
                    'result_id' => $resultId,
                    'module'    => $moduleResult->key,
                    'raw_score' => round($moduleResult->score, 4),
                    'weight'    => $weight,
                    'weighted'  => round($moduleResult->score * $weight, 2),
                    'passed'    => json_encode($moduleResult->passed, JSON_UNESCAPED_SLASHES),
                    'failed'    => json_encode($moduleResult->failed, JSON_UNESCAPED_SLASHES),
                    'detail'    => json_encode($moduleResult->detail, JSON_UNESCAPED_SLASHES),
                ]);
            }

            $this->db->commit();
        } catch (\Throwable $e) {
            $this->db->rollBack();
            throw $e;
        }
    }

    private function referrerType(DetectionOutcome $outcome): ?string
    {
        foreach ($outcome->results as $result) {
            if ($result->key === 'referrer') {
                return $result->detail['type'] ?? null;
            }
        }

        return null;
    }
}
