// Package geo wraps the GeoLite2 country resolver and exposes the
// platform-level geo helpers consumed by `backend/internal/auth` at user
// registration time and by the user-surface middleware on every
// authenticated request.
//
// The implementation shipped `LookupCountry` and
// `SetDeclaredCountryAtRegistration`. The implementation added the
// `OnUserDeleted` cascade leg. The implementation layers
// `IncrementCounterAsync` and `ListUserCounters` on top of the same
// Service plus the background-goroutine machinery (cancellable context
// and WaitGroup) needed to drain pending counter upserts on shutdown.
package geo

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"sync"
	"sync/atomic"

	"galaxy/geoip"

	"go.uber.org/zap"
)

// Service is the geo-domain entry point. It is safe for concurrent use.
type Service struct {
	db *sql.DB
	resolver *geoip.Resolver

	logger *zap.Logger

	// bgCtx is the lifetime context passed to fire-and-forget goroutines
	// launched by IncrementCounterAsync. It is cancelled by Close so that
	// in-flight counter upserts observe shutdown promptly. The matching
	// WaitGroup tracks live goroutines so Drain (and Close) can wait for
	// them.
	bgCtx context.Context
	bgCancel context.CancelFunc
	wg sync.WaitGroup
	closed atomic.Bool
}

// NewService constructs a Service backed by the GeoLite2 country database
// at databasePath and the supplied Postgres pool. Closing the returned
// Service releases the memory-mapped database file; the database pool is
// owned by the caller.
//
// A trimmed-empty databasePath is rejected with a non-nil error so that
// boot fails fast rather than silently hiding lookups behind a permanent
// failure path. Callers that explicitly want a no-op Service should
// inject their own implementation via the auth-level interfaces.
//
// The returned Service uses a no-op zap logger by default; callers that
// want diagnostic output from the asynchronous counter path inject one
// via SetLogger.
func NewService(databasePath string, db *sql.DB) (*Service, error) {
	if db == nil {
		return nil, errors.New("geo: db must not be nil")
	}
	resolver, err := geoip.Open(databasePath)
	if err != nil {
		return nil, fmt.Errorf("geo: open resolver: %w", err)
	}
	bgCtx, bgCancel := context.WithCancel(context.Background())
	return &Service{
		db: db,
		resolver: resolver,
		logger: zap.NewNop(),
		bgCtx: bgCtx,
		bgCancel: bgCancel,
	}, nil
}

// SetLogger replaces the diagnostic logger used by the asynchronous
// counter path. A nil argument resets the logger to a no-op so that
// production wiring can supply a real logger after construction without
// the test paths having to thread one through. SetLogger is nil-safe on
// the Service receiver.
func (s *Service) SetLogger(logger *zap.Logger) {
	if s == nil {
		return
	}
	if logger == nil {
		logger = zap.NewNop()
	}
	s.logger = logger.Named("geo")
}

// Drain blocks until every fire-and-forget goroutine launched through
// IncrementCounterAsync has finished, or until ctx is done. It cancels
// the Service-internal background context so live goroutines observe
// shutdown and stop waiting on the database. Drain is nil-safe and
// idempotent: subsequent calls return immediately.
//
// Drain does not close the GeoLite2 resolver — Close does. The split
// lets the boot orchestrator wait for in-flight writes within the
// shutdown deadline before the resolver and database pool are torn
// down.
func (s *Service) Drain(ctx context.Context) {
	if s == nil {
		return
	}
	if s.bgCancel != nil {
		s.bgCancel()
	}
	done := make(chan struct{})
	go func() {
		s.wg.Wait()
		close(done)
	}()
	select {
	case <-done:
	case <-ctx.Done():
	}
}

// Close releases the underlying GeoLite2 database resources. Pending
// counter goroutines launched through IncrementCounterAsync are
// signalled to stop via the internal background context but are NOT
// awaited; callers that need to wait must invoke Drain first. Close is
// idempotent and nil-safe; subsequent lookups return the empty country
// / language ("" treated as no data).
func (s *Service) Close() error {
	if s == nil {
		return nil
	}
	if !s.closed.CompareAndSwap(false, true) {
		return nil
	}
	if s.bgCancel != nil {
		s.bgCancel()
	}
	if s.resolver == nil {
		return nil
	}
	if err := s.resolver.Close(); err != nil {
		return fmt.Errorf("geo: close resolver: %w", err)
	}
	s.resolver = nil
	return nil
}

// LookupCountry resolves an uppercase ISO 3166-1 alpha-2 country code
// from sourceIP. The lookup is best-effort: the empty string is returned
// for any invalid address, missing record, or closed resolver. The
// returned error is always nil; callers that need diagnostic detail
// should query the geoip resolver directly.
func (s *Service) LookupCountry(sourceIP string) string {
	if s == nil || s.resolver == nil || sourceIP == "" {
		return ""
	}
	code, err := s.resolver.CountryString(sourceIP)
	if err != nil {
		return ""
	}
	return code
}
