const axios = require('axios');
const winston = require('winston');

// Configure logger
const logger = winston.createLogger({
    level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
    ),
    transports: [
        new winston.transports.Console({
            format: winston.format.simple()
        })
    ]
});

// Cache for geocoding results (simple in-memory cache)
const geocodeCache = new Map();
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours

// Clean up old cache entries periodically
setInterval(() => {
    const now = Date.now();
    for (const [key, value] of geocodeCache.entries()) {
        if (now - value.timestamp > CACHE_TTL) {
            geocodeCache.delete(key);
        }
    }
}, 60 * 60 * 1000); // Run every hour

/**
 * Multi-provider geocoding configuration
 * Providers are tried in order until one succeeds with good confidence
 */
// Provider configuration - order matters (higher quality providers first)
const GEOCODING_PROVIDERS = [
    // Premium provider (when API key is available)
    {
        name: 'Mapbox',
        func: geocodeWithMapbox,
        enabled: () => !!(process.env.MAPBOX_ACCESS_TOKEN || process.env.MAPBOX_API_KEY),
        options: { timeout: 8000, delay: 0 }
    },
    // Free providers (fallbacks)
    {
        name: 'Nominatim',
        func: geocodeWithNominatim,
        enabled: () => true,
        options: { timeout: 5000, delay: 1000 }
    },
    {
        name: 'Photon',
        func: geocodeWithPhoton,
        enabled: () => true,
        options: { timeout: 5000, delay: 500 }
    },
    {
        name: 'LocationIQ',
        func: geocodeWithLocationIQ,
        enabled: () => !!process.env.LOCATIONIQ_API_KEY,
        options: { timeout: 5000, delay: 0 }
    },
    {
        name: 'ArcGIS',
        func: geocodeWithArcGIS,
        enabled: () => true,
        options: { timeout: 8000, delay: 500 }
    }
];

/**
 * Geocode with Nominatim (OpenStreetMap)
 */
