package render

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"leadb2b.org/internal/api"
)

// MapCoords coordenadas para o mapa da ficha da empresa.
type MapCoords struct {
	Lat       float64
	Lon       float64
	OK        bool
	Approximate bool // true = centro da cidade ou CEP aproximado
}

type nominatimResult struct {
	Lat        string  `json:"lat"`
	Lon        string  `json:"lon"`
	Type       string  `json:"type"`
	Class      string  `json:"class"`
	Importance float64 `json:"importance"`
}

var geocodeClient = &http.Client{Timeout: 12 * time.Second}

var impreciseTypes = map[string]bool{
	"city": true, "town": true, "village": true, "municipality": true,
	"state": true, "country": true, "administrative": true, "region": true,
}

// GeocodeEmpresa busca coordenadas priorizando endereço de rua (não só a cidade).
func GeocodeEmpresa(d *api.EstabelecimentoDetailOut) MapCoords {
	if d == nil {
		return MapCoords{}
	}
	hasStreet := empresaHasStreet(d)

	// 1) Busca estruturada (melhor para logradouro + CEP)
	if lat, lon, ok := geocodeStructured(d); ok {
		return MapCoords{Lat: lat, Lon: lon, OK: true}
	}
	time.Sleep(1100 * time.Millisecond)

	// 2) Consultas em texto — só aceita resultado de rua/edifício se temos logradouro
	for _, q := range empresaGeocodeQueries(d) {
		if lat, lon, approx, ok := geocodeFreeText(q, hasStreet); ok {
			return MapCoords{Lat: lat, Lon: lon, OK: true, Approximate: approx}
		}
		time.Sleep(1100 * time.Millisecond)
	}

	// 3) Último recurso: cidade (marcado como aproximado)
	if !hasStreet {
		return MapCoords{}
	}
	cidade := strings.TrimSpace(d.Endereco.Municipio.Descricao)
	uf := strings.TrimSpace(d.Endereco.UF)
	if cidade != "" && uf != "" {
		if lat, lon, _, ok := geocodeFreeText(fmt.Sprintf("%s, %s, Brasil", cidade, uf), false); ok {
			return MapCoords{Lat: lat, Lon: lon, OK: true, Approximate: true}
		}
	}
	return MapCoords{}
}

func empresaHasStreet(d *api.EstabelecimentoDetailOut) bool {
	log := strings.TrimSpace(strings.TrimSpace(d.Endereco.TipoLogradouro + " " + d.Endereco.Logradouro))
	return log != "" || strings.TrimSpace(d.Endereco.CEP) != ""
}

func geocodeStructured(d *api.EstabelecimentoDetailOut) (float64, float64, bool) {
	street := buildStreetLine(d)
	city := strings.TrimSpace(d.Endereco.Municipio.Descricao)
	uf := strings.TrimSpace(d.Endereco.UF)
	if street == "" || city == "" || uf == "" {
		return 0, 0, false
	}

	q := url.Values{}
	q.Set("format", "json")
	q.Set("limit", "3")
	q.Set("addressdetails", "1")
	q.Set("countrycodes", "br")
	q.Set("street", street)
	q.Set("city", city)
	q.Set("state", uf)
	q.Set("country", "Brasil")
	if cep := formatCEP(d.Endereco.CEP); cep != "" {
		q.Set("postalcode", cep)
	}

	results, err := nominatimSearch(q)
	if err != nil || len(results) == 0 {
		return 0, 0, false
	}
	for _, r := range results {
		if isPreciseResult(r) {
			return parseCoords(r)
		}
	}
	// Aceita o primeiro se não for tipo "city"
	if len(results) > 0 && !impreciseTypes[strings.ToLower(results[0].Type)] {
		return parseCoords(results[0])
	}
	return 0, 0, false
}

func buildStreetLine(d *api.EstabelecimentoDetailOut) string {
	logradouro := strings.TrimSpace(strings.TrimSpace(d.Endereco.TipoLogradouro + " " + d.Endereco.Logradouro))
	if logradouro == "" {
		return ""
	}
	num := strings.TrimSpace(d.Endereco.Numero)
	if num == "" || strings.EqualFold(num, "sn") || strings.EqualFold(num, "s/n") {
		return logradouro
	}
	return logradouro + ", " + num
}

