// Package filters — starlark_filter.go
//
// v0.29.0 CS3: Bridges the pre-completion filter chain to Starlark
// extension scripts. When a package declares "filters.pre_completion"
// permission and is active, this filter runs the package's
// on_pre_completion(ctx) function and converts the return value
// into injected messages.
//
// Starlark contract:
//
//   def on_pre_completion(ctx):
//       """Called before each completion request.
//       ctx is a dict: {"channel_id": "...", "user_id": "...", "persona_id": "..."}
//       Return a list of dicts: [{"role": "system", "content": "..."}]
//       Return None or [] to inject nothing.
//       """
//       return [{"role": "system", "content": "Extra context from extension"}]
package filters

import (
	"context"
	"fmt"
	"log"

	"go.starlark.net/starlark"

	"chat-switchboard/sandbox"
	"chat-switchboard/store"
)

// StarlarkFilter runs a Starlark package's on_pre_completion entry point.
type StarlarkFilter struct {
	runner    *sandbox.Runner
	pkg       *store.PackageRegistration
	order     int
}

// NewStarlarkFilter creates a filter backed by a Starlark package.
func NewStarlarkFilter(runner *sandbox.Runner, pkg *store.PackageRegistration, order int) *StarlarkFilter {
	return &StarlarkFilter{
		runner: runner,
		pkg:    pkg,
		order:  order,
	}
}

func (f *StarlarkFilter) Name() string { return "ext:" + f.pkg.ID }
func (f *StarlarkFilter) Order() int   { return f.order }

func (f *StarlarkFilter) Execute(ctx context.Context, cc *CompletionContext) (*Contribution, error) {
	// Build context dict for the Starlark function
	ctxDict := starlark.NewDict(4)
	ctxDict.SetKey(starlark.String("channel_id"), starlark.String(cc.ChannelID))
	ctxDict.SetKey(starlark.String("user_id"), starlark.String(cc.UserID))
	ctxDict.SetKey(starlark.String("persona_id"), starlark.String(cc.PersonaID))
	ctxDict.SetKey(starlark.String("last_user_message"), starlark.String(cc.LastUserMessage))

	val, output, err := f.runner.CallEntryPoint(ctx, f.pkg, "on_pre_completion",
		starlark.Tuple{ctxDict}, nil, nil)
	if err != nil {
		return nil, fmt.Errorf("ext:%s: %w", f.pkg.ID, err)
	}

	if output != "" {
		log.Printf("  🔧 ext:%s print: %s", f.pkg.ID, output)
	}

	// Parse return value into messages
	messages, err := parseStarlarkMessages(val)
	if err != nil {
		return nil, fmt.Errorf("ext:%s: invalid return value: %w", f.pkg.ID, err)
	}

	if len(messages) == 0 {
		return nil, nil
	}

	return &Contribution{Messages: messages}, nil
}

// parseStarlarkMessages converts a Starlark return value to InjectedMessages.
// Accepts: None, empty list, or list of dicts with "role" and "content" keys.
func parseStarlarkMessages(val starlark.Value) ([]InjectedMessage, error) {
	if val == nil || val == starlark.None {
		return nil, nil
	}

	list, ok := val.(*starlark.List)
	if !ok {
		return nil, fmt.Errorf("expected list or None, got %s", val.Type())
	}

	if list.Len() == 0 {
		return nil, nil
	}

	var messages []InjectedMessage
	iter := list.Iterate()
	defer iter.Done()

	var item starlark.Value
	for iter.Next(&item) {
		dict, ok := item.(*starlark.Dict)
		if !ok {
			return nil, fmt.Errorf("expected dict in list, got %s", item.Type())
		}

		roleVal, found, _ := dict.Get(starlark.String("role"))
		if !found {
			return nil, fmt.Errorf("message dict missing 'role' key")
		}
		role, ok := starlark.AsString(roleVal)
		if !ok {
			return nil, fmt.Errorf("'role' must be a string")
		}

		contentVal, found, _ := dict.Get(starlark.String("content"))
		if !found {
			return nil, fmt.Errorf("message dict missing 'content' key")
		}
		content, ok := starlark.AsString(contentVal)
		if !ok {
			return nil, fmt.Errorf("'content' must be a string")
		}

		messages = append(messages, InjectedMessage{
			Role:    role,
			Content: content,
		})
	}

	return messages, nil
}