// Geocoding provider functions
async function geocodeWithMapbox(address, options = {}) {
    const { timeout = 5000, delay = 0 } = options;
    const apiKey = process.env.MAPBOX_ACCESS_TOKEN || process.env.MAPBOX_API_KEY;
    
    if (!apiKey) {
        throw new Error('Mapbox API key not configured');
    }
    
    // Rate limiting (Mapbox has generous rate limits)
    if (delay > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
    }
    
    // Parse address components for structured input
    const addressComponents = parseAddressString(address);
    let url;
    
    if (addressComponents.hasComponents) {
        // Use structured input for better accuracy
        const params = new URLSearchParams({
            access_token: apiKey,
            limit: 1,
            country: addressComponents.country || 'ca' // Default to Canada
        });
        
        if (addressComponents.address_number) params.append('address_number', addressComponents.address_number);
        if (addressComponents.street) params.append('street', addressComponents.street);
        if (addressComponents.place) params.append('place', addressComponents.place);
        if (addressComponents.region) params.append('region', addressComponents.region);
        if (addressComponents.postcode) params.append('postcode', addressComponents.postcode);
        
        url = `https://api.mapbox.com/search/geocode/v6/forward?${params.toString()}`;
    } else {
        // Fallback to simple search
        url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(address)}.json?access_token=${apiKey}&limit=1&country=ca`;
    }
    
    logger.info(`Geocoding with Mapbox: ${address}`);
    
    try {
        const response = await axios.get(url, { timeout });
        const data = response.data;
        
        // Debug: Log the API response structure
        logger.debug(`Mapbox response structure:`, {
            hasFeatures: !!(data.features && data.features.length > 0),
            hasData: !!(data.data && data.data.length > 0),
            featuresLength: data.features?.length || 0,
            dataLength: data.data?.length || 0,
            firstFeature: data.features?.[0] ? Object.keys(data.features[0]) : null,
            firstData: data.data?.[0] ? Object.keys(data.data[0]) : null
        });
        
        let result;
        
        // Handle direct feature response (newer format)
        if (data.geometry && data.properties) {
            result = data;
        } else if (data.features && data.features.length > 0) {
            // v5 API response format (legacy)
            result = data.features[0];
        } else if (data.data && data.data.length > 0) {
            // v6 API response format (structured)
            result = data.data[0];
        }
        
        if (!result) {
            logger.info(`Mapbox returned no results for address: ${address}`);
            return null;
        }
        
        // Extract coordinates - try multiple possible locations
        let latitude, longitude;
        
        if (result.properties?.coordinates?.latitude && result.properties?.coordinates?.longitude) {
            // New format: properties.coordinates object
            latitude = result.properties.coordinates.latitude;
            longitude = result.properties.coordinates.longitude;
        } else if (result.geometry?.coordinates && Array.isArray(result.geometry.coordinates) && result.geometry.coordinates.length >= 2) {
            // GeoJSON format: geometry.coordinates [lng, lat]
            longitude = result.geometry.coordinates[0];
            latitude = result.geometry.coordinates[1];
        } else if (result.center && Array.isArray(result.center) && result.center.length >= 2) {
            // v5 format: center [lng, lat]
            longitude = result.center[0];
            latitude = result.center[1];
        } else {
            logger.error(`Mapbox result missing valid coordinates:`, {
                hasPropsCoords: !!(result.properties?.coordinates),
                hasGeomCoords: !!(result.geometry?.coordinates),
                hasCenter: !!result.center,
                result: result
            });
            return null;
        }
        
        // Extract formatted address
        let formattedAddress = result.properties?.full_address || 
                             result.properties?.name_preferred || 
                             result.properties?.name || 
                             result.place_name || 
                             'Unknown Address';
        
        // Calculate confidence from match_code if available
        let confidence = 100;
        if (result.properties?.match_code) {
            const matchCode = result.properties.match_code;
            if (matchCode.confidence === 'exact') confidence = 100;
            else if (matchCode.confidence === 'high') confidence = 90;
            else if (matchCode.confidence === 'medium') confidence = 70;
            else if (matchCode.confidence === 'low') confidence = 50;
            else confidence = (result.relevance || 1) * 100;
        } else {
            confidence = (result.relevance || 1) * 100;
        }
        
        return {
            latitude: latitude,
            longitude: longitude,
            formattedAddress: formattedAddress,
            provider: 'Mapbox',
            confidence: confidence,
            components: extractMapboxComponents(result),
            raw: result
        };
        
        // No results found
        logger.info(`Mapbox returned no results for address: ${address}`);
        return null;
    } catch (error) {
        logger.error('Mapbox geocoding error:', error.message);
        throw error;
    }
}

async function geocodeWithNominatim(address, options = {}) {
    const { timeout = 5000, delay = 1000 } = options;
    
    // Rate limiting for Nominatim (1 request per second)
    if (delay > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
    }
    
    const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}&limit=1&addressdetails=1`;
    
    logger.info(`Geocoding with Nominatim: ${address}`);
    
    try {
        const response = await axios.get(url, {
            timeout,
            headers: {
                'User-Agent': 'MapApp/1.0'
            }
        });
        
        const data = response.data;
        if (!data || data.length === 0) {
            return null;
        }
        
        const result = data[0];
        return {
            latitude: parseFloat(result.lat),
            longitude: parseFloat(result.lon),
            formattedAddress: result.display_name,
            provider: 'Nominatim',
            confidence: calculateNominatimConfidence(result),
            components: extractAddressComponents(result.address || {}),
            raw: result
        };
    } catch (error) {
        logger.error('Nominatim geocoding error:', error.message);
        throw error;
    }
}

/**
 * Geocode with Photon (OpenStreetMap-based)
 */
