<?php
/**
 * Address Validation Utility
 *
 * @package SPAI_Contact_Form
 */

class SPAI_Address_Validator {
    
    /**
     * Complete ZIP code to state mapping - All US ZIP ranges
     */
    private static $zip_to_state = array(
        // Massachusetts (01xxx, 02xxx)
        '01' => 'MA', '02' => 'MA',
        // New Hampshire (03xxx)
        '03' => 'NH',
        // Maine (04xxx)  
        '04' => 'ME',
        // Vermont (05xxx)
        '05' => 'VT',
        // Connecticut (06xxx)
        '06' => 'CT',
        // New Jersey (07xxx, 08xxx)
        '07' => 'NJ', '08' => 'NJ',
        // New York (10xxx-14xxx)
        '10' => 'NY', '11' => 'NY', '12' => 'NY', '13' => 'NY', '14' => 'NY',
        // Pennsylvania (15xxx-19xxx)
        '15' => 'PA', '16' => 'PA', '17' => 'PA', '18' => 'PA', '19' => 'PA',
        // Delaware (19xxx) - Shared with PA
        '19' => array('DE', 'PA'),
        // Washington DC (20xxx) & Maryland (20xxx-21xxx) 
        '20' => array('DC', 'MD'), '21' => 'MD',
        // Virginia (22xxx-24xxx)
        '22' => 'VA', '23' => 'VA', '24' => 'VA',
        // West Virginia (24xxx-26xxx) - 24 shared with VA
        '24' => array('VA', 'WV'), '25' => 'WV', '26' => 'WV',
        // North Carolina (27xxx-28xxx)
        '27' => 'NC', '28' => 'NC',
        // South Carolina (29xxx)
        '29' => 'SC',
        // Georgia (30xxx-31xxx)
        '30' => 'GA', '31' => 'GA',
        // Florida (32xxx-34xxx)
        '32' => 'FL', '33' => 'FL', '34' => 'FL',
        // Alabama (35xxx-36xxx)
        '35' => 'AL', '36' => 'AL',
        // Tennessee (37xxx-38xxx)
        '37' => 'TN', '38' => 'TN',
        // Mississippi (38xxx-39xxx) - 38 shared with TN
        '38' => array('TN', 'MS'), '39' => 'MS',
        // Kentucky (40xxx-42xxx)
        '40' => 'KY', '41' => 'KY', '42' => 'KY',
        // Ohio (43xxx-45xxx)
        '43' => 'OH', '44' => 'OH', '45' => 'OH',
        // Indiana (46xxx-47xxx)
        '46' => 'IN', '47' => 'IN',
        // Michigan (48xxx-49xxx)
        '48' => 'MI', '49' => 'MI',
        // Iowa (50xxx-52xxx)
        '50' => 'IA', '51' => 'IA', '52' => 'IA',
        // Wisconsin (53xxx-54xxx)
        '53' => 'WI', '54' => 'WI',
        // Minnesota (55xxx-56xxx)
        '55' => 'MN', '56' => 'MN',
        // South Dakota (57xxx)
        '57' => 'SD',
        // North Dakota (58xxx)
        '58' => 'ND',
        // Montana (59xxx)
        '59' => 'MT',
        // Illinois (60xxx-62xxx)
        '60' => 'IL', '61' => 'IL', '62' => 'IL',
        // Missouri (63xxx-65xxx)
        '63' => 'MO', '64' => 'MO', '65' => 'MO',
        // Kansas (66xxx-67xxx)
        '66' => 'KS', '67' => 'KS',
        // Nebraska (68xxx-69xxx)
        '68' => 'NE', '69' => 'NE',
        // Louisiana (70xxx-71xxx)
        '70' => 'LA', '71' => 'LA',
        // Arkansas (71xxx-72xxx) - 71 shared with LA
        '71' => array('LA', 'AR'), '72' => 'AR',
        // Oklahoma (73xxx-74xxx)
        '73' => 'OK', '74' => 'OK',
        // Texas (75xxx-79xxx)
        '75' => 'TX', '76' => 'TX', '77' => 'TX', '78' => 'TX', '79' => 'TX',
        // Colorado (80xxx-81xxx)
        '80' => 'CO', '81' => 'CO',
        // Wyoming (82xxx)
        '82' => 'WY',
        // Idaho (83xxx)
        '83' => 'ID',
        // Utah (84xxx)
        '84' => 'UT',
        // Arizona (85xxx-86xxx)
        '85' => 'AZ', '86' => 'AZ',
        // New Mexico (87xxx-88xxx)
        '87' => 'NM', '88' => 'NM',
        // Nevada (89xxx)
        '89' => 'NV',
        // California (90xxx-96xxx)
        '90' => 'CA', '91' => 'CA', '92' => 'CA', '93' => 'CA', '94' => 'CA', '95' => 'CA', '96' => 'CA',
        // Hawaii (96xxx) - 96 shared with CA
        '96' => array('CA', 'HI'),
        // Oregon (97xxx)
        '97' => 'OR',
        // Washington (98xxx)
        '98' => 'WA',
        // Alaska (99xxx)
        '99' => 'AK'
    );
    
