package adapters

import (
	"encoding/json"
	"fmt"
	"sort"

	"go.uber.org/zap"

	"iop/apps/node/internal/adapters/mock"
	"iop/apps/node/internal/adapters/ollama"
	"iop/apps/node/internal/adapters/openai_compat"
	"iop/apps/node/internal/adapters/vllm"
	"iop/packages/go/config"
	runtime "iop/packages/go/execution"
	iop "iop/proto/gen/iop"
)

type ConfigSet struct {
	Registry *runtime.Registry
	Items    map[string]ConfigItem
	Runtime  RuntimeConfig
}

// RuntimeConfig captures node-wide runtime tuning carried alongside the adapter
// set so config refresh can detect concurrency changes.
type RuntimeConfig struct {
	Concurrency int
}

type ConfigItem struct {
	Key         string
	Type        string
	Fingerprint string
}

type ConfigDiff struct {
	Added     []string
	Updated   []string
	Removed   []string
	Unchanged []string
}

func BuildConfigSet(payload *iop.NodeConfigPayload, logger *zap.Logger) (*ConfigSet, error) {
	reg := runtime.NewRegistry()
	items := make(map[string]ConfigItem)

	for _, ac := range payload.GetAdapters() {
		if !ac.GetEnabled() {
			continue
		}
		instanceKey := ac.GetName()
		typeName := ac.GetType()
		if instanceKey == "" {
			instanceKey = typeName
		}

		var fingerprint string

		switch typeName {
		case "mock":
			a := mock.New(logger)
			reg.RegisterKeyed(instanceKey, typeName, a)
			fingerprint = "mock"

		case "ollama":
			var cfg config.OllamaConf
			if m := ac.GetOllama(); m != nil {
				cfg = ollamaConfFromProto(m)
			} else {
				cfg = ollamaConfFromStruct(ac.GetSettings())
			}
			a := ollama.New(cfg, logger, instanceKey)
			reg.RegisterKeyed(instanceKey, typeName, a)

			bs, err := json.Marshal(cfg)
			if err != nil {
				return nil, fmt.Errorf("marshal ollama config: %w", err)
			}
			fingerprint = string(bs)

		case "vllm":
			var cfg config.VllmConf
			if m := ac.GetVllm(); m != nil {
				cfg = vllmConfFromProto(m)
			} else {
				cfg = vllmConfFromStruct(ac.GetSettings())
			}
			a := vllm.New(cfg, logger, instanceKey)
			reg.RegisterKeyed(instanceKey, typeName, a)

			bs, err := json.Marshal(cfg)
			if err != nil {
				return nil, fmt.Errorf("marshal vllm config: %w", err)
			}
			fingerprint = string(bs)

		case "openai_compat":
			var cfg config.OpenAICompatConf
			if m := ac.GetOpenaiCompat(); m != nil {
				cfg = openAICompatConfFromProto(m)
			} else {
				cfg = openAICompatConfFromStruct(ac.GetSettings())
			}
			a := openai_compat.New(cfg, logger, instanceKey)
			reg.RegisterKeyed(instanceKey, typeName, a)

			bs, err := json.Marshal(cfg)
			if err != nil {
				return nil, fmt.Errorf("marshal openai_compat config: %w", err)
			}
			fingerprint = string(bs)

		default:
			return nil, fmt.Errorf("adapters: unknown adapter type %q", typeName)
		}

		items[instanceKey] = ConfigItem{
			Key:         instanceKey,
			Type:        typeName,
			Fingerprint: fingerprint,
		}
	}

	return &ConfigSet{
		Registry: reg,
		Items:    items,
		Runtime: RuntimeConfig{
			Concurrency: int(payload.GetRuntime().GetConcurrency()),
		},
	}, nil
}

func DiffConfigSets(current, next *ConfigSet) ConfigDiff {
	var diff ConfigDiff

	currItems := make(map[string]ConfigItem)
	if current != nil {
		currItems = current.Items
	}
	nextItems := make(map[string]ConfigItem)
	if next != nil {
		nextItems = next.Items
	}

	// Added / Updated / Unchanged
	for k, nextItem := range nextItems {
		currItem, exists := currItems[k]
		if !exists {
			diff.Added = append(diff.Added, k)
		} else if currItem.Type != nextItem.Type || currItem.Fingerprint != nextItem.Fingerprint {
			diff.Updated = append(diff.Updated, k)
		} else {
			diff.Unchanged = append(diff.Unchanged, k)
		}
	}

	// Removed
	for k := range currItems {
		if _, exists := nextItems[k]; !exists {
			diff.Removed = append(diff.Removed, k)
		}
	}

	sort.Strings(diff.Added)
	sort.Strings(diff.Updated)
	sort.Strings(diff.Removed)
	sort.Strings(diff.Unchanged)

	return diff
}
