<?php
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Check login attempts during authentication
 *
 * Verifies if the user has exceeded the maximum number of allowed login attempts
 * based on their IP address. If blocked, it returns a WP_Error object.
 *
 * @param WP_User|WP_Error|null $user The current user object or error.
 * @param string $username The username being attempted.
 * @param string $password The password being attempted.
 * @return WP_User|WP_Error The user object if allowed, or WP_Error if locked out.
 */
function admilogu_check_login_attempts($user, $username, $password)
{
    $max_attempts = (int) get_option('admilogu_max_attempts', 3);
    $ip = admilogu_get_ip();

    // Bypass check for whitelisted IPs
    if (admilogu_is_ip_whitelisted($ip)) {
        return $user;
    }

    // Check for blacklisted IPs
    if (admilogu_is_ip_blacklisted($ip)) {
        return new WP_Error(
            'ip_blacklisted',
            __('Your IP address has been permanently banned from accessing this site.', 'admin-login-guard-branding')
        );
    }

    $transient_name = 'admilogu_attempt_' . md5($ip);
    $attempts = (int) get_transient($transient_name);

    if ($attempts >= $max_attempts) {
        $remaining_seconds = admilogu_get_lockout_time_seconds($transient_name);
        return new WP_Error(
            'too_many_attempts',
            sprintf(
                /* translators: 1: Lockout time in seconds, 2: Formatted lockout time */
                __('Too many failed login attempts. Please try again in <span id="admilogu-lockout-timer" data-seconds="%1$d">%2$s</span>.', 'admin-login-guard-branding'),
                $remaining_seconds,
                admilogu_format_duration($remaining_seconds)
            )
        );
    }

    return $user;
}
// Run early (5) to block before password checks, and late (99) to catch any bypasses.
add_filter('authenticate', 'admilogu_check_login_attempts', 5, 3);
add_filter('authenticate', 'admilogu_check_login_attempts', 99, 3);

/**
 * Increment failed attempts on login failure
 *
 * Triggered when a login fails. It increments the failed attempt count for the IP address
 * stored in a transient. It also logs the failure (time, IP, username, user agent)
 * to the custom database table.
 *
 * @param string $username The username that failed to login.
 */
