package dialecttransport

import (
	"fmt"
	"sort"
	"strings"
	"sync"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-api-host/dialect"
)

// The gin-free door's process registry. Bundled gin-free dialect partitions
// register their identity here (via the bundle manifest's Register), and the host
// drains the registry through MountRegistered — so the host bootstrap names no
// dialect and no pack. A new app pack's partition mounts by registering itself;
// the platform runtime does not change.
var (
	registryMu sync.Mutex
	registry   = map[string]dialect.Dialect{}
)

// RegisterInto adds a gin-free dialect partition to the process registry. Name is
// the unique registry key (the same key the binder enforces and the SDK registry
// uses); an empty or duplicate Name panics at registration so a collision fails
// closed at init, never as an ambiguous mount identity at request time.
func RegisterInto(d dialect.Dialect) {
	registryMu.Lock()
	defer registryMu.Unlock()
	name := strings.TrimSpace(d.Name())
	if name == "" {
		panic("dialecttransport: RegisterInto a dialect with empty Name (Name is the unique registry key)")
	}
	if _, dup := registry[name]; dup {
		panic(fmt.Sprintf("dialecttransport: duplicate dialect name %q registered", name))
	}
	registry[name] = d
}

// Registered returns the registered partitions sorted by Name (deterministic mount
// and discovery order, matching the binder's own sort).
func Registered() []dialect.Dialect {
	registryMu.Lock()
	defer registryMu.Unlock()
	out := make([]dialect.Dialect, 0, len(registry))
	for _, d := range registry {
		out = append(out, d)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
	return out
}

// ResetForTest clears the registry so a test that exercises registration starts
// from a known-empty state. Production never calls it (the bundle manifest's
// sync.Once populates the registry exactly once per process).
func ResetForTest() {
	registryMu.Lock()
	defer registryMu.Unlock()
	registry = map[string]dialect.Dialect{}
}

// MountRegistered mounts every registered gin-free partition onto router through
// Mount. The host SUPPLIES the pack catalog (packCatalog) — the single 3-state
// known-pack map built once at boot from external authorities (DB rows + the
// platform-pack list); MountRegistered no longer derives one from the registered
// partitions' own OwningPacks (that self-seed was the #1987 circularity, now
// removed). The host still names no dialect or pack here: partitions self-register
// in the bundle manifest, so a second app pack's partition mounts with zero host
// change. It returns one neutral dialect.Mount per mounted version.
func MountRegistered(
	router *gin.Engine,
	deps dialect.Deps,
	base []gin.HandlerFunc,
	userFromCtx UserExtractor,
	permKnown PermissionResolver,
	packCatalog map[string]bool,
	opts ...MountOption,
) []dialect.Mount {
	// An EMPTY registry is LEGITIMATE here (#2015 PR-0): the app-blind public-core
	// build (cmd/server, build !crm) bundles NO product dialect, so the gin-free
	// door registers nothing and this generic drain mounts nothing — exactly the
	// app-blind goal. MountRegistered must therefore tolerate empty and not fail
	// closed on it. The fail-closed-on-broken-registration assertion lives in the
	// product distribution hook (for CRM, cmd/crm-server), which asserts the
	// expected partition is present immediately after registering it — the place
	// that actually knows registration was supposed to happen. A registered
	// partition whose owning pack is absent/inactive still keeps the registry
	// non-empty and skips at Mount.
	return Mount(router, Registered(), deps, base, userFromCtx, permKnown, packCatalog, opts...)
}
