<?php
/**
 * IP Reputation Checker for Guard Dog.
 *
 * Checks IP addresses against free reputation services to identify malicious IPs.
 *
 * @package GuardDog
 * @since 1.8.45
 */

if (!defined('ABSPATH')) exit; // Exit if accessed directly

class Guard_Dog_Ip_Reputation_Checker {

    /**
     * Cache group for reputation results
     * @var string
     */
    const CACHE_GROUP = 'guard_dog_ip_reputation';

    /**
     * Cache expiry time (24 hours)
     * @var int
     */
    const CACHE_EXPIRY = DAY_IN_SECONDS;

    /**
     * Geo cache expiry time (7 days)
     * @var int
     */
    const GEO_CACHE_EXPIRY = WEEK_IN_SECONDS;

    /**
     * Activity log instance
     * @var Guard_Dog_Activity_Log|null
     */
    private $activity_log;

    /**
     * Constructor
     *
     * @param Guard_Dog_Activity_Log|null $activity_log Activity log instance
     */
    public function __construct( $activity_log = null ) {
        $this->activity_log = $activity_log;
        $this->add_hooks();
    }

    /**
     * Register hooks
     */
    private function add_hooks() {
        // Login-time IP reputation check (priority 5 - before everything else)
        add_filter( 'authenticate', array( $this, 'check_ip_on_login' ), 5, 3 );
    }

    /**
     * Check IP reputation during login attempt
     *
     * @param WP_User|WP_Error|null $user User object, error, or null
     * @param string $username Username
     * @param string $password Password
     * @return WP_User|WP_Error
     */
    public function check_ip_on_login( $user, $username, $password ) {
        // Skip if feature disabled
        if ( ! get_option( 'guard_dog_ip_reputation_login_enabled', true ) ) {
            return $user;
        }

        // Skip if no username provided (empty form submission)
        if ( empty( $username ) ) {
            return $user;
        }

        // Skip for WP-CLI
        if ( defined( 'WP_CLI' ) && WP_CLI ) {
            return $user;
        }

        $ip = Guard_Dog_Utils::get_client_ip();
        $threshold = (int) get_option( 'guard_dog_ip_reputation_login_threshold', 80 );

        $result = $this->check_ip_reputation( $ip );

        // Skip blocking if API failed
        if ( ! $result['success'] ) {
            return $user;
        }

        $abuse_confidence = isset( $result['abuse_confidence'] ) ? (int) $result['abuse_confidence'] : 0;

        // Block if abuse confidence exceeds threshold
        if ( $abuse_confidence >= $threshold ) {
            // Log the blocked attempt
            $this->log_blocked_login( $ip, $username, $result );

            return new \WP_Error(
                'ip_blocked',
                sprintf(
                    /* translators: %d is the abuse confidence score */
                    __( 'Login blocked due to suspicious IP activity (confidence: %d%%).', 'guard-dog' ),
                    $abuse_confidence
                )
            );
        }

        return $user;
    }

    /**
     * Log a blocked login attempt
     *
     * @param string $ip IP address
     * @param string $username Attempted username
     * @param array $reputation_data IP reputation data
     */
    private function log_blocked_login( $ip, $username, $reputation_data ) {
        if ( ! $this->activity_log ) {
            return;
        }

        $reason = sprintf(
            'IP reputation check failed (confidence: %d%%, status: %s, user: %s)',
            $reputation_data['abuse_confidence'],
            $reputation_data['status'],
            $username
        );

        $this->activity_log->log_ip_blocked( $ip, $reason );
    }