func empresaGeocodeQueries(d *api.EstabelecimentoDetailOut) []string {
	seen := make(map[string]struct{})
	var out []string
	add := func(s string) {
		s = strings.TrimSpace(s)
		if s == "" {
			return
		}
		if _, dup := seen[s]; dup {
			return
		}
		seen[s] = struct{}{}
		out = append(out, s)
	}

	cep := formatCEP(d.Endereco.CEP)
	cidade := strings.TrimSpace(d.Endereco.Municipio.Descricao)
	uf := strings.TrimSpace(d.Endereco.UF)
	bairro := strings.TrimSpace(d.Endereco.Bairro)
	logradouro := buildStreetLine(d)

	// CEP + rua costuma ser mais preciso que endereco_completo longo da RF
	if cep != "" && logradouro != "" && cidade != "" {
		add(fmt.Sprintf("%s, %s, %s, %s, Brasil", logradouro, bairro, cidade, uf))
		add(fmt.Sprintf("%s, %s, Brasil", cep, cidade))
	}
	if cep != "" {
		add(cep + ", Brasil")
	}

	// Endereço completo sem vírgulas duplicadas
	if full := normalizeAddressForGeocode(d); full != "" {
		add(full)
	}

	if logradouro != "" && bairro != "" && cidade != "" {
		add(fmt.Sprintf("%s, %s, %s, %s, Brasil", logradouro, bairro, cidade, uf))
	}
	if logradouro != "" && cidade != "" {
		add(fmt.Sprintf("%s, %s, %s, Brasil", logradouro, cidade, uf))
	}
	// Não incluir só cidade aqui — tratado no final como aproximado
	return out
}

func normalizeAddressForGeocode(d *api.EstabelecimentoDetailOut) string {
	s := strings.TrimSpace(d.Endereco.EnderecoCompleto)
	if s == "" {
		return ""
	}
	s = strings.ReplaceAll(s, ",,", ",")
	s = strings.ReplaceAll(s, " ,", ", ")
	for strings.Contains(s, ",,") {
		s = strings.ReplaceAll(s, ",,", ",")
	}
	return s + ", Brasil"
}

func formatCEP(cep string) string {
	d := digitsOnly(cep)
	if len(d) == 8 {
		return d[:5] + "-" + d[5:]
	}
	return strings.TrimSpace(cep)
}

func geocodeFreeText(address string, requirePrecise bool) (lat, lon float64, approximate bool, ok bool) {
	q := url.Values{}
	q.Set("format", "json")
	q.Set("limit", "5")
	q.Set("addressdetails", "1")
	q.Set("countrycodes", "br")
	q.Set("q", address)

	results, err := nominatimSearch(q)
	if err != nil || len(results) == 0 {
		return 0, 0, false, false
	}

	if requirePrecise {
		for _, r := range results {
			if isPreciseResult(r) {
				lat, lon, ok := parseCoords(r)
				return lat, lon, false, ok
			}
		}
		return 0, 0, false, false
	}

	lat, lon, ok = parseCoords(results[0])
	return lat, lon, true, ok
}

func isPreciseResult(r nominatimResult) bool {
	t := strings.ToLower(strings.TrimSpace(r.Type))
	if impreciseTypes[t] {
		return false
	}
	// house, building, residential, office, road, suburb com número costumam ser ok
	precise := map[string]bool{
		"house": true, "building": true, "residential": true, "commercial": true,
		"office": true, "retail": true, "industrial": true, "yes": true,
		"road": true, "pedestrian": true, "place": true,
	}
	if precise[t] {
		return true
	}
	// suburb/bairro ainda melhor que cidade inteira
	if t == "suburb" || t == "neighbourhood" || t == "quarter" {
		return true
	}
	return r.Importance > 0.45 && !impreciseTypes[t]
}

func nominatimSearch(q url.Values) ([]nominatimResult, error) {
	req, err := http.NewRequest(http.MethodGet, "https://nominatim.openstreetmap.org/search?"+q.Encode(), nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "LeadB2B/1.0 (+https://leadb2b.org; geocode empresa)")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Accept-Language", "pt-BR")

	resp, err := geocodeClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("nominatim status %d", resp.StatusCode)
	}
	var results []nominatimResult
	if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
		return nil, err
	}
	return results, nil
}

func parseCoords(r nominatimResult) (float64, float64, bool) {
	lat, err1 := strconv.ParseFloat(r.Lat, 64)
	lon, err2 := strconv.ParseFloat(r.Lon, 64)
	if err1 != nil || err2 != nil {
		return 0, 0, false
	}
	return lat, lon, true
}