async function geocodeWithPhoton(address, options = {}) {
    const { timeout = 15000, delay = 500 } = options;
    
    // Rate limiting
    if (delay > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
    }
    
    const response = await axios.get('https://photon.komoot.io/api/', {
        params: {
            q: address,
            limit: 1,
            lang: 'en'
        },
        timeout
    });
    
    if (!response.data?.features || response.data.features.length === 0) {
        return null;
    }
    
    const feature = response.data.features[0];
    const coords = feature.geometry.coordinates;
    const props = feature.properties;
    
    return {
        provider: 'photon',
        latitude: coords[1],
        longitude: coords[0],
        formattedAddress: buildFormattedAddressFromPhoton(props),
        components: extractPhotonComponents(props),
        confidence: calculatePhotonConfidence(feature),
        raw: feature
    };
}

/**
 * Geocode with LocationIQ (fallback)
 */
async function geocodeWithLocationIQ(address, options = {}) {
    const { timeout = 15000, delay = 0 } = options;
    const apiKey = process.env.LOCATIONIQ_API_KEY;
    
    // Rate limiting
    if (delay > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
    }
    
    // LocationIQ can work without API key but has rate limits
    const params = {
        format: 'json',
        q: address,
        limit: 1,
        addressdetails: 1,
        countrycodes: 'ca'
    };
    
    // Add API key if available for better rate limits
    if (apiKey) {
        params.key = apiKey;
    }
    
    const response = await axios.get('https://us1.locationiq.com/v1/search.php', {
        params,
        headers: {
            'User-Agent': 'NocoDB Map Viewer 1.0'
        },
        timeout
    });
    
    if (!response.data || response.data.length === 0) {
        return null;
    }
    
    const data = response.data[0];
    return {
        provider: 'locationiq',
        latitude: parseFloat(data.lat),
        longitude: parseFloat(data.lon),
        formattedAddress: buildFormattedAddress(data.address),
        components: extractAddressComponents(data.address),
        confidence: calculateNominatimConfidence(data), // Similar format to Nominatim
        boundingBox: data.boundingbox,
        raw: data
    };
}

/**
 * Geocode with ArcGIS World Geocoding Service
 */
async function geocodeWithArcGIS(address, options = {}) {
    const { timeout = 15000, delay = 500 } = options;
    
    // Rate limiting
    if (delay > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
    }
    
    const response = await axios.get('https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates', {
        params: {
            SingleLine: address,
            f: 'json',
            outFields: '*',
            maxLocations: 1,
            countryCode: 'CA'
        },
        timeout
    });
    
    if (!response.data?.candidates || response.data.candidates.length === 0) {
        return null;
    }
    
    const candidate = response.data.candidates[0];
    const location = candidate.location;
    const attributes = candidate.attributes;
    
    return {
        provider: 'arcgis',
        latitude: location.y,
        longitude: location.x,
        formattedAddress: attributes.LongLabel || candidate.address,
        components: extractArcGISComponents(attributes),
        confidence: candidate.score || 50, // ArcGIS provides score 0-100
        raw: candidate
    };
}

/**
 * Helper functions for address processing
 */
function buildFormattedAddress(addressComponents) {
    if (!addressComponents) return '';
    
    let formattedAddress = '';
    if (addressComponents.house_number) formattedAddress += addressComponents.house_number + ' ';
    if (addressComponents.road) formattedAddress += addressComponents.road + ', ';
    if (addressComponents.suburb) formattedAddress += addressComponents.suburb + ', ';
    if (addressComponents.city) formattedAddress += addressComponents.city + ', ';
    if (addressComponents.state) formattedAddress += addressComponents.state + ' ';
    if (addressComponents.postcode) formattedAddress += addressComponents.postcode;
    
    return formattedAddress.trim().replace(/,$/, '');
}

function buildFormattedAddressFromPhoton(props) {
    let address = '';
    if (props.housenumber) address += props.housenumber + ' ';
    if (props.street) address += props.street + ', ';
    if (props.district) address += props.district + ', ';
    if (props.city) address += props.city + ', ';
    if (props.state) address += props.state + ' ';
    if (props.postcode) address += props.postcode;
    
    return address.trim().replace(/,$/, '');
}