    /**
     * Check IP reputation using available services
     *
     * @param string $ip_address The IP address to check
     * @return array Reputation data with status, confidence, and details
     */
    public function check_ip_reputation(string $ip_address): array {
        // Validate IP address
        if (!filter_var($ip_address, FILTER_VALIDATE_IP)) {
            return [
                'success' => false,
                'error' => __('Invalid IP address', 'guard-dog'),
            ];
        }

        // Check cache first
        $cache_key = 'ip_reputation_' . md5($ip_address);
        $cached_result = wp_cache_get($cache_key, self::CACHE_GROUP);
        
        if ($cached_result !== false) {
            return $cached_result;
        }

        // Build result from DNSBL, Tor, and optional geo (see helpers below)
        $dnsbl   = $this->check_dnsbl( $ip_address );
        $is_tor  = $this->check_tor( $ip_address );
        $geo     = $this->fetch_geo_network( $ip_address );

        $abuse_confidence = 0;
        if ( $is_tor ) {
            $abuse_confidence = 100;
        } elseif ( $dnsbl['listed'] ) {
            $abuse_confidence = 85;
        } elseif ( ! empty( $geo['proxy'] ) ) {
            $abuse_confidence = 50;
        } elseif ( ! empty( $geo['hosting'] ) ) {
            $abuse_confidence = 30;
        }

        $status = 'clean';
        $status_label = __( 'Clean', 'guard-dog' );
        if ( $is_tor ) {
            $status = 'malicious';
            $status_label = __( 'Tor Exit Node', 'guard-dog' );
        } elseif ( $abuse_confidence >= 75 ) {
            $status = 'malicious';
            $status_label = __( 'Malicious', 'guard-dog' );
        } elseif ( $abuse_confidence >= 50 ) {
            $status = 'suspicious';
            $status_label = __( 'Suspicious', 'guard-dog' );
        } elseif ( $abuse_confidence >= 25 ) {
            $status = 'low_risk';
            $status_label = __( 'Low Risk', 'guard-dog' );
        }

        $threat_indicators = array();
        if ( $is_tor ) {
            $threat_indicators[] = __( 'Tor exit node', 'guard-dog' );
        }
        if ( $dnsbl['listed'] && ! empty( $dnsbl['list_names'] ) ) {
            $threat_indicators[] = sprintf(
                /* translators: %s is the DNSBL list names */
                __( 'Listed in: %s', 'guard-dog' ),
                implode( ', ', $dnsbl['list_names'] )
            );
        }
        if ( $abuse_confidence >= 50 ) {
            $threat_indicators[] = __( 'High abuse confidence', 'guard-dog' );
        }
        if ( ! empty( $geo['hosting'] ) ) {
            $threat_indicators[] = __( 'Hosting provider IP', 'guard-dog' );
        }
        if ( ! empty( $geo['proxy'] ) ) {
            $threat_indicators[] = __( 'Proxy/VPN detected', 'guard-dog' );
        }

        $service = 'local';
        if ( ! empty( $geo['provider'] ) ) {
            $service = $geo['provider'];
        }

        $usage_type = 'ISP';
        if ( ! empty( $geo['proxy'] ) ) {
            $usage_type = 'Proxy/VPN';
        } elseif ( ! empty( $geo['hosting'] ) ) {
            $usage_type = 'Hosting';
        } elseif ( ! empty( $geo['mobile'] ) ) {
            $usage_type = 'Mobile';
        }

        $result = array(
            'success'            => true,
            'service'            => $service,
            'ip_address'         => $ip_address,
            'status'             => $status,
            'status_label'       => $status_label,
            'abuse_confidence'   => $abuse_confidence,
            'total_reports'      => 0,
            'num_distinct_users' => 0,
            'last_reported_at'   => '',
            'country_code'       => isset( $geo['country_code'] ) ? $geo['country_code'] : '',
            'country'            => isset( $geo['country'] ) ? $geo['country'] : '',
            'region'             => isset( $geo['region'] ) ? $geo['region'] : '',
            'city'               => isset( $geo['city'] ) ? $geo['city'] : '',
            'timezone'           => isset( $geo['timezone'] ) ? $geo['timezone'] : '',
            'isp'                => isset( $geo['isp'] ) ? $geo['isp'] : '',
            'org'                => isset( $geo['org'] ) ? $geo['org'] : '',
            'as'                 => isset( $geo['as'] ) ? $geo['as'] : '',
            'asname'             => isset( $geo['asname'] ) ? $geo['asname'] : '',
            'continent'          => isset( $geo['continent'] ) ? $geo['continent'] : '',
            'domain'             => '',
            'reverse_dns'        => isset( $geo['reverse_dns'] ) ? $geo['reverse_dns'] : '',
            'usage_type'         => $usage_type,
            'is_whitelisted'     => false,
            'is_tor'             => $is_tor,
            'is_proxy'           => ! empty( $geo['proxy'] ),
            'is_hosting'         => ! empty( $geo['hosting'] ),
            'is_mobile'          => ! empty( $geo['mobile'] ),
            'dnsbl_listed_count' => $dnsbl['count'],
            'threat_indicators'  => $threat_indicators,
            'checked_at'         => current_time( 'mysql' ),
        );

        // Cache the result
        wp_cache_set($cache_key, $result, self::CACHE_GROUP, self::CACHE_EXPIRY);

        return $result;
    }

