// Package geo abstracts external geographic providers (routing/geocoding) so
// the business domain never depends on one vendor. Providers are OFF unless
// explicitly configured; every endpoint degrades to an "available:false"
// payload instead of failing the caller (map audit 2026-08-25).
package geo

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// Point is a WGS84 coordinate.
type Point struct {
	Lat float64 `json:"lat"`
	Lng float64 `json:"lng"`
}

// RouteResult is a driving route between two or more points.
type RouteResult struct {
	Available  bool    `json:"available"`
	DistanceM  float64 `json:"distance_m"`
	DurationS  float64 `json:"duration_s"`
	Source     string  `json:"source"`
	GeometryLnLg [][2]float64 `json:"geometry"` // [lng, lat] pairs (GeoJSON order)
}

// Router produces routes. Implementations: OSRM, GraphHopper, Valhalla, SaaS.
type Router interface {
	Route(ctx context.Context, from Point, to Point, via []Point) (*RouteResult, error)
}

// Geocoder resolves addresses ↔ coordinates. The public Nominatim service has
// strict usage policies: it must be self-hosted or replaced by a commercial
// provider before any high-volume use. Default configuration keeps it OFF.
type Geocoder interface {
	Forward(ctx context.Context, query string) ([]Hit, error)
	Reverse(ctx context.Context, p Point) (*Address, error)
	Name() string
}

// Hit is one forward-geocode match.
type Hit struct {
	Label    string `json:"label"`
	Lat      float64 `json:"lat"`
	Lng      float64 `json:"lng"`
	Importance float64 `json:"importance,omitempty"`
}

// Address is a reverse-geocoded human-readable place.
type Address struct {
	Label string `json:"label"`
}

// --- OSRM router ---------------------------------------------------------------

// OSRMRouter talks to an OSRM server (self-hosted or a provider exposing the
// OSRM API, e.g. routed.academic/Mapbox-compatible endpoints).
type OSRMRouter struct {
	BaseURL string
	Client  *http.Client
}

// NewOSRM builds an OSRM router for the given base URL.
func NewOSRM(baseURL string, client *http.Client) *OSRMRouter {
	return &OSRMRouter{BaseURL: strings.TrimRight(baseURL, "/"), Client: client}
}

func (r *OSRMRouter) client() *http.Client {
	if r.Client != nil {
		return r.Client
	}
	return &http.Client{Timeout: 10 * time.Second}
}

// Route queries the OSRM route service. Geometry is returned as [lng,lat].
func (r *OSRMRouter) Route(ctx context.Context, from Point, to Point, via []Point) (*RouteResult, error) {
	pts := make([]string, 0, len(via)+2)
	pts = append(pts, fmt.Sprintf("%f,%f", from.Lng, from.Lat))
	for _, v := range via {
		pts = append(pts, fmt.Sprintf("%f,%f", v.Lng, v.Lat))
	}
	pts = append(pts, fmt.Sprintf("%f,%f", to.Lng, to.Lat))
	u := fmt.Sprintf("%s/route/v1/driving/%s?overview=simplified&geometries=geojson",
		r.BaseURL, strings.Join(pts, ";"))
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "keluargamart-erp")
	resp, err := r.client().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var body struct {
		Code   string `json:"code"`
		Routes []struct {
			Distance float64 `json:"distance"`
			Duration float64 `json:"duration"`
			Geometry struct {
				Coordinates [][2]float64 `json:"coordinates"`
			} `json:"geometry"`
		} `json:"routes"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		return nil, err
	}
	if body.Code != "Ok" || len(body.Routes) == 0 {
		return nil, fmt.Errorf("osrm code=%s", body.Code)
	}
	rt := body.Routes[0]
	return &RouteResult{
		Available:    true,
		DistanceM:    rt.Distance,
		DurationS:    rt.Duration,
		Source:       "osrm",
		GeometryLnLg: rt.Geometry.Coordinates,
	}, nil
}

// --- Nominatim geocoder (opt-in only) ------------------------------------------

// NominatimGeocoder targets a Nominatim instance. Production deployments must
// point BaseURL at a SELF-HOSTED instance or a provider that permits app use;
// the public endpoint forbids autocomplete/high-volume backends.
type NominatimGeocoder struct {
	BaseURL    string
	UserAgent  string
	Client     *http.Client
}

// NewNominatim builds a Nominatim geocoder.
func NewNominatim(baseURL, userAgent string, client *http.Client) *NominatimGeocoder {
	return &NominatimGeocoder{
		BaseURL:   strings.TrimRight(baseURL, "/"),
		UserAgent: userAgent,
		Client:    client,
	}
}

func (g *NominatimGeocoder) Name() string { return "nominatim" }

func (g *NominatimGeocoder) client() *http.Client {
	if g.Client != nil {
		return g.Client
	}
	return &http.Client{Timeout: 8 * time.Second}
}

func (g *NominatimGeocoder) ua() string {
	if g.UserAgent == "" {
		return "keluargamart-erp"
	}
	return g.UserAgent
}

// Forward resolves an address to candidate coordinates.
func (g *NominatimGeocoder) Forward(ctx context.Context, query string) ([]Hit, error) {
	u := fmt.Sprintf("%s/search?q=%s&format=jsonv2&limit=5&addressdetails=0",
		g.BaseURL, url.QueryEscape(query))
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", g.ua())
	var raw []struct {
		DisplayName string  `json:"display_name"`
		Lat         string  `json:"lat"`
		Lon         string  `json:"lon"`
		Importance  float64 `json:"importance"`
	}
	if err := doJSON(g.client(), req, &raw); err != nil {
		return nil, err
	}
	hits := make([]Hit, 0, len(raw))
	for _, h := range raw {
		var la, lo float64
		fmt.Sscanf(h.Lat, "%f", &la)
		fmt.Sscanf(h.Lon, "%f", &lo)
		hits = append(hits, Hit{Label: h.DisplayName, Lat: la, Lng: lo, Importance: h.Importance})
	}
	return hits, nil
}

// Reverse resolves coordinates to an address.
func (g *NominatimGeocoder) Reverse(ctx context.Context, p Point) (*Address, error) {
	u := fmt.Sprintf("%s/reverse?lat=%f&lon=%f&format=jsonv2", g.BaseURL, p.Lat, p.Lng)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", g.ua())
	var raw struct {
		DisplayName string `json:"display_name"`
	}
	if err := doJSON(g.client(), req, &raw); err != nil {
		return nil, err
	}
	return &Address{Label: raw.DisplayName}, nil
}

func doJSON(c *http.Client, req *http.Request, out any) error {
	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return json.NewDecoder(resp.Body).Decode(out)
}

var _ = json.Marshal