    /**
     * Major cities to state mapping - Top 100 US cities
     */
    private static $city_to_state = array(
        // Top 50 largest US cities
        'new york' => array('NY'),
        'los angeles' => array('CA'),
        'chicago' => array('IL'),
        'houston' => array('TX'),
        'phoenix' => array('AZ'),
        'philadelphia' => array('PA'),
        'san antonio' => array('TX'),
        'san diego' => array('CA'),
        'dallas' => array('TX'),
        'san jose' => array('CA'),
        'austin' => array('TX'),
        'jacksonville' => array('FL'),
        'fort worth' => array('TX'),
        'columbus' => array('OH'),
        'charlotte' => array('NC'),
        'san francisco' => array('CA'),
        'indianapolis' => array('IN'),
        'seattle' => array('WA'),
        'denver' => array('CO'),
        'washington' => array('DC'),
        'boston' => array('MA'),
        'el paso' => array('TX'),
        'nashville' => array('TN'),
        'detroit' => array('MI'),
        'oklahoma city' => array('OK'),
        'portland' => array('OR'),
        'las vegas' => array('NV'),
        'memphis' => array('TN'),
        'louisville' => array('KY'),
        'baltimore' => array('MD'),
        'milwaukee' => array('WI'),
        'albuquerque' => array('NM'),
        'tucson' => array('AZ'),
        'fresno' => array('CA'),
        'mesa' => array('AZ'),
        'sacramento' => array('CA'),
        'atlanta' => array('GA'),
        'kansas city' => array('MO'),
        'colorado springs' => array('CO'),
        'omaha' => array('NE'),
        'raleigh' => array('NC'),
        'miami' => array('FL'),
        'long beach' => array('CA'),
        'virginia beach' => array('VA'),
        'oakland' => array('CA'),
        'minneapolis' => array('MN'),
        'tulsa' => array('OK'),
        'arlington' => array('TX'),
        'tampa' => array('FL'),
        'new orleans' => array('LA'),
        
        // Cities 51-100
        'wichita' => array('KS'),
        'cleveland' => array('OH'),
        'bakersfield' => array('CA'),
        'aurora' => array('CO'),
        'anaheim' => array('CA'),
        'honolulu' => array('HI'),
        'santa ana' => array('CA'),
        'corpus christi' => array('TX'),
        'riverside' => array('CA'),
        'lexington' => array('KY'),
        'stockton' => array('CA'),
        'toledo' => array('OH'),
        'st. paul' => array('MN'),
        'st. petersburg' => array('FL'),
        'pittsburgh' => array('PA'),
        'cincinnati' => array('OH'),
        'anchorage' => array('AK'),
        'henderson' => array('NV'),
        'greensboro' => array('NC'),
        'plano' => array('TX'),
        'newark' => array('NJ'),
        'lincoln' => array('NE'),
        'buffalo' => array('NY'),
        'jersey city' => array('NJ'),
        'chula vista' => array('CA'),
        'fort wayne' => array('IN'),
        'orlando' => array('FL'),
        'st. louis' => array('MO'),
        'chandler' => array('AZ'),
        'madison' => array('WI'),
        'lubbock' => array('TX'),
        'laredo' => array('TX'),
        'norfolk' => array('VA'),
        'durham' => array('NC'),
        'winston salem' => array('NC'),
        'garland' => array('TX'),
        'glendale' => array('AZ'),
        'hialeah' => array('FL'),
        'reno' => array('NV'),
        'baton rouge' => array('LA'),
        'irvine' => array('CA'),
        'chesapeake' => array('VA'),
        'irving' => array('TX'),
        'scottsdale' => array('AZ'),
        'north las vegas' => array('NV'),
        'fremont' => array('CA'),
        'gilbert' => array('AZ'),
        'san bernardino' => array('CA'),
        'boise' => array('ID'),
        'birmingham' => array('AL'),
        
        // NYC Boroughs
        'brooklyn' => array('NY'),
        'manhattan' => array('NY'),
        'queens' => array('NY'),
        'bronx' => array('NY'),
        'staten island' => array('NY'),
        
        // Cities with same names in multiple states (allow all valid states)
        'springfield' => array('IL', 'MA', 'MO', 'OH'),
        'portland' => array('ME', 'OR'),
        'columbus' => array('OH', 'GA'),
        'aurora' => array('CO', 'IL'),
        'richmond' => array('VA', 'CA'),
        'cambridge' => array('MA', 'MD')
    );
    