function extractAddressComponents(address) {
    // If address is a string, parse it
    if (typeof address === 'string') {
        return parseAddressString(address);
    }
    
    // If address is already an object (from Nominatim/LocationIQ response)
    return {
        house_number: address?.house_number || '',
        road: address?.road || '',
        suburb: address?.suburb || address?.neighbourhood || '',
        city: address?.city || address?.town || address?.village || '',
        state: address?.state || address?.province || '',
        postcode: address?.postcode || '',
        country: address?.country || ''
    };
}

function parseAddressString(addressStr) {
    // Parse a string address into components for Mapbox structured input
    const components = {
        address_number: '',
        street: '',
        place: '',
        region: '',
        postcode: '',
        country: 'ca',
        hasComponents: false
    };
    
    if (!addressStr || typeof addressStr !== 'string') {
        return components;
    }
    
    // Clean up the address string
    const cleanAddress = addressStr.trim();
    
    // Basic regex patterns for Canadian addresses
    const patterns = {
        // Match house number at the start
        houseNumber: /^(\d+[A-Za-z]?)\s+(.+)/,
        // Match postal code (Canadian format: A1A 1A1)
        postalCode: /([A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d)$/i,
        // Match province abbreviations
        province: /,?\s*(AB|BC|MB|NB|NL|NT|NS|NU|ON|PE|QC|SK|YT|Alberta|British Columbia|Manitoba|New Brunswick|Newfoundland|Northwest Territories|Nova Scotia|Nunavut|Ontario|Prince Edward Island|Quebec|Saskatchewan|Yukon)\s*,?\s*CA(?:NADA)?$/i
    };
    
    let workingAddress = cleanAddress;
    
    // Extract postal code
    const postalMatch = workingAddress.match(patterns.postalCode);
    if (postalMatch) {
        components.postcode = postalMatch[1].replace(/\s/g, '');
        workingAddress = workingAddress.replace(patterns.postalCode, '').trim();
    }
    
    // Extract province/region
    const provinceMatch = workingAddress.match(patterns.province);
    if (provinceMatch) {
        components.region = provinceMatch[1];
        workingAddress = workingAddress.replace(patterns.province, '').trim();
    }
    
    // Extract house number and street
    const houseMatch = workingAddress.match(patterns.houseNumber);
    if (houseMatch) {
        components.address_number = houseMatch[1];
        workingAddress = houseMatch[2];
        components.hasComponents = true;
    }
    
    // The remaining part is likely the street and city
    const parts = workingAddress.split(',').map(p => p.trim()).filter(p => p);
    
    if (parts.length > 0) {
        components.street = parts[0];
        components.hasComponents = true;
    }
    
    if (parts.length > 1) {
        components.place = parts[1];
    }
    
    return components;
}

function extractPhotonComponents(props) {
    return {
        house_number: props.housenumber || '',
        road: props.street || '',
        suburb: props.district || '',
        city: props.city || '',
        state: props.state || '',
        postcode: props.postcode || '',
        country: props.country || ''
    };
}

function extractArcGISComponents(attributes) {
    return {
        house_number: attributes.AddNum || '',
        road: attributes.StName || '',
        suburb: attributes.District || '',
        city: attributes.City || '',
        state: attributes.Region || '',
        postcode: attributes.Postal || '',
        country: attributes.Country || ''
    };
}

function extractMapboxComponents(result) {
    // Universal Mapbox component extractor for multiple API formats
    const components = {
        house_number: '',
        road: '',
        suburb: '',
        city: '',
        state: '',
        postcode: '',
        country: ''
    };
    
    // New format: properties.context object
    if (result.properties?.context) {
        const ctx = result.properties.context;
        
        components.house_number = ctx.address?.address_number || '';
        components.road = ctx.address?.street_name || ctx.street?.name || '';
        components.suburb = ctx.neighborhood?.name || '';
        components.city = ctx.place?.name || '';
        components.state = ctx.region?.name || '';
        components.postcode = ctx.postcode?.name || '';
        components.country = ctx.country?.name || '';
        
        return components;
    }
    
    // Legacy v6 format: properties object
    if (result.properties) {
        const props = result.properties;
        components.house_number = props.address_number || '';
        components.road = props.street || '';
        components.suburb = props.neighborhood || '';
        components.city = props.place || '';
        components.state = props.region || '';
        components.postcode = props.postcode || '';
        components.country = props.country || '';
        
        return components;
    }
    
    // Legacy v5 format: context array
    if (result.context && Array.isArray(result.context)) {
        result.context.forEach(item => {
            const id = item.id || '';
            if (id.startsWith('postcode.')) components.postcode = item.text;
            else if (id.startsWith('place.')) components.city = item.text;
            else if (id.startsWith('region.')) components.state = item.text;
            else if (id.startsWith('country.')) components.country = item.text;
            else if (id.startsWith('neighborhood.')) components.suburb = item.text;
        });
        
        // Extract house number from place name (first part)
        if (result.place_name) {
            const match = result.place_name.match(/^(\d+[A-Za-z]?)\s+/);
            if (match) components.house_number = match[1];
            
            // Extract road from place name or address
            if (components.house_number) {
                const addressPart = result.place_name.replace(new RegExp(`^${components.house_number}\\s+`), '').split(',')[0];
                components.road = addressPart.trim();
            }
        }
    }
    
    return components;
}

function calculateNominatimConfidence(data) {
    // Basic confidence calculation for Nominatim results
    let confidence = 100;
    
    if (!data.address?.house_number) confidence -= 20;
    if (!data.address?.road) confidence -= 30;
    if (data.type === 'administrative') confidence -= 25;
    if (data.class === 'place' && data.type === 'suburb') confidence -= 20;
    
    return Math.max(confidence, 10);
}

function calculatePhotonConfidence(feature) {
    // Basic confidence for Photon results
    let confidence = 100;
    const props = feature.properties;
    
    if (!props.housenumber) confidence -= 20;
    if (!props.street) confidence -= 30;
    if (props.osm_type === 'relation') confidence -= 15;
    
    return Math.max(confidence, 10);
}
async function reverseGeocode(lat, lng) {
    // Create cache key - use full precision
    const cacheKey = `${lat},${lng}`;
    
    // Check cache first
    const cached = geocodeCache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
        logger.debug(`Geocoding cache hit for ${cacheKey}`);
        return cached.data;
    }
    
    try {
        // Add delay to respect Nominatim rate limits (max 1 request per second)
        await new Promise(resolve => setTimeout(resolve, 1000));
        
        logger.info(`Reverse geocoding: ${lat}, ${lng}`);
        
        const response = await axios.get('https://nominatim.openstreetmap.org/reverse', {
            params: {
                format: 'json',
                lat: lat,
                lon: lng,
                zoom: 18,
                addressdetails: 1,
                'accept-language': 'en'
            },
            headers: {
                'User-Agent': 'NocoDB Map Viewer 1.0 (https://github.com/yourusername/nocodb-map-viewer)'
            },
            timeout: 10000
        });
        
        if (response.data.error) {
            throw new Error(response.data.error);
        }
        
        // Process the response
        const result = processGeocodeResponse(response.data);
        
        // Cache the result
        geocodeCache.set(cacheKey, {
            data: result,
            timestamp: Date.now()
        });
        
        return result;
        
    } catch (error) {
        logger.error('Reverse geocoding error:', error.message);
        
        if (error.response?.status === 429) {
            throw new Error('Rate limit exceeded. Please try again later.');
        } else if (error.code === 'ECONNABORTED') {
            throw new Error('Geocoding service timeout');
        } else {
            throw new Error('Geocoding service unavailable');
        }
    }
}

