package node

import (
	"fmt"

	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

// BuildConfigPayload converts a NodeRecord's adapter config to the proto payload
// sent to node during registration.
//
// Provider-first entries in rec.Providers are compiled first: each provider id
// becomes the adapter instance key. Legacy adapter instances in rec.Adapters are
// appended afterwards. An instance key that appears in both produces an error.
// Providers with a non-empty Adapter field reference a legacy instance and are
// skipped during provider-first compile.
//
// After config normalisation, OllamaInstances, VllmInstances, and
// OpenAICompatInstances already contain the promoted legacy single-instance
// entries, so only the slice fields are iterated here. Duplicate instance names
// produce an error.
func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) {
	payload := &iop.NodeConfigPayload{
		Runtime: &iop.NodeRuntimeConfig{
			Concurrency: int32(rec.Runtime.Concurrency),
		},
	}

	payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
		Type:    "mock",
		Enabled: true,
		Config:  &iop.AdapterConfig_Mock{Mock: &iop.MockAdapterConfig{}},
	})

	// flat namespace to detect conflicts between provider ids and legacy adapter keys
	seenFlat := make(map[string]struct{})

	for _, p := range rec.Providers {
		if p.Adapter != "" {
			// references a legacy adapter instance; skip provider-first compile
			continue
		}
		if _, dup := seenFlat[p.ID]; dup {
			return nil, fmt.Errorf("duplicate provider id %q", p.ID)
		}
		seenFlat[p.ID] = struct{}{}
		ac, err := providerToAdapterConfig(p)
		if err != nil {
			return nil, err
		}
		payload.Adapters = append(payload.Adapters, ac)
	}

	seen := make(map[string]struct{})

	for _, inst := range rec.Adapters.OllamaInstances {
		if !inst.Enabled {
			continue
		}
		key := "ollama:" + inst.Name
		if _, dup := seen[key]; dup {
			return nil, fmt.Errorf("duplicate ollama instance name %q", inst.Name)
		}
		seen[key] = struct{}{}
		if _, conflict := seenFlat[inst.Name]; conflict {
			return nil, fmt.Errorf("ollama adapter instance key %q conflicts with provider id", inst.Name)
		}
		payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
			Type:    "ollama",
			Enabled: true,
			Name:    inst.Name,
			Config: &iop.AdapterConfig_Ollama{
				Ollama: &iop.OllamaAdapterConfig{
					BaseUrl:          inst.BaseURL,
					ContextSize:      int32(inst.ContextSize),
					Capacity:         int32(inst.Capacity),
					MaxQueue:         int32(inst.MaxQueue),
					QueueTimeoutMs:   int32(inst.QueueTimeoutMS),
					RequestTimeoutMs: int32(inst.RequestTimeoutMS),
				},
			},
		})
	}

	for _, inst := range rec.Adapters.VllmInstances {
		if !inst.Enabled {
			continue
		}
		key := "vllm:" + inst.Name
		if _, dup := seen[key]; dup {
			return nil, fmt.Errorf("duplicate vllm instance name %q", inst.Name)
		}
		seen[key] = struct{}{}
		if _, conflict := seenFlat[inst.Name]; conflict {
			return nil, fmt.Errorf("vllm adapter instance key %q conflicts with provider id", inst.Name)
		}
		payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
			Type:    "vllm",
			Enabled: true,
			Name:    inst.Name,
			Config: &iop.AdapterConfig_Vllm{
				Vllm: &iop.VllmAdapterConfig{
					Endpoint:         inst.Endpoint,
					Capacity:         int32(inst.Capacity),
					MaxQueue:         int32(inst.MaxQueue),
					QueueTimeoutMs:   int32(inst.QueueTimeoutMS),
					RequestTimeoutMs: int32(inst.RequestTimeoutMS),
				},
			},
		})
	}

	for _, inst := range rec.Adapters.OpenAICompatInstances {
		if !inst.Enabled {
			continue
		}
		key := "openai_compat:" + inst.Name
		if _, dup := seen[key]; dup {
			return nil, fmt.Errorf("duplicate openai_compat instance name %q", inst.Name)
		}
		seen[key] = struct{}{}
		if _, conflict := seenFlat[inst.Name]; conflict {
			return nil, fmt.Errorf("openai_compat adapter instance key %q conflicts with provider id", inst.Name)
		}
		payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
			Type:    "openai_compat",
			Enabled: true,
			Name:    inst.Name,
			Config: &iop.AdapterConfig_OpenaiCompat{
				OpenaiCompat: &iop.OpenAICompatAdapterConfig{
					Provider:         inst.Provider,
					Endpoint:         inst.Endpoint,
					Headers:          inst.Headers,
					Capacity:         int32(inst.Capacity),
					MaxQueue:         int32(inst.MaxQueue),
					QueueTimeoutMs:   int32(inst.QueueTimeoutMS),
					RequestTimeoutMs: int32(inst.RequestTimeoutMS),
				},
			},
		})
	}

	if rec.Adapters.CLI.Enabled {
		if _, conflict := seenFlat["cli"]; conflict {
			return nil, fmt.Errorf("cli adapter instance key %q conflicts with provider id", "cli")
		}
		profiles := make(map[string]*iop.CLIProfileConfig, len(rec.Adapters.CLI.Profiles))
		for name, p := range rec.Adapters.CLI.Profiles {
			profiles[name] = cliProfileToProto(p)
		}
		payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
			Type:    "cli",
			Enabled: true,
			Config: &iop.AdapterConfig_Cli{
				Cli: &iop.CLIAdapterConfig{Profiles: profiles},
			},
		})
	}
	return payload, nil
}

