"""
Vendor geocoding utility.

Strategy (in order of accuracy):
  1. Google Places Text Search  – search by business name + city
  2. Google Geocoding API        – search by full address
  3. OpenStreetMap Nominatim     – free fallback

If no GOOGLE_MAPS_API_KEY is configured, steps 1 & 2 are skipped.
"""

import re
import time
import urllib.parse
import urllib.request
import json

from django.conf import settings

# ---- URLs ----
_GOOGLE_PLACES_URL = 'https://maps.googleapis.com/maps/api/place/textsearch/json'
_GOOGLE_GEOCODE_URL = 'https://maps.googleapis.com/maps/api/geocode/json'
_NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
_USER_AGENT = (
    f"{getattr(settings, 'SITE_SLUG', 'melhorprecodf')}/1.0 "
    f"({getattr(settings, 'SITE_DOMAIN', 'melhorprecodf.com.br')})"
)

# Brasília centre – used as location bias for Google Places
_BRASILIA_LAT = -15.7975
_BRASILIA_LNG = -47.8919

# Nominatim rate-limiting
_LAST_NOMINATIM_TIME = 0.0


# ---------------------------------------------------------------------------
# Text helpers
# ---------------------------------------------------------------------------

def _clean_field(text):
    """Collapse newlines, tabs, and multiple spaces into a single space."""
    if not text:
        return ''
    return re.sub(r'\s+', ' ', text).strip()


def _clean_municipio(text):
    """
    Clean the municipality field which often comes from SEFAZ as
    '5300108 -\\n                    BRASILIA'
    Returns just the city name.
    """
    if not text:
        return ''
    clean = _clean_field(text)
    clean = re.sub(r'^\d{5,7}\s*-\s*', '', clean)
    return clean.strip()


def _clean_endereco(text):
    """
    Clean the address field. Removes internal info that won't help
    geocoding (BLOCO, LOJA, SALA, ANDAR, etc.) and collapses whitespace.
    """
    if not text:
        return ''
    clean = _clean_field(text)
    clean = re.sub(r'\bS/?N\.?\b', '', clean, flags=re.IGNORECASE)
    clean = re.sub(
        r'\b(BLOCO|BL|LOJA|LJ|SALA|SL|ANDAR|PISO|PAVIMENTO|GALPAO|BOX|MODULO|QUADRA|QD)\b[\s\w]*$',
        '', clean, flags=re.IGNORECASE,
    )
    clean = re.sub(r'[\s,]+$', '', clean)
    return clean.strip()


def sanitize_address(endereco, bairro, cep, municipio=None, uf=None):
    """
    Build a clean single-line address string from vendor fields.

    Example output:
        'AV CASTANHEIRAS 3070, AGUAS CLARAS, 71900-100, BRASILIA, DF, Brasil'
    """
    parts = []

    clean_end = _clean_endereco(endereco)
    if clean_end:
        parts.append(clean_end)

    clean_bairro = _clean_field(bairro)
    if clean_bairro:
        parts.append(clean_bairro)

    if cep:
        digits = re.sub(r'[^\d]', '', _clean_field(cep))
        if len(digits) == 8:
            parts.append(f'{digits[:5]}-{digits[5:]}')

    city = _clean_municipio(municipio) if municipio else getattr(settings, 'DEFAULT_CITY', 'Brasília')
    if not city:
        city = getattr(settings, 'DEFAULT_CITY', 'Brasília')
    state = _clean_field(uf) if uf else getattr(settings, 'DEFAULT_UF', 'DF')
    if not state:
        state = getattr(settings, 'DEFAULT_UF', 'DF')
    parts.append(f'{city}, {state}, Brasil')

    return ', '.join(parts)


# ---------------------------------------------------------------------------
# Strategy 1 – Google Places Text Search (by business name)
# ---------------------------------------------------------------------------

def _google_places_search(query, api_key, lat_bias=None, lng_bias=None):
    """
    Search for a place by name using Google Places Text Search.
    Returns (latitude, longitude) or None.
    """
    params = {
        'query': query,
        'key': api_key,
    }
    if lat_bias is not None and lng_bias is not None:
        params['location'] = f'{lat_bias},{lng_bias}'
        params['radius'] = 50000  # 50 km bias

    url = f'{_GOOGLE_PLACES_URL}?{urllib.parse.urlencode(params)}'
    req = urllib.request.Request(url)

    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode('utf-8'))
            if data.get('status') == 'OK' and data.get('results'):
                loc = data['results'][0]['geometry']['location']
                return (loc['lat'], loc['lng'])
    except Exception as e:
        print(f'[Geocoding] Google Places error for "{query}": {e}')

    return None


# ---------------------------------------------------------------------------
# Strategy 2 – Google Geocoding API (by address)
# ---------------------------------------------------------------------------

def _google_geocode(address, api_key):
    """
    Geocode an address using Google Geocoding API.
    Returns (latitude, longitude) or None.
    """
    params = {
        'address': address,
        'key': api_key,
        'region': 'br',
    }
    url = f'{_GOOGLE_GEOCODE_URL}?{urllib.parse.urlencode(params)}'
    req = urllib.request.Request(url)

    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode('utf-8'))
            if data.get('status') == 'OK' and data.get('results'):
                loc = data['results'][0]['geometry']['location']
                return (loc['lat'], loc['lng'])
    except Exception as e:
        print(f'[Geocoding] Google Geocode error for "{address}": {e}')

    return None


# ---------------------------------------------------------------------------
# Strategy 3 – Nominatim (free fallback)
# ---------------------------------------------------------------------------

