<?php
/**
 * LoginLimiterEngine — Core brute-force detection and lockout logic.
 *
 * Extracted from the module class so the runtime hooks stay thin.
 * All decision-making about when to lock out, progressive backoff,
 * and lockout duration calculation lives here.
 *
 * @package UltraguardPro\modules\loginlimiter
 * @since   6.5.0
 */

namespace UltraguardPro\modules\loginlimiter;

use UltraguardPro\Plugin;
use UltraguardPro\core\MailManager;

if ( ! defined( 'ABSPATH' ) ) { exit; }

class LoginLimiterEngine {

    /** @var Plugin */
    private Plugin $plugin;

    public function __construct( Plugin $plugin ) {
        $this->plugin = $plugin;
    }

    // ── Public API ─────────────────────────────────────────────────────────────

    /**
     * Check failure count for an IP and trigger a lockout if threshold is exceeded.
     *
     * @param  string      $ip        Client IP address.
     * @param  string|null $username  Username that was attempted.
     */
    public function maybe_lockout( string $ip, ?string $username = null ): void {
        $max_attempts = (int) $this->setting( 'loginlimiter_max_attempts',      5 );
        $window_mins  = (int) $this->setting( 'loginlimiter_attempt_window',   60 );

        $att_t = $this->table( 'login_attempts' );
        if ( ! $att_t ) { return; }

        $recent_failures = $att_t->count_failures( $ip, $window_mins );

        if ( $recent_failures < $max_attempts ) { return; }

        $lo_t = $this->table( 'login_limiter_lockouts' );
        if ( ! $lo_t ) { return; }

        // Don't stack a new lockout if one is already active
        if ( $lo_t->is_locked_out( $ip ) ) { return; }

        $duration = $this->calc_lockout_duration( $ip, $lo_t );

        $lo_t->lockout( $ip, $duration, $recent_failures, $username );

        do_action( 'ultraguard_ip_locked_out', $ip, $username, $duration );

        // Notify admin if configured
        if ( $this->setting( 'loginlimiter_notify_admin', true ) ) {
            $this->send_lockout_notification( $ip, $username, $recent_failures, $duration );
        }
    }

    /**
     * Manually release a lockout for a given IP.
     *
     * @param  string $ip
     * @return bool
     */
    public function release_lockout( string $ip ): bool {
        $lo_t = $this->table( 'login_limiter_lockouts' );
        if ( ! $lo_t ) { return false; }

        $released = $lo_t->release( $ip );
        if ( $released ) {
            do_action( 'ultraguard_ip_unlocked', $ip );
        }
        return (bool) $released;
    }

    /**
     * Run a protection health-check and return a scored report.
     *
     * @return array{checks:array, score:int, grade:string}
     */
    public function run_protection_test(): array {
        $enabled      = (bool) $this->setting( 'loginlimiter_enabled',         true );
        $max_attempts = (int)  $this->setting( 'loginlimiter_max_attempts',    5    );
        $lockout_mins = (int)  $this->setting( 'loginlimiter_lockout_duration', 30   );
        $hide_errors  = (bool) $this->setting( 'loginlimiter_hide_errors',     true );
        $two_fa       = (bool) $this->setting( 'loginlimiter_two_factor_enabled', false );
        $captcha      = (bool) $this->setting( 'loginlimiter_captcha_enabled', false );
        $progressive  = (bool) $this->setting( 'loginlimiter_progressive_lockout', false );

        $lo_t = $this->table( 'login_limiter_lockouts' );

        $checks = [
            'protection_enabled'  => $enabled,
            'max_attempts'        => $max_attempts,
            'lockout_duration'    => $lockout_mins,
            'hide_errors'         => $hide_errors,
            'two_factor'          => $two_fa,
            'captcha'             => $captcha,
            'progressive_lockout' => $progressive,
            'active_lockouts'     => $lo_t ? $lo_t->count( [ 'is_active' => 1 ] ) : 0,
        ];

        $score = 0;
        if ( $enabled )             { $score += 25; }
        if ( $max_attempts <= 5 )   { $score += 15; }
        if ( $lockout_mins >= 15 )  { $score += 15; }
        if ( $hide_errors )         { $score += 10; }
        if ( $two_fa )              { $score += 20; }
        if ( $captcha )             { $score += 10; }
        if ( $progressive )         { $score += 5;  }

        $grade = match ( true ) {
            $score >= 90 => 'A',
            $score >= 75 => 'B',
            $score >= 60 => 'C',
            $score >= 40 => 'D',
            default      => 'F',
        };

        return compact( 'checks', 'score', 'grade' );
    }

