package gemini

import (
	"context"
	"errors"

	"github.com/danielmiessler/fabric/common"
	"github.com/google/generative-ai-go/genai"
	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
)

func NewClient() (ret *Client) {
	vendorName := "Gemini"
	ret = &Client{}

	ret.Configurable = &common.Configurable{
		Label:         vendorName,
		EnvNamePrefix: common.BuildEnvVariablePrefix(vendorName),
	}

	ret.ApiKey = ret.Configurable.AddSetupQuestion("API key", true)

	return
}

type Client struct {
	*common.Configurable
	ApiKey *common.SetupQuestion

	client *genai.Client
}

func (ge *Client) ListModels() (ret []string, err error) {
	ctx := context.Background()
	var client *genai.Client
	if client, err = genai.NewClient(ctx, option.WithAPIKey(ge.ApiKey.Value)); err != nil {
		return
	}
	defer client.Close()

	iter := client.ListModels(ctx)
	for {
		var resp *genai.ModelInfo
		if resp, err = iter.Next(); err != nil {
			break
		}
		ret = append(ret, resp.Name)
	}
	return
}

func (ge *Client) Send(msgs []*common.Message, opts *common.ChatOptions) (ret string, err error) {
	systemInstruction, userText := toContent(msgs)

	ctx := context.Background()
	var client *genai.Client
	if client, err = genai.NewClient(ctx, option.WithAPIKey(ge.ApiKey.Value)); err != nil {
		return
	}
	defer client.Close()

	model := ge.client.GenerativeModel(opts.Model)
	model.SetTemperature(float32(opts.Temperature))
	model.SetTopP(float32(opts.TopP))
	model.SystemInstruction = systemInstruction

	var response *genai.GenerateContentResponse
	if response, err = model.GenerateContent(ctx, genai.Text(userText)); err != nil {
		return
	}

	ret = ge.extractText(response)
	return
}

func (ge *Client) SendStream(msgs []*common.Message, opts *common.ChatOptions, channel chan string) (err error) {
	ctx := context.Background()
	var client *genai.Client
	if client, err = genai.NewClient(ctx, option.WithAPIKey(ge.ApiKey.Value)); err != nil {
		return
	}
	defer client.Close()

	systemInstruction, userText := toContent(msgs)

	model := client.GenerativeModel(opts.Model)
	model.SetTemperature(float32(opts.Temperature))
	model.SetTopP(float32(opts.TopP))
	model.SystemInstruction = systemInstruction

	iter := model.GenerateContentStream(ctx, genai.Text(userText))
	for {
		var resp *genai.GenerateContentResponse
		if resp, err = iter.Next(); err == nil {
			for _, candidate := range resp.Candidates {
				if candidate.Content != nil {
					for _, part := range candidate.Content.Parts {
						if text, ok := part.(genai.Text); ok {
							channel <- string(text)
						}
					}
				}
			}
		} else if errors.Is(err, iterator.Done) {
			channel <- "\n"
			close(channel)
			err = nil
		}
		return
	}
}

func (ge *Client) extractText(response *genai.GenerateContentResponse) (ret string) {
	for _, candidate := range response.Candidates {
		if candidate.Content == nil {
			break
		}
		for _, part := range candidate.Content.Parts {
			if text, ok := part.(genai.Text); ok {
				ret += string(text)
			}
		}
	}
	return
}

// Current implementation does not support session
// We need to retrieve the System instruction and User instruction
// Considering how we've built msgs, it's the last 2 messages
// FIXME: I know it's not clean, but will make it for now
func toContent(msgs []*common.Message) (ret *genai.Content, userText string) {
	sys := msgs[len(msgs)-2]
	usr := msgs[len(msgs)-1]

	ret = &genai.Content{
		Parts: []genai.Part{
			genai.Part(genai.Text(sys.Content)),
		},
	}
	userText = usr.Content

	return
}