package auth

import (
	"context"
	"net/http"

	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
)

// StoreScope is the caller's tenant membership (multi-tenant §5.4):
// platform-wide users (no store_users rows) act across all tenants;
// restricted users are limited to the listed store ids.
type StoreScope struct{ ids []int64 }

// NewStoreScope builds a scope; nil ids means platform-wide.
func NewStoreScope(ids []int64) StoreScope { return StoreScope{ids: ids} }

// PlatformWide reports unrestricted cross-store access.
func (s StoreScope) PlatformWide() bool { return s.ids == nil }

// Allows reports access to a resource bound to storeID. A NULL store
// reference is a platform-level row: visible only to platform-wide scopes.
func (s StoreScope) Allows(storeID *int64) bool {
	if s.PlatformWide() {
		return true
	}
	if storeID == nil {
		return false
	}
	for _, id := range s.ids {
		if id == *storeID {
			return true
		}
	}
	return false
}

// AllowsID is Allows for a required (non-null) store reference.
func (s StoreScope) AllowsID(id int64) bool { return s.Allows(&id) }

// ResolveStoreBinding derives the tenant binding for a write from the
// caller's identity (multi-tenant §5.4): tenant context must never come from
// request input alone.
//
//   - Platform-wide callers keep the caller-supplied binding (nil stays nil:
//     legacy/unbound rows).
//   - A restricted caller with exactly one store gets that store implied when
//     none was supplied.
//   - A restricted multi-store caller MUST supply an explicit binding.
//   - An explicit binding outside the caller's stores is rejected.
//
// Returns the resolved binding (possibly nil) or false when denied.
func (s StoreScope) ResolveStoreBinding(in *int64) (*int64, bool) {
	if s.PlatformWide() {
		return in, true
	}
	switch {
	case in == nil && len(s.ids) == 1:
		id := s.ids[0]
		return &id, true
	case in != nil && s.Allows(in):
		return in, true
	default:
		return nil, false
	}
}

// IDs exposes the scoped store ids (nil = platform-wide). Used by list
// filters that push tenant isolation into SQL.
func (s StoreScope) IDs() []int64 { return s.ids }

const scopeKey ctxKey = 3

// WithStoreScope attaches the scope to ctx.
func WithStoreScope(ctx context.Context, sc StoreScope) context.Context {
	return context.WithValue(ctx, scopeKey, sc)
}

// StoreScopeFrom returns the caller's scope; absent middleware = platform-wide.
func StoreScopeFrom(ctx context.Context) StoreScope {
	if sc, ok := ctx.Value(scopeKey).(StoreScope); ok {
		return sc
	}
	return StoreScope{}
}

// HasStoreScope reports whether a scope was actually ATTACHED to ctx, as
// opposed to StoreScopeFrom's default.
//
// The two are indistinguishable through StoreScope alone: an attached
// platform-wide scope (admin/owner, or a user with no store_users row) and an
// ABSENT scope both carry ids == nil, and both answer PlatformWide(). That
// default is right for the pre-existing callers, which treat an absent scope as
// "no restriction" — but it is the wrong reading for a surface that must fail
// CLOSED, because "the middleware never ran" and "this caller is an
// administrator" are not the same statement.
//
// A surface that must not leak when scoping is absent therefore asks this
// question first, and answers it by treating an absent scope as no access at
// all. See finance.cashflowStoreScope.
func HasStoreScope(ctx context.Context) bool {
	_, ok := ctx.Value(scopeKey).(StoreScope)
	return ok
}

// ScopeLoader resolves a user's tenant memberships as store ids.
// Return nil for platform-wide users.
type ScopeLoader func(ctx context.Context, userID int64) ([]int64, error)

// ScopeMiddleware loads and injects the authenticated user's store scope.
// Must run after Middleware. Loader failure = 500.
func ScopeMiddleware(load ScopeLoader) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if u := UserFrom(r.Context()); u != nil {
				ids, err := load(r.Context(), u.ID)
				if err != nil {
					httpresponse.Error(w, http.StatusInternalServerError, middleware.RequestIDFrom(r.Context()),
						httpresponse.CodeInternal, "Gagal memuat cakupan toko", nil)
					return
				}
				r = r.WithContext(WithStoreScope(r.Context(), NewStoreScope(ids)))
			}
			next.ServeHTTP(w, r)
		})
	}
}