def _nominatim_geocode(address):
    """
    Geocode an address using OpenStreetMap Nominatim.
    Respects the 1 request/second rate limit.
    Returns (latitude, longitude) or None.
    """
    global _LAST_NOMINATIM_TIME

    now = time.time()
    elapsed = now - _LAST_NOMINATIM_TIME
    if elapsed < 1.0:
        time.sleep(1.0 - elapsed)

    params = urllib.parse.urlencode({
        'q': address,
        'format': 'json',
        'limit': 1,
    })
    url = f'{_NOMINATIM_URL}?{params}'
    req = urllib.request.Request(url, headers={'User-Agent': _USER_AGENT})

    try:
        _LAST_NOMINATIM_TIME = time.time()
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.loads(resp.read().decode('utf-8'))
            if data and len(data) > 0:
                return (float(data[0]['lat']), float(data[0]['lon']))
    except Exception as e:
        print(f'[Geocoding] Nominatim error for "{address}": {e}')

    return None


# ---------------------------------------------------------------------------
# Public API (kept backwards-compatible)
# ---------------------------------------------------------------------------

def geocode_address(address_str):
    """
    Geocode an address string (Nominatim only — legacy helper).
    Returns (latitude, longitude) or None.
    """
    return _nominatim_geocode(address_str)


def geocode_vendor(vendor):
    """
    Geocode a Vendor instance using a multi-strategy approach:

      1. Google Places Text Search  – search by business name + city/neighbourhood
      2. Google Geocoding API        – search by structured address
      3. Nominatim (OSM)             – free fallback

    Also sanitizes the raw address fields in the database.
    Returns True if geocoded successfully, False otherwise.
    """
    # ---- Sanitize raw fields ----
    update_fields = []
    clean_endereco = _clean_field(vendor.endereco_text)
    if clean_endereco != (vendor.endereco_text or ''):
        vendor.endereco_text = clean_endereco
        update_fields.append('endereco_text')

    clean_bairro = _clean_field(vendor.bairro_text)
    if clean_bairro != (vendor.bairro_text or ''):
        vendor.bairro_text = clean_bairro
        update_fields.append('bairro_text')

    clean_cep = _clean_field(vendor.cep_text)
    if clean_cep != (vendor.cep_text or ''):
        vendor.cep_text = clean_cep
        update_fields.append('cep_text')

    clean_municipio = _clean_municipio(vendor.municipio_text)
    if clean_municipio != (vendor.municipio_text or ''):
        vendor.municipio_text = clean_municipio
        update_fields.append('municipio_text')

    clean_uf = _clean_field(vendor.uf_text)
    if clean_uf != (vendor.uf_text or ''):
        vendor.uf_text = clean_uf
        update_fields.append('uf_text')

    if update_fields:
        vendor.save(update_fields=update_fields)
        print(f'[Geocoding] Vendor {vendor.id}: sanitized fields {update_fields}')

    # ---- Prepare data ----
    api_key = getattr(settings, 'GOOGLE_MAPS_API_KEY', None)

    business_name = (
        _clean_field(vendor.nomefantasia_text)
        or _clean_field(vendor.nomerazaosocial_text)
        or ''
    )
    bairro = _clean_field(vendor.bairro_text) or ''
    city = _clean_municipio(vendor.municipio_text) or 'Brasília'
    state = _clean_field(vendor.uf_text) or 'DF'

    full_address = sanitize_address(
        vendor.endereco_text,
        vendor.bairro_text,
        vendor.cep_text,
        vendor.municipio_text,
        vendor.uf_text,
    )

    result = None

    # ---- Strategy 1: Google Places – search by name + street address ----
    if api_key and business_name:
        # Include the street address so chains with multiple branches
        # (e.g. "Atacadão") resolve to the correct location.
        # Query: "COSTA ATACADAO, AV CASTANHEIRAS 3070, AGUAS CLARAS, BRASILIA, DF"
        street = _clean_endereco(vendor.endereco_text) or ''
        query_parts = [business_name]
        if street:
            query_parts.append(street)
        if bairro:
            query_parts.append(bairro)
        query_parts.append(f'{city}, {state}')
        places_query = ', '.join(query_parts)

        print(f'[Geocoding] Vendor {vendor.id}: Places search "{places_query}"')
        result = _google_places_search(
            places_query, api_key,
            lat_bias=getattr(settings, 'GEOCODE_LAT', _BRASILIA_LAT),
            lng_bias=getattr(settings, 'GEOCODE_LNG', _BRASILIA_LNG),
        )
        if result:
            print(f'[Geocoding] Vendor {vendor.id}: Places found {result}')

    # ---- Strategy 2: Google Geocoding – search by address ----
    if not result and api_key:
        print(f'[Geocoding] Vendor {vendor.id}: Google Geocode "{full_address}"')
        result = _google_geocode(full_address, api_key)
        if result:
            print(f'[Geocoding] Vendor {vendor.id}: Google Geocode found {result}')

    # ---- Strategy 3: Nominatim – free fallback ----
    if not result:
        print(f'[Geocoding] Vendor {vendor.id}: Nominatim "{full_address}"')
        result = _nominatim_geocode(full_address)
        if result:
            print(f'[Geocoding] Vendor {vendor.id}: Nominatim found {result}')

    # ---- Save result ----
    if result:
        vendor.latitude, vendor.longitude = result
        vendor.save(update_fields=['latitude', 'longitude'])
        print(f'[Geocoding] Vendor {vendor.id} geocoded: {result}')
        return True
    else:
        print(f'[Geocoding] Could not geocode vendor {vendor.id}: "{full_address}"')
        return False