    /**
     * State abbreviations to full names
     */
    private static $state_names = array(
        'AL' => 'Alabama', 'AK' => 'Alaska', 'AZ' => 'Arizona', 'AR' => 'Arkansas',
        'CA' => 'California', 'CO' => 'Colorado', 'CT' => 'Connecticut', 'DE' => 'Delaware',
        'FL' => 'Florida', 'GA' => 'Georgia', 'HI' => 'Hawaii', 'ID' => 'Idaho',
        'IL' => 'Illinois', 'IN' => 'Indiana', 'IA' => 'Iowa', 'KS' => 'Kansas',
        'KY' => 'Kentucky', 'LA' => 'Louisiana', 'ME' => 'Maine', 'MD' => 'Maryland',
        'MA' => 'Massachusetts', 'MI' => 'Michigan', 'MN' => 'Minnesota', 'MS' => 'Mississippi',
        'MO' => 'Missouri', 'MT' => 'Montana', 'NE' => 'Nebraska', 'NV' => 'Nevada',
        'NH' => 'New Hampshire', 'NJ' => 'New Jersey', 'NM' => 'New Mexico', 'NY' => 'New York',
        'NC' => 'North Carolina', 'ND' => 'North Dakota', 'OH' => 'Ohio', 'OK' => 'Oklahoma',
        'OR' => 'Oregon', 'PA' => 'Pennsylvania', 'RI' => 'Rhode Island', 'SC' => 'South Carolina',
        'SD' => 'South Dakota', 'TN' => 'Tennessee', 'TX' => 'Texas', 'UT' => 'Utah',
        'VT' => 'Vermont', 'VA' => 'Virginia', 'WA' => 'Washington', 'WV' => 'West Virginia',
        'WI' => 'Wisconsin', 'WY' => 'Wyoming', 'DC' => 'Washington DC'
    );
    
    /**
     * Validate US address components
     */
    public static function validate_us_address($address_data) {
        $errors = array();
        
        // Extract address components - support both formats
        $street = isset($address_data['street']) ? trim($address_data['street']) : (isset($address_data['address.street']) ? trim($address_data['address.street']) : '');
        $city = isset($address_data['city']) ? trim($address_data['city']) : (isset($address_data['address.city']) ? trim($address_data['address.city']) : '');
        $state = isset($address_data['state']) ? trim($address_data['state']) : (isset($address_data['address.state']) ? trim($address_data['address.state']) : '');
        $zip = isset($address_data['zip']) ? trim($address_data['zip']) : (isset($address_data['address.zipCode']) ? trim($address_data['address.zipCode']) : '');
        
        // Skip validation if no address fields provided
        if (empty($street) && empty($city) && empty($state) && empty($zip)) {
            return array('valid' => true, 'errors' => array());
        }
        
        // Normalize state (convert full name to abbreviation if needed)
        $state = self::normalize_state($state);
        
        // Validate ZIP code format and state consistency
        if (!empty($zip) && !empty($state)) {
            $zip_validation = self::validate_zip_state($zip, $state);
            if (!$zip_validation['valid']) {
                $errors[] = $zip_validation['error'];
            }
        }
        
        // Validate city and state consistency
        if (!empty($city) && !empty($state)) {
            $city_validation = self::validate_city_state($city, $state);
            if (!$city_validation['valid']) {
                $errors[] = $city_validation['error'];
            }
        }
        
        return array(
            'valid' => empty($errors),
            'errors' => $errors
        );
    }
    