function admilogu_login_failed($username)
{
    $ip = admilogu_get_ip();

    // Do not log if permanently blacklisted
    if (admilogu_is_ip_blacklisted($ip)) {
        return;
    }

    $transient_name = 'admilogu_attempt_' . md5($ip);
    $attempts = (int) get_transient($transient_name);
    $max_attempts = (int) get_option('admilogu_max_attempts', 3);

    // If the IP is already locked out, do not log further attempts to the history table
    if ($attempts >= $max_attempts && !admilogu_is_ip_whitelisted($ip)) {
        return;
    }

    // Only increment attempts for non-whitelisted IPs.
    if (!admilogu_is_ip_whitelisted($ip)) {
        if ($attempts < $max_attempts) {
            $attempts++;
            $lockout_duration = (int) get_option('admilogu_lockout_duration', 30);
            set_transient($transient_name, $attempts, $lockout_duration * MINUTE_IN_SECONDS);

            if ($attempts >= $max_attempts) {
                update_option($transient_name . '_expiry', time() + ($lockout_duration * MINUTE_IN_SECONDS));

                // Send email notification if enabled
                if (get_option('admilogu_notify_admin_lockout', 0)) {
                    $admin_email = get_option('admin_email');
                    /* translators: 1: Blog name */
                    $subject = sprintf(__('[%1$s] Lockout Alert: Too many failed login attempts', 'admin-login-guard-branding'), wp_specialchars_decode(get_option('blogname'), ENT_QUOTES));
                    /* translators: 1: IP address, 2: Max attempts */
                    $message = sprintf(__('An IP address (%1$s) has been locked out after %2$d failed login attempts.', 'admin-login-guard-branding'), $ip, $max_attempts);
                    wp_mail($admin_email, $subject, $message);
                }
            }
        }
    }

    // Log to database
    global $wpdb;
    $table_name = $wpdb->prefix . 'admilogu_login_history';

    // Capture username more robustly. Prioritize strings from hook, then POST/REQUEST.
    $log_username = '';
    if (!empty($username) && is_string($username)) {
        $log_username = sanitize_text_field(wp_unslash($username));
        // phpcs:ignore WordPress.Security.NonceVerification.Missing
    } elseif (!empty($_POST['log'])) {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing
        $log_username = sanitize_text_field(wp_unslash($_POST['log']));
        // phpcs:ignore WordPress.Security.NonceVerification.Missing
    } elseif (!empty($_POST['user_login'])) {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing
        $log_username = sanitize_text_field(wp_unslash($_POST['user_login']));
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
    } elseif (!empty($_REQUEST['log'])) {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
        $log_username = sanitize_text_field(wp_unslash($_REQUEST['log']));
    }

    // Final fallback
    if (empty($log_username) || is_wp_error($log_username)) {
        $log_username = '(empty)';
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
    $wpdb->insert(
        $table_name,
        array(
            'ip_address' => $ip,
            'username' => $log_username,
            'status' => 'failed',
            'time' => current_time('mysql'),
            'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''
        )
    );
}
add_action('wp_login_failed', 'admilogu_login_failed');

/**
 * Clear failed attempts on successful login
 *
 * Resets the failed login attempt counter for the user's IP address
 * upon a successful login.
 *
 * @param string $user_login The username.
 * @param WP_User $user The logged-in user object.
 */
function admilogu_login_success($user_login, $user)
{
    $ip = admilogu_get_ip();
    $transient_name = 'admilogu_attempt_' . md5($ip);
    delete_transient($transient_name);
    delete_option($transient_name . '_expiry');

    // Log to database
    global $wpdb;
    $table_name = $wpdb->prefix . 'admilogu_login_history';
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
    $wpdb->insert(
        $table_name,
        array(
            'ip_address' => $ip,
            'username' => $user_login,
            'status' => 'success',
            'time' => current_time('mysql'),
            'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''
        )
    );

    // Notify user of login from new IP if enabled
    if (get_option('admilogu_notify_user_new_ip', 0)) {
        $known_ips = get_user_meta($user->ID, 'admilogu_known_ips', true);
        if (!is_array($known_ips)) {
            $known_ips = array();
        }

        if (!in_array($ip, $known_ips, true)) {
            // It's a new IP! Send email.
            /* translators: 1: Blog name */
            $subject = sprintf(__('[%1$s] Notice of Login from New Device/IP', 'admin-login-guard-branding'), wp_specialchars_decode(get_option('blogname'), ENT_QUOTES));
            /* translators: 1: User display name, 2: IP address */
            $message = sprintf(__('Hello %1$s,

We noticed a successful login to your account from a new IP address: %2$s.

If this was you, you can safely ignore this email.
If you did not authorize this login, please reset your password immediately.', 'admin-login-guard-branding'), $user->display_name, $ip);
            
            wp_mail($user->user_email, $subject, $message);

            // Add to known IPs
            $known_ips[] = $ip;
            update_user_meta($user->ID, 'admilogu_known_ips', $known_ips);
        }
    }
}
add_action('wp_login', 'admilogu_login_success', 10, 2);

/**
 * Show attempts remaining in login error message
 *
 * Appends a message to the login error indicating how many attempts are remaining
 * before the user is locked out.
 *
 * @param WP_Error $errors The current WP_Error object containing login errors.
 * @return WP_Error The modified error object with the attempts message added.
 */
function admilogu_show_attempt_message($errors)
{
    // Only modify if there is an error
    if (!is_wp_error($errors)) {
        return $errors;
    }

    // Check if it's an invalid username or incorrect password error
    $error_codes = $errors->get_error_codes();
    if (empty($error_codes) || (!in_array('invalid_username', $error_codes) && !in_array('incorrect_password', $error_codes) && !in_array('invalid_email', $error_codes))) {
        return $errors;
    }

    $max_attempts = (int) get_option('admilogu_max_attempts', 3);
    $ip = admilogu_get_ip();

    // Hide for whitelisted IPs
    if (admilogu_is_ip_whitelisted($ip)) {
        return $errors;
    }

    $transient_name = 'admilogu_attempt_' . md5($ip);
    $attempts = (int) get_transient($transient_name);

    $remaining = $max_attempts - $attempts;

    if ($remaining <= 0) {
        $remaining_seconds = admilogu_get_lockout_time_seconds($transient_name);
        $message = sprintf(
            /* translators: 1: Lockout time in seconds, 2: Formatted lockout time */
            __('No attempts remaining. You are now locked out for <span id="admilogu-lockout-timer" data-seconds="%1$d">%2$s</span>.', 'admin-login-guard-branding'),
            $remaining_seconds,
            admilogu_format_duration($remaining_seconds)
        );
    } else {
        /* translators: %d: number of attempts remaining */
        $message = sprintf(_n('%d attempt remaining.', '%d attempts remaining.', $remaining, 'admin-login-guard-branding'), $remaining);
    }

    $errors->add('attempts_remaining', '<strong>' . $message . '</strong>');

    return $errors;
}
add_filter('wp_login_errors', 'admilogu_show_attempt_message');

/**
 * Get the remaining lockout time in minutes for a given transient.
 *
 * Checks the dedicated expiry option first, then falls back to the
 * transient timeout, and finally returns the configured lockout
 * duration if neither is available.
 *
 * @since 1.0.0
 *
 * @param string $transient_name The base name of the lockout transient.
 * @return int Minutes remaining until lockout expires.
 */
function admilogu_get_lockout_time_remaining($transient_name)
{
    // Check our dedicated expiry option first.
    $expiry = get_option($transient_name . '_expiry');
    if ($expiry) {
        $remaining = (int) $expiry - time();
        return $remaining > 0 ? ceil($remaining / 60) : 0;
    }

    // Fallback to transient timeout for backward compatibility.
    $timeout = get_option('_transient_timeout_' . $transient_name);
    if ($timeout) {
        $remaining = (int) $timeout - time();
        return $remaining > 0 ? ceil($remaining / 60) : 0;
    }

    $lockout_duration = (int) get_option('admilogu_lockout_duration', 30);
    return $lockout_duration;
}

/**
 * Get the remaining lockout time in seconds for a given transient.
 *
 * @param string $transient_name The name of the lockout transient.
 * @return int Seconds remaining until expiration.
 */
function admilogu_get_lockout_time_seconds($transient_name)
{
    // Check our dedicated expiry option.
    $expiry = get_option($transient_name . '_expiry');
    if ($expiry) {
        $remaining = (int) $expiry - time();
        if ($remaining <= 0) {
            delete_option($transient_name . '_expiry');
            return 0;
        }
        return $remaining;
    }

    // Fallback to transient timeout.
    $timeout = get_option('_transient_timeout_' . $transient_name);
    if ($timeout) {
        $remaining = (int) $timeout - time();
        return $remaining > 0 ? $remaining : 0;
    }

    $lockout_duration = (int) get_option('admilogu_lockout_duration', 30);
    return $lockout_duration * 60;
}


/**
 * Format duration in seconds to MM:SS or human readable minutes.
 *
 * @param int $seconds Total seconds.
 * @return string Formatted duration.
 */
function admilogu_format_duration($seconds)
{
    if ($seconds >= 60) {
        $minutes = floor($seconds / 60);
        $secs = $seconds % 60;
        return sprintf('%d:%02d', $minutes, $secs);
    }
    /* translators: %d: number of seconds */
    return sprintf(__('%d seconds', 'admin-login-guard-branding'), $seconds);
}

/**
 * Check if an IP address is whitelisted.
 *
 * @param string $ip The IP address to check.
 * @return bool True if whitelisted, false otherwise.
 */
function admilogu_is_ip_whitelisted($ip)
{
    $whitelist_str = get_option('admilogu_ip_whitelist', '');
    if (empty($whitelist_str)) {
        return false;
    }

    $whitelisted_ips = array_map('trim', explode("\n", $whitelist_str));
    $whitelisted_ips = array_filter($whitelisted_ips); // Remove empty lines

    return in_array($ip, $whitelisted_ips, true);
}

/**
 * Check if an IP address is blacklisted.
 *
 * @param string $ip The IP address to check.
 * @return bool True if blacklisted, false otherwise.
 */
function admilogu_is_ip_blacklisted($ip)
{
    $blacklist_str = get_option('admilogu_ip_blacklist', '');
    if (empty($blacklist_str)) {
        return false;
    }

    $blacklisted_ips = array_map('trim', explode("\n", $blacklist_str));
    $blacklisted_ips = array_filter($blacklisted_ips); // Remove empty lines

    return in_array($ip, $blacklisted_ips, true);
}