// providerToAdapterConfig compiles a provider-first NodeProviderConf into an
// AdapterConfig using the provider id as the instance key.
func providerToAdapterConfig(p config.NodeProviderConf) (*iop.AdapterConfig, error) {
	typeName := config.NormalizeProviderType(p.Type)
	switch typeName {
	case "ollama":
		return &iop.AdapterConfig{
			Type:    "ollama",
			Enabled: true,
			Name:    p.ID,
			Config: &iop.AdapterConfig_Ollama{
				Ollama: &iop.OllamaAdapterConfig{
					BaseUrl:          p.BaseURL,
					ContextSize:      int32(p.ContextSize),
					Capacity:         int32(p.Capacity),
					MaxQueue:         int32(p.MaxQueue),
					QueueTimeoutMs:   int32(p.QueueTimeoutMS),
					RequestTimeoutMs: int32(p.RequestTimeoutMS),
				},
			},
		}, nil

	case "openai_compat":
		return &iop.AdapterConfig{
			Type:    "openai_compat",
			Enabled: true,
			Name:    p.ID,
			Config: &iop.AdapterConfig_OpenaiCompat{
				OpenaiCompat: &iop.OpenAICompatAdapterConfig{
					Provider:         p.Provider,
					Endpoint:         p.Endpoint,
					Headers:          p.Headers,
					Capacity:         int32(p.Capacity),
					MaxQueue:         int32(p.MaxQueue),
					QueueTimeoutMs:   int32(p.QueueTimeoutMS),
					RequestTimeoutMs: int32(p.RequestTimeoutMS),
				},
			},
		}, nil

	case "cli":
		// provider id becomes both the adapter instance key and the profile key
		return &iop.AdapterConfig{
			Type:    "cli",
			Enabled: true,
			Name:    p.ID,
			Config: &iop.AdapterConfig_Cli{
				Cli: &iop.CLIAdapterConfig{
					Profiles: map[string]*iop.CLIProfileConfig{
						p.ID: {
							Command:      p.Command,
							Args:         append([]string(nil), p.Args...),
							Env:          append([]string(nil), p.Env...),
							Mode:         p.Mode,
							OutputFormat: p.OutputFormat,
							ResumeArgs:   append([]string(nil), p.ResumeArgs...),
						},
					},
				},
			},
		}, nil

	default:
		return nil, fmt.Errorf("provider %q: unsupported type %q for provider-first compile", p.ID, p.Type)
	}
}

func cliProfileToProto(p config.CLIProfileConf) *iop.CLIProfileConfig {
	out := &iop.CLIProfileConfig{
		Command:               p.Command,
		Args:                  append([]string(nil), p.Args...),
		Env:                   append([]string(nil), p.Env...),
		Persistent:            p.Persistent,
		Terminal:              p.Terminal,
		ResponseIdleTimeoutMs: int32(p.ResponseIdleTimeoutMS),
		StartupIdleTimeoutMs:  int32(p.StartupIdleTimeoutMS),
		OutputFormat:          p.OutputFormat,
		Mode:                  p.Mode,
		ResumeArgs:            append([]string(nil), p.ResumeArgs...),
	}
	if !p.CompletionMarker.Empty() {
		out.CompletionMarker = &iop.CLICompletionMarker{
			Line:  p.CompletionMarker.Line,
			Regex: p.CompletionMarker.Regex,
		}
	}
	return out
}
