// Package sandbox — lib_module.go
//
// v0.38.2: Library loading module for Starlark extensions.
// No permission required — any starlark package can use lib.require().
//
// Starlark API:
//   gitea = lib.require("gitea-client")   → struct with library's exports
//
// The loaded library runs with its own permission context, not the
// consumer's. Results are cached per ExecPackage invocation.
package sandbox

import (
	"context"
	"fmt"
	"log"

	"go.starlark.net/starlark"
	"go.starlark.net/starlarkstruct"

	"switchboard-core/models"
)

// libContext carries per-invocation state for lib.require() calls.
// Created once per ExecPackage and threaded through buildModules.
type libContext struct {
	cache   map[string]starlark.Value // library ID → frozen exports struct
	loading map[string]bool           // cycle detection
}

func newLibContext() *libContext {
	return &libContext{
		cache:   make(map[string]starlark.Value),
		loading: make(map[string]bool),
	}
}

// BuildLibModule creates the "lib" module.
// consumerPkgID is the package calling lib.require() — used to validate
// the dependency record exists.
func BuildLibModule(ctx context.Context, r *Runner, consumerPkgID string, rc *RunContext, lc *libContext) *starlarkstruct.Module {
	return MakeModule("lib", starlark.StringDict{
		"require": starlark.NewBuiltin("lib.require", func(
			thread *starlark.Thread, b *starlark.Builtin,
			args starlark.Tuple, kwargs []starlark.Tuple,
		) (starlark.Value, error) {
			var libraryID string
			if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &libraryID); err != nil {
				return nil, err
			}

			// 1. Check cache
			if cached, ok := lc.cache[libraryID]; ok {
				return cached, nil
			}

			// 2. Cycle detection
			if lc.loading[libraryID] {
				return nil, fmt.Errorf("lib.require: circular dependency: %q", libraryID)
			}
			lc.loading[libraryID] = true
			defer func() { delete(lc.loading, libraryID) }()

			// 3. Verify dependency is declared
			if r.stores.Dependencies == nil {
				return nil, fmt.Errorf("lib.require: dependency store not available")
			}
			deps, err := r.stores.Dependencies.ListByConsumer(ctx, consumerPkgID)
			if err != nil {
				return nil, fmt.Errorf("lib.require: failed to check dependencies: %w", err)
			}
			declared := false
			for _, dep := range deps {
				if dep.LibraryID == libraryID {
					declared = true
					break
				}
			}
			if !declared {
				return nil, fmt.Errorf("lib.require: package %q does not declare dependency on %q", consumerPkgID, libraryID)
			}

			// 4. Load library's PackageRegistration
			libPkg, err := r.stores.Packages.Get(ctx, libraryID)
			if err != nil {
				return nil, fmt.Errorf("lib.require: library %q not found: %w", libraryID, err)
			}
			if libPkg.Type != "library" {
				return nil, fmt.Errorf("lib.require: package %q is type %q, not library", libraryID, libPkg.Type)
			}
			if libPkg.Status != models.PackageStatusActive {
				return nil, fmt.Errorf("lib.require: library %q is %s, not active", libraryID, libPkg.Status)
			}
			if libPkg.Tier != models.ExtTierStarlark {
				return nil, fmt.Errorf("lib.require: library %q is tier %s, not starlark", libraryID, libPkg.Tier)
			}

			// 5. Build library's module set (library's own permissions)
			libModules, err := r.buildModulesWithLibCtx(ctx, libraryID, libPkg.Manifest, rc, lc)
			if err != nil {
				return nil, fmt.Errorf("lib.require: failed to build modules for library %q: %w", libraryID, err)
			}

			// 6. Load library script from disk
			libScript, err := r.loadScript(libPkg)
			if err != nil {
				return nil, fmt.Errorf("lib.require: %w", err)
			}

			// 7. Execute with library-scoped loader
			libLoader := r.packageLoader(libraryID, libModules)
			result, err := r.sandbox.ExecWithLoader(ctx, libraryID+"/script.star", libScript, libModules, libLoader)
			if err != nil {
				return nil, fmt.Errorf("lib.require: error executing library %q: %w", libraryID, err)
			}

			// 8. Extract exports from manifest
			exports := extractExports(libPkg.Manifest)
			if len(exports) == 0 {
				return nil, fmt.Errorf("lib.require: library %q declares no exports", libraryID)
			}

			members := make(starlark.StringDict, len(exports))
			for _, name := range exports {
				val, ok := result.Globals[name]
				if !ok {
					return nil, fmt.Errorf("lib.require: library %q does not export %q", libraryID, name)
				}
				members[name] = val
			}

			// 9. Freeze into struct, cache, return
			frozen := starlarkstruct.FromStringDict(starlark.String(libraryID), members)
			lc.cache[libraryID] = frozen

			log.Printf("  📦 lib.require: loaded %s (%d exports) for %s", libraryID, len(exports), consumerPkgID)
			return frozen, nil
		}),
	})
}

// extractExports reads the "exports" array from a package manifest.
func extractExports(manifest map[string]any) []string {
	raw, ok := manifest["exports"].([]any)
	if !ok {
		return nil
	}
	exports := make([]string, 0, len(raw))
	for _, v := range raw {
		if s, ok := v.(string); ok {
			exports = append(exports, s)
		}
	}
	return exports
}
