package distribution

// RBAC/map audit 2026-08-25 — shipment location services:
//   * LatestShipmentLocation: last known GPS fix, Redis-cached (60s) with
//     PostgreSQL fallback. Redis is never the historical source of truth.
//   * ActiveLocations / MapFeed: lightweight feeds for the ERP tracking map.
//   * canPostTrackingEvent: the dispatched driver may post GPS fixes for
//     their own shipment without dispatcher permissions; everyone else needs
//     distribution.tracking.record.

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/TaruLab/keluargamart-backend/pkg/cache"
)

const locCachePrefix = "loc:shipment:"
const locCacheTTL = 60 * time.Second

// WithLocationCache wires an optional read-through cache for latest fixes.
func (s *Service) WithLocationCache(c cache.Cache) *Service {
	s.locCache = c
	return s
}

// ActiveLocation is one active shipment's latest known position (nullable:
// a pending shipment may have no GPS fix yet).
type ActiveLocation struct {
	ShipmentID     int64      `json:"shipment_id"`
	ShipmentNumber string     `json:"shipment_number"`
	DONumber       string     `json:"do_number"`
	Status         string     `json:"status"`
	ShipDate       string     `json:"ship_date"`
	Latitude       *float64   `json:"latitude"`
	Longitude      *float64   `json:"longitude"`
	LastFixAt      *time.Time `json:"last_fix_at"`
}

// MapPlace is a mappable facility point. Coordinates are nullable: callers
// must skip null points instead of inventing positions.
type MapPlace struct {
	ID        int64    `json:"id"`
	Code      string   `json:"code"`
	Name      string   `json:"name"`
	Address   string   `json:"address,omitempty"`
	City      string   `json:"city,omitempty"`
	IsActive  bool     `json:"is_active"`
	Latitude  *float64 `json:"latitude"`
	Longitude *float64 `json:"longitude"`
	// CoordinatePrecision says where the position came from, so a client can
	// tell a surveyed pin from a street-level guess:
	//
	//	"exact"  surveyed, or dropped by a human on the map
	//	"street" the middle of a named street; city/block level
	//	"city"   a city or district centroid only
	//
	// NULL means provenance is unknown, which is NOT the same as accurate. It
	// has to stay distinguishable from "exact" rather than being defaulted to
	// it: every warehouse and store coordinate in the system today is
	// street-level, and a UI that renders those as ordinary pins is the failure
	// this field exists to prevent. See migrations 000119 and 000120.
	CoordinatePrecision *string `json:"coordinate_precision"`
}

// FleetPosition is a fleet unit's latest known fix (nullable coords when the
// assigned shipment has no GPS yet).
type FleetPosition struct {
	FleetID     int64      `json:"fleet_id"`
	PlateNumber string     `json:"plate_number"`
	VehicleName string     `json:"vehicle_name"`
	Priority    string     `json:"priority"`
	Latitude    *float64   `json:"latitude"`
	Longitude   *float64   `json:"longitude"`
	LastFixAt   *time.Time `json:"last_fix_at"`
}

// MapFeed is one response powering the ERP tracking map workspace.
type MapFeed struct {
	Warehouses []MapPlace       `json:"warehouses"`
	Stores     []MapPlace       `json:"stores"`
	Fleets     []FleetPosition  `json:"fleets"`
	Shipments  []ActiveLocation `json:"shipments"`
}

func locCacheKey(id int64) string { return fmt.Sprintf("%s%d", locCachePrefix, id) }

func (s *Service) LatestShipmentLocation(ctx context.Context, shipmentID int64) (*TrackingEvent, error) {
	if _, err := s.store.ShipmentByID(ctx, shipmentID); err != nil {
		return nil, err
	}
	// Customer isolation: buyers may only read their own shipments.
	if u := auth.UserFrom(ctx); u != nil && u.Role == auth.RoleBuyer {
		custID, err := s.store.CustomerByUserID(ctx, u.ID)
		if err != nil {
			return nil, ErrNotFound
		}
		owner, err := s.store.ShipmentCustomerID(ctx, shipmentID)
		if err != nil || owner != custID {
			return nil, ErrNotFound
		}
	}
	if s.locCache != nil {
		if raw, ok, _ := s.locCache.Get(ctx, locCacheKey(shipmentID)); ok {
			var ev TrackingEvent
			if err := json.Unmarshal([]byte(raw), &ev); err == nil {
				return &ev, nil
			}
		}
	}
	row, err := s.store.ShipmentLatestEvent(ctx, shipmentID)
	if err != nil {
		return nil, err
	}
	ev := latestEventFromRow(row)
	if s.locCache != nil {
		if b, jerr := json.Marshal(ev); jerr == nil {
			_ = s.locCache.Set(ctx, locCacheKey(shipmentID), string(b), locCacheTTL)
		}
	}
	return &ev, nil
}

func firstErr(errs ...error) error {
	for _, e := range errs {
		if e != nil {
			return e
		}
	}
	return nil
}