/**
 * Forward geocode address to get coordinates (for search - returns multiple results)
 * @param {string} address - Address to search
 * @param {number} limit - Maximum number of results to return
 * @returns {Promise<Array>} Array of geocoding results
 */
async function forwardGeocodeSearch(address, limit = 5) {
    // Create cache key
    const cacheKey = `search:${address.toLowerCase()}:${limit}`;
    
    // Check cache first
    const cached = geocodeCache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
        logger.debug(`Geocoding search cache hit for ${cacheKey}`);
        return cached.data;
    }
    
    try {
        // Add delay to respect rate limits
        await new Promise(resolve => setTimeout(resolve, 1000));
        
        logger.info(`Forward geocoding search: ${address}`);
        
        const response = await axios.get('https://nominatim.openstreetmap.org/search', {
            params: {
                format: 'json',
                q: address,
                limit: limit,
                addressdetails: 1,
                'accept-language': 'en',
                countrycodes: 'ca' // Limit to Canada for this application
            },
            headers: {
                'User-Agent': 'NocoDB Map Viewer 1.0 (contact@example.com)'
            },
            timeout: 15000
        });
        
        if (!response.data || response.data.length === 0) {
            return [];
        }
        
        // Process all results
        const results = response.data.map(item => processGeocodeResponse(item));
        
        // Cache the results
        geocodeCache.set(cacheKey, {
            data: results,
            timestamp: Date.now()
        });
        
        return results;
        
    } catch (error) {
        logger.error('Forward geocoding search error:', error.message);
        
        if (error.response?.status === 429) {
            throw new Error('Rate limit exceeded. Please try again later.');
        } else if (error.response?.status === 403) {
            throw new Error('Access denied by geocoding service');
        } else if (error.response?.status === 500) {
            throw new Error('Geocoding service internal error');
        } else if (error.code === 'ECONNABORTED') {
            throw new Error('Geocoding request timeout');
        } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
            throw new Error('Cannot connect to geocoding service');
        } else {
            throw new Error(`Geocoding search failed: ${error.message}`);
        }
    }
}

