<?php
/**
 * LoginLimiterLockoutsTable — Active and historical IP lockout records.
 *
 * @package UltraguardPro\modules\loginlimiter\db
 * @since   6.5.0
 */

namespace UltraguardPro\modules\loginlimiter\db;

use UltraguardPro\db\BaseTable;
use UltraguardPro\core\DatabaseCrud;

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

class LoginLimiterLockoutsTable extends BaseTable {

    protected $table_name  = 'ultraguard_login_limiter_lockouts';
    protected $cache_group = 'ultraguard_login_limiter_lockouts';

    public function get_defaults(): array {
        return [
            'id'             => 0,
            'ip_address'     => '',
            'username'       => null,
            'lockout_type'   => 'ip',
            'attempts_count' => 0,
            'locked_at'      => '',
            'unlock_at'      => '',
            'is_active'      => 1,
            'released_by'    => null,
            'created_at'     => '',
            'updated_at'     => '',
        ];
    }

    protected function validate_data( $data ) {
        if ( empty( $data['ip_address'] ) || empty( $data['unlock_at'] ) ) { return false; }
        return $data;
    }

    protected function sanitize_data( $data ): array {
        $out = [];
        foreach ( $data as $k => $v ) {
            $out[ $k ] = match ( $k ) {
                'attempts_count' => absint( $v ),
                'is_active'      => (int) (bool) $v,
                'released_by'    => is_null( $v ) ? null : absint( $v ),
                default          => is_string( $v ) ? sanitize_text_field( $v ) : $v,
            };
        }
        return $out;
    }

    protected function get_searchable_fields(): array {
        return [ 'ip_address', 'username' ];
    }

    // ── Domain methods ─────────────────────────────────────────────────────────

    /**
     * Check if an IP currently has an active lockout.
     * Direct query — called on every login_init request for speed.
     *
     * @param  string $ip
     * @return bool
     */
    public function is_locked_out( string $ip ): bool {
        // WHERE ip_address=? AND is_active=1 AND (unlock_at IS NULL OR unlock_at >= now)
        // unlock_at is never NULL in practice, so this reduces to the plain datetime check.
        return DatabaseCrud::record_exists_not_expired(
            $this->table_name,
            [ 'ip_address' => $ip, 'is_active' => 1 ],
            'unlock_at'
        );
    }

    /**
     * Create a new lockout record.
     *
     * @param  string      $ip
     * @param  int         $duration_minutes
     * @param  int         $attempts
     * @param  string|null $username
     * @return int|false   New row ID or false.
     */
    public function lockout( string $ip, int $duration_minutes, int $attempts, ?string $username = null ): int|false {
        // All datetime values stored in UTC so is_locked_out() comparison is timezone-safe.
        $now       = gmdate( 'Y-m-d H:i:s' );
        $unlock_at = gmdate( 'Y-m-d H:i:s', time() + ( $duration_minutes * 60 ) );

        return $this->insert( [
            'ip_address'     => $ip,
            'username'       => $username,
            'attempts_count' => $attempts,
            'locked_at'      => $now,
            'unlock_at'      => $unlock_at,
            'is_active'      => 1,
        ] );
    }

    /**
     * Release all active lockouts for an IP.
     *
     * @param  string $ip
     * @return bool
     */
    public function release( string $ip ): bool {
        return (bool) $this->update_where(
            [ 'is_active' => 0, 'released_by' => get_current_user_id() ?: null ],
            [ 'ip_address' => $ip, 'is_active' => 1 ]
        );
    }

    /**
     * Release ALL active lockouts (admin bulk action).
     *
     * @return int  Number of lockouts cleared.
     */
    public function release_all(): int {
        return $this->update_where(
            [ 'is_active' => 0, 'released_by' => get_current_user_id() ?: null ],
            [ 'is_active' => 1 ]
        );
    }

    /**
     * Extend an active lockout by N minutes (temporary unlock workaround).
     *
     * @param  string $ip
     * @param  int    $extra_minutes
     * @return bool
     */
    public function extend_lockout( string $ip, int $extra_minutes ): bool {
        $unlock_at = gmdate( 'Y-m-d H:i:s', time() + ( $extra_minutes * 60 ) );
        return (bool) $this->update_where(
            [ 'unlock_at' => $unlock_at ],
            [ 'ip_address' => $ip, 'is_active' => 1 ]
        );
    }

    /**
     * Expire any lockouts whose unlock_at is in the past.
     *
     * @return int  Rows updated.
     */
    public function cleanup_expired(): int {
        return $this->update_where(
            [ 'is_active' => 0 ],
            [ 'unlock_at <' => gmdate( 'Y-m-d H:i:s' ), 'is_active' => 1 ]
        );
    }

    /**
     * Return all currently-active lockout rows.
     *
     * @param  int $limit
     * @return array
     */
    public function get_active_lockouts( int $limit = 200 ): array {
        return $this->get_by( [ 'is_active' => 1 ], $limit, 'locked_at', 'DESC' );
    }

    /**
     * Stat snapshot for dashboard cards.
     *
     * @return array{active:int, total:int, released:int}
     */
    public function get_lockout_summary(): array {
        return [
            'active'   => $this->count( [ 'is_active' => 1 ] ),
            'total'    => $this->count(),
            'released' => $this->count( [ 'is_active' => 0 ] ),
        ];
    }
}