func activeFromRow(r db.ActiveShipmentLocationsRow) ActiveLocation {
	item := ActiveLocation{
		ShipmentID:     r.ShipmentID,
		ShipmentNumber: r.ShipmentNumber,
		DONumber:       r.DoNumber,
		Status:         r.Status,
		ShipDate:       r.ShipDate.Time.Format("2006-01-02"),
	}
	if r.Latitude.Valid && r.Longitude.Valid {
		lat := *nullDecimalToFloatPtr(r.Latitude)
		lng := *nullDecimalToFloatPtr(r.Longitude)
		item.Latitude = &lat
		item.Longitude = &lng
	}
	if r.LastFixAt.Valid {
		t := r.LastFixAt.Time
		item.LastFixAt = &t
	}
	return item
}

func placeFromWarehouseRow(w db.ListWarehouseLocationsRow) MapPlace {
	mp := MapPlace{
		ID: w.ID, Code: w.Code, Name: w.Name,
		Address: w.Address.String, City: w.City.String, IsActive: w.IsActive,
		CoordinatePrecision: textToStrPtr(w.CoordinatePrecision),
	}
	if w.Latitude.Valid && w.Longitude.Valid {
		lat := *nullDecimalToFloatPtr(w.Latitude)
		lng := *nullDecimalToFloatPtr(w.Longitude)
		mp.Latitude = &lat
		mp.Longitude = &lng
	}
	return mp
}

func placeFromStoreRow(st db.ListStoreLocationsRow) MapPlace {
	mp := MapPlace{
		ID: st.ID, Code: st.Code, Name: st.Name, IsActive: st.IsActive,
		CoordinatePrecision: textToStrPtr(st.CoordinatePrecision),
	}
	if st.Latitude.Valid && st.Longitude.Valid {
		lat := *nullDecimalToFloatPtr(st.Latitude)
		lng := *nullDecimalToFloatPtr(st.Longitude)
		mp.Latitude = &lat
		mp.Longitude = &lng
	}
	return mp
}

func (s *Service) ActiveLocations(ctx context.Context) ([]ActiveLocation, error) {
	rows, err := s.store.ActiveShipmentLocations(ctx)
	if err != nil {
		return nil, err
	}
	out := make([]ActiveLocation, 0, len(rows))
	for _, r := range rows {
		out = append(out, activeFromRow(r))
	}
	return out, nil
}

// MapFeed composes facilities (warehouses/stores) and active shipments.
func (s *Service) MapFeed(ctx context.Context) (*MapFeed, error) {
	whRows, wErr := s.store.ListWarehouseLocations(ctx)
	stRows, sErr := s.store.ListStoreLocations(ctx)
	shRows, shErr := s.store.ActiveShipmentLocations(ctx)
	if err := firstErr(wErr, sErr, shErr); err != nil {
		return nil, err
	}
	feed := &MapFeed{Warehouses: []MapPlace{}, Stores: []MapPlace{}, Fleets: []FleetPosition{}, Shipments: []ActiveLocation{}}
	flRows, fErr := s.store.FleetLatestLocations(ctx)
	if err := firstErr(fErr); err != nil {
		return nil, err
	}
	for _, fr := range flRows {
		fp := FleetPosition{
			FleetID: fr.FleetID, PlateNumber: fr.PlateNumber,
			VehicleName: fr.VehicleName, Priority: fr.Priority,
		}
		if fr.Latitude.Valid && fr.Longitude.Valid {
			lat := *nullDecimalToFloatPtr(fr.Latitude)
			lng := *nullDecimalToFloatPtr(fr.Longitude)
			fp.Latitude = &lat
			fp.Longitude = &lng
		}
		if fr.LastFixAt.Valid {
			t := fr.LastFixAt.Time
			fp.LastFixAt = &t
		}
		feed.Fleets = append(feed.Fleets, fp)
	}
	for _, w := range whRows {
		feed.Warehouses = append(feed.Warehouses, placeFromWarehouseRow(w))
	}
	for _, st := range stRows {
		feed.Stores = append(feed.Stores, placeFromStoreRow(st))
	}
	for _, r := range shRows {
		feed.Shipments = append(feed.Shipments, activeFromRow(r))
	}
	return feed, nil
}

// canPostTrackingEvent decides whether the caller may append a tracking event
// to this shipment: dispatched driver OR distribution.tracking.record holder.
func (s *Service) canPostTrackingEvent(ctx context.Context, u *auth.User, shipmentID int64) bool {
	if u == nil {
		return false
	}
	if u.Can("distribution.tracking.record") {
		return true
	}
	d, err := s.store.DispatchByShipmentID(ctx, shipmentID)
	if err != nil || !d.DriverID.Valid {
		return false
	}
	return int64(d.DriverID.Int64) == u.ID
}

func latestEventFromRow(r db.ShipmentLatestEventRow) TrackingEvent {
	return TrackingEvent{
		ID:          r.ID,
		ShipmentID:  r.ShipmentID,
		EventTime:   r.EventTime.Time,
		Status:      r.Status,
		Source:      r.Source,
		Latitude:    nullDecimalToFloatPtr(r.Latitude),
		Longitude:   nullDecimalToFloatPtr(r.Longitude),
		AccuracyM:   nullDecimalToFloatPtr(r.AccuracyM),
		SpeedMps:    nullDecimalToFloatPtr(r.SpeedMps),
		HeadingDeg:  nullDecimalToFloatPtr(r.HeadingDeg),
		Note:        textToStrPtr(r.Note),
		ProviderRef: textToStrPtr(r.ProviderRef),
		CreatedBy:   int8ToPtr(r.CreatedBy),
		CreatedAt:   r.CreatedAt.Time,
	}
}