    /**
     * Check IP against DNS blocklists (Spamhaus ZEN, CBL). IPv4 only.
     * On DNS failure, treats as not listed (fail open).
     *
     * @param string $ip IPv4 address
     * @return array { listed: bool, list_names: string[], count: int }
     */
    private function check_dnsbl( string $ip ): array {
        $default = array( 'listed' => false, 'list_names' => array(), 'count' => 0 );
        if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
            return $default;
        }
        $rev = implode( '.', array_reverse( explode( '.', $ip ) ) );
        $lists = array(
            'zen' => array( 'host' => $rev . '.zen.spamhaus.org', 'name' => 'Spamhaus ZEN' ),
            'cbl' => array( 'host' => $rev . '.cbl.abuseat.org', 'name' => 'CBL' ),
        );
        $list_names = array();
        foreach ( $lists as $key => $info ) {
            $rec = @dns_get_record( $info['host'], DNS_A );
            if ( is_array( $rec ) && ! empty( $rec ) ) {
                $list_names[] = $info['name'];
            }
        }
        return array(
            'listed'     => count( $list_names ) > 0,
            'list_names' => $list_names,
            'count'      => count( $list_names ),
        );
    }

    /**
     * Check if IP is a Tor exit node via dan.me.uk. IPv4 only.
     * On failure, treats as not Tor (fail open).
     *
     * @param string $ip IPv4 address
     * @return bool
     */
    private function check_tor( string $ip ): bool {
        if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
            return false;
        }
        $rev   = implode( '.', array_reverse( explode( '.', $ip ) ) );
        $host  = $rev . '.torexit.dan.me.uk';
        $rec   = @dns_get_record( $host, DNS_A );
        if ( ! is_array( $rec ) || empty( $rec ) ) {
            return false;
        }
        foreach ( $rec as $r ) {
            if ( isset( $r['ip'] ) && $r['ip'] === '127.0.0.100' ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Fetch geo and network data from ipinfo.io (if token set) or ip-api.com.
     * Skips HTTP when guard_dog_ip_reputation_disable_geo is true.
     *
     * @param string $ip IP address
     * @return array Common shape: country_code, country, region, city, timezone, continent, isp, org, as, asname, reverse_dns, proxy, hosting, mobile; provider (ipapi|ipinfo) or null on skip/failure
     */
    private function fetch_geo_network( string $ip ): array {
        $empty = array(
            'country_code' => '', 'country' => '', 'region' => '', 'city' => '',
            'timezone' => '', 'continent' => '', 'isp' => '', 'org' => '', 'as' => '', 'asname' => '',
            'reverse_dns' => '', 'proxy' => false, 'hosting' => false, 'mobile' => false, 'provider' => null,
        );
        if ( get_option( 'guard_dog_ip_reputation_disable_geo', false ) ) {
            return $empty;
        }
        $token = get_option( 'guard_dog_ipinfo_token', '' );
        if ( is_string( $token ) && trim( $token ) !== '' ) {
            $out = $this->fetch_geo_ipinfo( $ip, trim( $token ) );
            if ( ! empty( $out['provider'] ) ) {
                return $out;
            }
        }
        return $this->fetch_geo_ipapi( $ip );
    }

    /**
     * Fetch geo from ipinfo.io.
     *
     * @param string $ip   IP address
     * @param string $token ipinfo token
     * @return array Common shape; provider=ipinfo on success, else provider=null
     */
    private function fetch_geo_ipinfo( string $ip, string $token ): array {
        $empty = array(
            'country_code' => '', 'country' => '', 'region' => '', 'city' => '',
            'timezone' => '', 'continent' => '', 'isp' => '', 'org' => '', 'as' => '', 'asname' => '',
            'reverse_dns' => '', 'proxy' => false, 'hosting' => false, 'mobile' => false, 'provider' => null,
        );
        $url = 'https://ipinfo.io/' . urlencode( $ip ) . '?token=' . urlencode( $token );
        $response = wp_remote_get( $url, array( 'timeout' => 5, 'headers' => array( 'Accept' => 'application/json' ) ) );
        if ( is_wp_error( $response ) ) {
            if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                Guard_Dog()->debug->error( 'IP Reputation ipinfo error: ' . $response->get_error_message() );
            }
            return $empty;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        if ( $code < 200 || $code >= 300 ) {
            return $empty;
        }
        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );
        if ( ! is_array( $data ) ) {
            return $empty;
        }
        $country = isset( $data['country'] ) ? $data['country'] : '';
        return array(
            'country_code' => $country,
            'country'      => $country,
            'region'       => isset( $data['region'] ) ? $data['region'] : '',
            'city'         => isset( $data['city'] ) ? $data['city'] : '',
            'timezone'     => isset( $data['timezone'] ) ? $data['timezone'] : '',
            'continent'    => '',
            'isp'          => isset( $data['org'] ) ? $data['org'] : '',
            'org'          => isset( $data['org'] ) ? $data['org'] : '',
            'as'           => '',
            'asname'       => '',
            'reverse_dns'  => isset( $data['hostname'] ) ? $data['hostname'] : '',
            'proxy'        => false,
            'hosting'      => false,
            'mobile'       => false,
            'provider'     => 'ipinfo',
        );
    }

    /**
     * Fetch geo from ip-api.com.
     *
     * @param string $ip IP address
     * @return array Common shape; provider=ipapi on success, else provider=null
     */
    private function fetch_geo_ipapi( string $ip ): array {
        $empty = array(
            'country_code' => '', 'country' => '', 'region' => '', 'city' => '',
            'timezone' => '', 'continent' => '', 'isp' => '', 'org' => '', 'as' => '', 'asname' => '',
            'reverse_dns' => '', 'proxy' => false, 'hosting' => false, 'mobile' => false, 'provider' => null,
        );
        $fields = 'status,message,country,countryCode,regionName,city,timezone,continent,continentCode,isp,org,as,asname,mobile,proxy,hosting,reverse';
        $url = 'http://ip-api.com/json/' . urlencode( $ip ) . '?fields=' . $fields;
        $response = wp_remote_get( $url, array( 'timeout' => 5, 'headers' => array( 'Accept' => 'application/json' ) ) );
        if ( is_wp_error( $response ) ) {
            if ( function_exists( 'Guard_Dog' ) && Guard_Dog()->debug ) {
                Guard_Dog()->debug->error( 'IP Reputation ip-api error: ' . $response->get_error_message() );
            }
            return $empty;
        }
        $code = (int) wp_remote_retrieve_response_code( $response );
        if ( $code === 429 || $code < 200 || $code >= 300 ) {
            return $empty;
        }
        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );
        if ( ! is_array( $data ) || ( isset( $data['status'] ) && $data['status'] !== 'success' ) ) {
            return $empty;
        }
        return array(
            'country_code' => isset( $data['countryCode'] ) ? $data['countryCode'] : '',
            'country'      => isset( $data['country'] ) ? $data['country'] : '',
            'region'       => isset( $data['regionName'] ) ? $data['regionName'] : '',
            'city'         => isset( $data['city'] ) ? $data['city'] : '',
            'timezone'     => isset( $data['timezone'] ) ? $data['timezone'] : '',
            'continent'    => isset( $data['continent'] ) ? $data['continent'] : ( isset( $data['continentCode'] ) ? $data['continentCode'] : '' ),
            'isp'          => isset( $data['isp'] ) ? $data['isp'] : '',
            'org'          => isset( $data['org'] ) ? $data['org'] : '',
            'as'           => isset( $data['as'] ) ? $data['as'] : '',
            'asname'       => isset( $data['asname'] ) ? $data['asname'] : '',
            'reverse_dns'  => isset( $data['reverse'] ) ? $data['reverse'] : '',
            'proxy'        => ! empty( $data['proxy'] ),
            'hosting'      => ! empty( $data['hosting'] ),
            'mobile'       => ! empty( $data['mobile'] ),
            'provider'     => 'ipapi',
        );
    }

    /**
     * Clear cached reputation for an IP address
     *
     * @param string $ip_address The IP address to clear cache for
     * @return bool True on success, false on failure
     */
    public function clear_cache(string $ip_address): bool {
        $cache_key = 'ip_reputation_' . md5($ip_address);
        return wp_cache_delete($cache_key, self::CACHE_GROUP);
    }

    /**
     * Get geo data from cache for an IP address.
     *
     * Uses WordPress transients for persistent storage across page loads.
     *
     * @param string $ip_address The IP address to get geo data for.
     * @return array|false Cached geo data or false if not cached.
     */
    public function get_cached_geo( string $ip_address ) {
        $transient_key = 'guard_dog_geo_' . md5( $ip_address );
        return get_transient( $transient_key );
    }

    /**
     * Cache geo data for an IP address.
     *
     * @param string $ip_address The IP address.
     * @param array  $geo_data   Geo data to cache (country_code, country, city, region, timezone, continent).
     * @return bool True on success, false on failure.
     */
    public function cache_geo_data( string $ip_address, array $geo_data ): bool {
        $transient_key = 'guard_dog_geo_' . md5( $ip_address );
        return set_transient( $transient_key, $geo_data, self::GEO_CACHE_EXPIRY );
    }

    /**
     * Get geo data with fallback to cache.
     *
     * Primary method for getting geo data that handles API failures gracefully.
     *
     * @param string $ip_address The IP address to get geo data for.
     * @return array Geo data array with 'success' key indicating if data is from live API or cache.
     */
    public function get_geo_data_with_fallback( string $ip_address ): array {
        // First try the full reputation check
        $reputation = $this->check_ip_reputation( $ip_address );

        if ( $reputation['success'] ) {
            // Extract and cache geo data
            $geo_data = array(
                'country_code' => isset( $reputation['country_code'] ) ? $reputation['country_code'] : '',
                'country'      => isset( $reputation['country'] ) ? $reputation['country'] : '',
                'city'         => isset( $reputation['city'] ) ? $reputation['city'] : '',
                'region'       => isset( $reputation['region'] ) ? $reputation['region'] : '',
                'timezone'     => isset( $reputation['timezone'] ) ? $reputation['timezone'] : '',
                'continent'    => isset( $reputation['continent'] ) ? $reputation['continent'] : '',
            );

            $this->cache_geo_data( $ip_address, $geo_data );

            return array(
                'success'    => true,
                'from_cache' => false,
                'geo_data'   => $geo_data,
            );
        }

        // API failed - try to use cached geo data
        $cached_geo = $this->get_cached_geo( $ip_address );

        if ( false !== $cached_geo ) {
            Guard_Dog()->debug->log( 'IP Reputation API failed, using cached geo data for: ' . $ip_address, 'WARNING' );

            return array(
                'success'    => true,
                'from_cache' => true,
                'geo_data'   => $cached_geo,
            );
        }

        // No cache available - log warning
        Guard_Dog()->debug->log( 'IP Reputation API failed and no cached geo data available for: ' . $ip_address, 'WARNING' );

        return array(
            'success'    => false,
            'from_cache' => false,
            'geo_data'   => array(
                'country_code' => '',
                'country'      => '',
                'city'         => '',
                'region'       => '',
                'timezone'     => '',
                'continent'    => '',
            ),
            'error'      => __( 'Geo data unavailable - API failed and no cached data.', 'guard-dog' ),
        );
    }

    /**
     * Clear cached geo data for an IP address.
     *
     * @param string $ip_address The IP address to clear geo cache for.
     * @return bool True on success, false on failure.
     */
    public function clear_geo_cache( string $ip_address ): bool {
        $transient_key = 'guard_dog_geo_' . md5( $ip_address );
        return delete_transient( $transient_key );
    }

}