/**
 * Validate geocoding result and calculate confidence score
 * @param {string} originalAddress - The original address searched
 * @param {Object} geocodeResult - The geocoding result
 * @returns {Object} Validation result with confidence score
 */
function validateGeocodeResult(originalAddress, geocodeResult) {
    const validation = {
        isValid: true,
        confidence: 100,
        warnings: [],
        isMalformed: false
    };
    
    if (!geocodeResult || !geocodeResult.formattedAddress) {
        validation.isValid = false;
        validation.confidence = 0;
        return validation;
    }
    
    // Extract key components from original address
    const originalLower = originalAddress.toLowerCase();
    const resultLower = geocodeResult.formattedAddress.toLowerCase();
    
    // Check for street number presence
    const streetNumberMatch = originalAddress.match(/^\d+/);
    const resultStreetNumber = geocodeResult.components.house_number;
    
    if (streetNumberMatch && !resultStreetNumber) {
        validation.warnings.push('Street number not found in result');
        validation.confidence -= 30;
        validation.isMalformed = true;
    } else if (streetNumberMatch && resultStreetNumber) {
        if (streetNumberMatch[0] !== resultStreetNumber) {
            validation.warnings.push('Street number mismatch');
            validation.confidence -= 40;
            validation.isMalformed = true;
        }
    }
    
    // Check for street name presence
    const streetNameWords = originalLower
        .replace(/^\d+\s*/, '') // Remove leading number
        .replace(/,.*$/, '') // Remove everything after first comma
        .trim()
        .split(/\s+/)
        .filter(word => !['nw', 'ne', 'sw', 'se', 'street', 'st', 'avenue', 'ave', 'road', 'rd', 'crescent', 'close'].includes(word));
    
    let matchedWords = 0;
    streetNameWords.forEach(word => {
        if (resultLower.includes(word)) {
            matchedWords++;
        }
    });
    
    const matchPercentage = streetNameWords.length > 0 ? (matchedWords / streetNameWords.length) * 100 : 0;
    
    if (matchPercentage < 50) {
        validation.warnings.push('Poor street name match');
        validation.confidence -= 30;
        validation.isMalformed = true;
    } else if (matchPercentage < 75) {
        validation.warnings.push('Partial street name match');
        validation.confidence -= 15;
    }
    
    // Check for generic/fallback results (often indicates geocoding failure)
    const genericIndicators = ['castle downs', 'clover bar', 'downtown', 'city centre'];
    const hasGenericResult = genericIndicators.some(indicator => 
        resultLower.includes(indicator) && !originalLower.includes(indicator)
    );
    
    if (hasGenericResult) {
        validation.warnings.push('Result appears to be generic area, not specific address');
        validation.confidence -= 25;
        validation.isMalformed = true;
    }
    
    // Final validation
    validation.isValid = validation.confidence >= 50;
    
    return validation;
}

