package registry

import (
	"fmt"
	"io/fs"

	"github.com/tidbcloud/AgentTeam/internal/extension/spec"
)

// RegisterEmbeddedExtensions loads every Definition package under root in fsys
// and registers them as External Extensions. OpenAPI-derived Packages carry an
// HTTP binding (not a code executor), so per spec §5.1 they are External, not
// Builtin.
//
// Registration is ATOMIC: under a single lock the whole batch is validated
// (each package's full Definition re-validated plus digest re-verified, plus
// conflict checks against the existing registry AND among the batch), and ONLY
// if every package passes are all of them committed. On ANY failure the
// registry is left UNCHANGED — zero writes. This means a single bad or
// conflicting package aborts the whole bootstrap rather than leaving a partial
// registry, and a concurrent Register cannot interleave with the batch to
// produce partial state. Packages are processed in the sorted order returned by
// the loader, so startup is deterministic.
//
// Repeated calls are idempotent: an already-registered same-(domain, version,
// digest) External package is a no-op success for that package.
//
// V1 EXPLICITLY ALLOWS a production embed that ships zero bundled external
// packages (spec §5.1). Real Extension Definitions are curated later, so an
// empty embed is a SUCCESS no-op: the server starts with zero built-in
// Extensions rather than failing closed on an empty set. A non-empty embed that
// fails to load or validate still fails closed as usual.
func RegisterEmbeddedExtensions(r *Registry, fsys fs.FS, root string) error {
	if r == nil {
		return fmt.Errorf("registry: RegisterEmbeddedExtensions called with nil registry")
	}
	if fsys == nil {
		return fmt.Errorf("registry: RegisterEmbeddedExtensions called with nil fs.FS")
	}

	pkgs, err := spec.LoadDefinitions(fsys, root)
	if err != nil {
		return fmt.Errorf("load embedded definitions: %w", err)
	}
	if len(pkgs) == 0 {
		// V1 allows zero bundled external packages; this is a success no-op,
		// not a wiring error (see the doc comment above).
		return nil
	}

	return r.registerEmbeddedBatch(pkgs)
}

// registerEmbeddedBatch atomically validates, normalizes, and commits pkgs as
// External Extensions. It acquires r.mu exclusively and, under that single lock:
//
//  1. Prepares every package through the SAME ValidateAndNormalizeDefinition
//     path RegisterExternal uses (full Definition re-validation + digest
//     re-verify + NormalizeDefinition + digest post-verify). This is what makes
//     the embedded path and a direct RegisterExternal produce byte-identical
//     stored Definitions/Bindings for the same semantic content.
//  2. Conflict-checks every package against the existing registry AND against
//     the other packages in this batch (no partial commits if a later package
//     conflicts with an earlier one).
//  3. Only if ALL packages pass, commits every newly-staged entry.
//
// On ANY failure it returns an error and writes NOTHING, so a conflicting or
// corrupt package cannot leave the registry in a partial state. The whole
// prepare-all → commit-all sequence runs under one lock, so a concurrent
// Register cannot interleave with the batch to produce partial state.
func (r *Registry) registerEmbeddedBatch(pkgs []spec.Package) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	// Prepare-all (validate → verify → normalize → post-verify) before any
	// commit. Same helper as RegisterExternal, so the two paths share one
	// preparation route and produce identical stored records for the same digest.
	normalized := make([]spec.Package, len(pkgs))
	for i := range pkgs {
		n, err := spec.ValidateAndNormalizeDefinition(pkgs[i])
		if err != nil {
			return fmt.Errorf("verify embedded definition %q: %w", pkgs[i].Metadata.Domain, err)
		}
		normalized[i] = n
	}

	staged, err := r.planEmbeddedBatch(normalized)
	if err != nil {
		return err
	}
	for _, s := range staged {
		entry := &registeredEntry{
			kind:       External,
			definition: s.definition,
			binding:    s.binding,
		}
		r.byDigest[s.digest] = entry
		r.byIdentity[s.key] = s.digest
	}
	return nil
}

// stagedEntry is one batch package that passed all conflict checks and is ready
// to commit. Its Definition and Binding are already deep-cloned and detached
// from the caller's input.
type stagedEntry struct {
	digest     string
	key        string
	definition Definition
	binding    ExternalBinding
}

// planEmbeddedBatch checks every (already-canonical) package against the
// registry and the rest of the batch WITHOUT mutating the registry, returning
// the entries to commit. The caller MUST hold r.mu exclusively. Each package is
// the output of ValidateAndNormalizeDefinition (validated, digest-verified, and
// normalized), so buildDefinitionFromPackage/buildExternalBindingFromPackage see
// canonical input — exactly what RegisterExternal stores. It enforces the same
// idempotency, conflict, and kind-immutability rules as RegisterExternal.
func (r *Registry) planEmbeddedBatch(pkgs []spec.Package) ([]stagedEntry, error) {
	staged := make([]stagedEntry, 0, len(pkgs))
	batchKey := make(map[string]string, len(pkgs))  // identityKey -> digest (intra-batch)
	batchDigest := make(map[string]bool, len(pkgs)) // digest already staged this batch

	for i := range pkgs {
		pkg := pkgs[i]
		digest := pkg.Digest
		key := identityKey(pkg.Metadata.Domain, pkg.Metadata.Version)

		if existingDigest, ok := r.byIdentity[key]; ok {
			if existingDigest != digest {
				return nil, &ConflictError{
					Domain: pkg.Metadata.Domain, Version: pkg.Metadata.Version,
					Existing: existingDigest, Incoming: digest,
				}
			}
			existing := r.byDigest[digest]
			if existing.kind != External {
				return nil, &KindMismatchError{
					Domain: pkg.Metadata.Domain, Version: pkg.Metadata.Version,
					Digest: digest, Existing: existing.kind, Incoming: External,
				}
			}
			continue // already registered idempotently; nothing to stage
		}
		if _, exists := r.byDigest[digest]; exists {
			return nil, fmt.Errorf(
				"registry: digest %s already registered under a different (domain, version) identity", digest)
		}
		if prevDigest, ok := batchKey[key]; ok {
			if prevDigest != digest {
				return nil, &ConflictError{
					Domain: pkg.Metadata.Domain, Version: pkg.Metadata.Version,
					Existing: prevDigest, Incoming: digest,
				}
			}
			continue // duplicate within the batch; idempotent
		}
		if batchDigest[digest] {
			return nil, fmt.Errorf(
				"registry: digest %s appears twice in the embedded batch under different (domain, version) identities", digest)
		}
		staged = append(staged, stagedEntry{
			digest:     digest,
			key:        key,
			definition: buildDefinitionFromPackage(pkg),
			binding:    buildExternalBindingFromPackage(pkg),
		})
		batchKey[key] = digest
		batchDigest[digest] = true
	}
	return staged, nil
}