    /**
     * Validate ZIP code and state consistency
     */
    private static function validate_zip_state($zip, $state) {
        // Extract first 2 digits of ZIP code
        $zip_prefix = substr($zip, 0, 2);
        
        // Check if ZIP prefix maps to expected state(s)
        if (isset(self::$zip_to_state[$zip_prefix])) {
            $expected_states = is_array(self::$zip_to_state[$zip_prefix]) 
                ? self::$zip_to_state[$zip_prefix] 
                : array(self::$zip_to_state[$zip_prefix]);
                
            if (!in_array($state, $expected_states)) {
                return array(
                    'valid' => false,
                    'error' => "Please verify your address."
                );
            }
        }
        
        return array('valid' => true);
    }
    
    /**
     * Validate city and state consistency
     */
    private static function validate_city_state($city, $state) {
        $city_lower = strtolower(trim($city));
        
        // Check if city is in our known city-to-state mapping
        if (isset(self::$city_to_state[$city_lower])) {
            $valid_states = self::$city_to_state[$city_lower];
            
            if (!in_array($state, $valid_states)) {
                return array(
                    'valid' => false,
                    'error' => "Please verify your address."
                );
            }
        }
        
        return array('valid' => true);
    }
    
    /**
     * Normalize state input (convert full name to abbreviation)
     */
    private static function normalize_state($state) {
        $state = trim($state);
        
        // If already an abbreviation, return uppercase
        if (strlen($state) == 2) {
            return strtoupper($state);
        }
        
        // Convert full state name to abbreviation
        $state_lower = strtolower($state);
        foreach (self::$state_names as $abbr => $name) {
            if (strtolower($name) === $state_lower) {
                return $abbr;
            }
        }
        
        return strtoupper($state); // Return as-is if not found
    }
    
    /**
     * Check if IP geolocation is consistent with submitted address
     */
    public static function validate_ip_consistency($address_data, $ip_address = null) {
        // Skip if no GeoIP available
        if (!class_exists('SPAI_GeoIP')) {
            return array('valid' => true, 'warning' => null);
        }
        
        $ip = $ip_address ?: self::get_client_ip();
        $state = isset($address_data['address.state']) ? trim($address_data['address.state']) : '';
        
        if (empty($state) || empty($ip)) {
            return array('valid' => true, 'warning' => null);
        }
        
        try {
            $geoip = new SPAI_GeoIP();
            $geo_data = $geoip->lookup($ip);
            
            if ($geo_data && isset($geo_data['country_code']) && $geo_data['country_code'] === 'US') {
                // For US IPs, we could potentially validate state consistency
                // For now, just log suspicious cases for manual review
                // This is more of a warning than a hard validation failure
                return array('valid' => true, 'warning' => null);
            }
        } catch (Exception $e) {
            // GeoIP lookup failed, continue without IP validation
        }
        
        return array('valid' => true, 'warning' => null);
    }
    
    /**
     * Get client IP address
     */
    private static function get_client_ip() {
        $ip_keys = array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
        
        foreach ($ip_keys as $key) {
            if (array_key_exists($key, $_SERVER) === true) {
                foreach (explode(',', $_SERVER[$key]) as $ip) {
                    $ip = trim($ip);
                    
                    if (filter_var($ip, FILTER_VALIDATE_IP,
                        FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
                        return $ip;
                    }
                }
            }
        }
        
        return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
    }
}