/**
 * Multi-provider forward geocode with fallback support
 * @param {string} address - Address to geocode
 * @returns {Promise<Object>} Geocoding result with provider info
 */
async function forwardGeocode(address) {
    // Input validation
    if (!address || typeof address !== 'string') {
        logger.warn(`Invalid address provided for geocoding: ${address}`);
        throw new Error('Invalid address: address must be a non-empty string');
    }
    
    address = address.trim();
    if (address.length === 0) {
        throw new Error('Invalid address: address cannot be empty');
    }
    
    // Create cache key
    const cacheKey = `addr:${address.toLowerCase()}`;
    
    // Check cache first
    const cached = geocodeCache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
        logger.debug(`Geocoding cache hit for ${cacheKey}`);
        return cached.data;
    }
    
    // Try different address format variations
    const addressVariations = [
        address, // Original
        address.replace(/\s+NW\s*$/i, ' Northwest'), // Expand NW
        address.replace(/\s+Northwest\s*$/i, ' NW'), // Contract Northwest  
        address.replace(/,\s*CA\s*$/i, ', Canada'), // Expand CA to Canada
        address.replace(/\s+Street\s+/i, ' St '), // Abbreviate Street
        address.replace(/\s+St\s+/i, ' Street '), // Expand St
        address.replace(/\s+Avenue\s+/i, ' Ave '), // Abbreviate Avenue
        address.replace(/\s+Ave\s+/i, ' Avenue '), // Expand Ave
    ];
    
    // Provider functions mapping
    const providerFunctions = {
        'Mapbox': geocodeWithMapbox,
        'Nominatim': geocodeWithNominatim,
        'Photon': geocodeWithPhoton,
        'LocationIQ': geocodeWithLocationIQ,
        'ArcGIS': geocodeWithArcGIS
    };
    
    let bestResult = null;
    let bestConfidence = 0;
    const allErrors = [];
    
    // Try each provider with each address variation
    for (const provider of GEOCODING_PROVIDERS) {
        logger.info(`Trying provider: ${provider.name}`);
        
        for (let varIndex = 0; varIndex < addressVariations.length; varIndex++) {
            const addressVariation = addressVariations[varIndex];
            
            try {
                logger.info(`${provider.name} - attempt ${varIndex + 1}/${addressVariations.length}: ${addressVariation}`);
                
                const providerResult = await providerFunctions[provider.name](addressVariation, provider.options);
                
                if (!providerResult) {
                    continue; // No results from this provider/variation
                }
                
                // Convert to standard format
                const result = {
                    fullAddress: providerResult.formattedAddress,
                    formattedAddress: providerResult.formattedAddress,
                    components: providerResult.components,
                    coordinates: {
                        lat: providerResult.latitude,
                        lng: providerResult.longitude
                    },
                    latitude: providerResult.latitude,
                    longitude: providerResult.longitude,
                    provider: providerResult.provider,
                    providerConfidence: providerResult.confidence,
                    addressVariation: addressVariation,
                    variationIndex: varIndex,
                    boundingBox: providerResult.boundingBox || null,
                    raw: providerResult.raw
                };
                
                // Validate the result
                const validation = validateGeocodeResult(address, result);
                result.validation = validation;
                
                // Calculate combined confidence (provider confidence + validation confidence)
                const combinedConfidence = Math.round((providerResult.confidence + validation.confidence) / 2);
                result.combinedConfidence = combinedConfidence;
                
                logger.info(`${provider.name} result - Provider: ${providerResult.confidence}%, Validation: ${validation.confidence}%, Combined: ${combinedConfidence}%`);
                
                // If this is a very high confidence result, use it immediately
                if (combinedConfidence >= 90 && validation.confidence >= 80) {
                    logger.info(`High confidence result found with ${provider.name}, using immediately`);
                    
                    // Cache and return the result
                    geocodeCache.set(cacheKey, {
                        data: result,
                        timestamp: Date.now()
                    });
                    
                    return result;
                }
                
                // Keep track of the best result so far
                if (combinedConfidence > bestConfidence) {
                    bestResult = result;
                    bestConfidence = combinedConfidence;
                }
                
                // If we have a decent result from the first variation, don't try more variations for this provider
                if (varIndex === 0 && combinedConfidence >= 70) {
                    logger.info(`Good result from ${provider.name} with original address, skipping variations`);
                    break;
                }
                
            } catch (error) {
                logger.error(`${provider.name} error with "${addressVariation}": ${error.message}`);
                allErrors.push(`${provider.name}: ${error.message}`);
                
                // If rate limited, wait extra time
                if (error.response?.status === 429) {
                    await new Promise(resolve => setTimeout(resolve, 5000));
                }
                
                continue; // Try next variation or provider
            }
        }
        
        // If we found a good result with this provider, we can stop trying other providers
        if (bestConfidence >= 75) {
            logger.info(`Acceptable result found with ${provider.name} (${bestConfidence}%), stopping provider search`);
            break;
        }
    }
    
    // If we have any result, return the best one
    if (bestResult) {
        logger.info(`Returning best result from ${bestResult.provider} with ${bestConfidence}% confidence`);
        
        // Cache the result
        geocodeCache.set(cacheKey, {
            data: bestResult,
            timestamp: Date.now()
        });
        
        return bestResult;
    }
    
    // All providers failed
    const errorMessage = `All geocoding providers failed: ${allErrors.join('; ')}`;
    logger.error(errorMessage);
    throw new Error('Geocoding failed: No providers could locate this address');
}