    // ── Private helpers ────────────────────────────────────────────────────────

    /**
     * Calculate the lockout duration for an IP, applying progressive backoff
     * if that feature is enabled.
     *
     * @param  string                                               $ip
     * @param  \UltraguardPro\modules\loginlimiter\db\LoginLimiterLockoutsTable $lo_t
     * @return int  Duration in minutes.
     */
    private function calc_lockout_duration( string $ip, $lo_t ): int {
        $base = (int) $this->setting( 'loginlimiter_lockout_duration', 30 );

        if ( ! $this->setting( 'loginlimiter_progressive_lockout', false ) ) {
            return $base;
        }

        // Count prior lockouts for this IP to determine backoff multiplier
        $prior = $lo_t->count( [ 'ip_address' => $ip ] );
        if ( $prior <= 0 ) { return $base; }

        // Cap the multiplier at 8× to prevent absurdly long lockouts
        $multiplier = min( 8, 2 ** $prior );
        return $base * $multiplier;
    }

    /**
     * Send a lockout notification email to the admin.
     *
     * @param  string      $ip
     * @param  string|null $username
     * @param  int         $attempts
     * @param  int         $duration_minutes
     */
    private function send_lockout_notification( string $ip, ?string $username, int $attempts, int $duration_minutes ): void {
        $to      = get_option( 'admin_email' );
        $subject = sprintf(
            /* translators: Placeholder values are inserted into this string. */
            __( '[%s] Security Alert: IP Address Locked Out', 'ultraguard-security-suite' ),
            get_bloginfo( 'name' )
        );

        MailManager::notification( [
            'to'        => $to,
            'subject'   => $subject,
            'title'     => __( 'IP address locked out', 'ultraguard-security-suite' ),
            'message'   => __( 'UltraGuard blocked further login attempts after repeated failures.', 'ultraguard-security-suite' ),
            'tone'      => 'danger',
            'details'   => [
                __( 'IP Address', 'ultraguard-security-suite' )         => $ip,
                __( 'Username attempted', 'ultraguard-security-suite' ) => $username ?? __( 'Unknown', 'ultraguard-security-suite' ),
                __( 'Failed attempts', 'ultraguard-security-suite' )    => $attempts,
                /* translators: %d: integer */
                __( 'Lockout duration', 'ultraguard-security-suite' )   => sprintf( __( '%d minutes', 'ultraguard-security-suite' ), $duration_minutes ),
                __( 'Site', 'ultraguard-security-suite' )               => get_bloginfo( 'url' ),
            ],
            'cta_url'   => admin_url( 'admin.php?page=ultraguard-security-suite-dashboard&ug_page=lockouts' ),
            'cta_label' => __( 'Manage Lockouts', 'ultraguard-security-suite' ),
        ] );
    }

    /**
     * Read a setting value from the settings store.
     *
     * @param  string $key
     * @param  mixed  $default
     * @return mixed
     */
    private function setting( string $key, $default = null ) {
        return $this->plugin->settings->get_option( $key, $default );
    }

    /**
     * Retrieve a table by key.
     *
     * @param  string $key
     * @return \UltraguardPro\db\BaseTable|null
     */
    private function table( string $key ) {
        $db = $this->plugin->modules->get_db_tables();
        return $db ? $db->get_table( $key ) : null;
    }
}