/**
 * Process geocoding response into standardized format
 * @param {Object} data - Raw geocoding response
 * @returns {Object} Processed geocoding data
 */
function processGeocodeResponse(data) {
    // Extract address components
    const addressComponents = {
        house_number: data.address?.house_number || '',
        road: data.address?.road || '',
        suburb: data.address?.suburb || data.address?.neighbourhood || '',
        city: data.address?.city || data.address?.town || data.address?.village || '',
        state: data.address?.state || data.address?.province || '',
        postcode: data.address?.postcode || '',
        country: data.address?.country || ''
    };
    
    // Create formatted address string
    let formattedAddress = '';
    if (addressComponents.house_number) formattedAddress += addressComponents.house_number + ' ';
    if (addressComponents.road) formattedAddress += addressComponents.road + ', ';
    if (addressComponents.suburb) formattedAddress += addressComponents.suburb + ', ';
    if (addressComponents.city) formattedAddress += addressComponents.city + ', ';
    if (addressComponents.state) formattedAddress += addressComponents.state + ' ';
    if (addressComponents.postcode) formattedAddress += addressComponents.postcode;
    
    // Clean up formatting
    formattedAddress = formattedAddress.trim().replace(/,$/, '');
    
    return {
        fullAddress: data.display_name || '',
        formattedAddress: formattedAddress,
        components: addressComponents,
        coordinates: {
            lat: parseFloat(data.lat),
            lng: parseFloat(data.lon)
        },
        // Backward compatibility
        latitude: parseFloat(data.lat),
        longitude: parseFloat(data.lon),
        boundingBox: data.boundingbox || null,
        placeId: data.place_id || null,
        osmType: data.osm_type || null,
        osmId: data.osm_id || null
    };
}

/**
 * Get cache statistics
 * @returns {Object} Cache statistics
 */
function getCacheStats() {
    return {
        size: geocodeCache.size,
        maxSize: 1000, // Could be made configurable
        ttl: CACHE_TTL
    };
}

/**
 * Clear the geocoding cache
 */
function clearCache() {
    geocodeCache.clear();
    logger.info('Geocoding cache cleared');
}

module.exports = {
    reverseGeocode,
    forwardGeocode,
    forwardGeocodeSearch,
    getCacheStats,
    clearCache
};
