package openai_compat

import (
	"net/http"
	"net/url"
	"strings"

	"go.uber.org/zap"

	"iop/packages/go/config"
)

const Name = "openai_compat"

// Adapter is the OpenAI-compatible adapter. It carries an explicit provider
// label, per-request auth/header injection and top-level option passthrough so
// a provider boundary and its config contract stay distinct.
type Adapter struct {
	instanceName     string
	provider         string
	endpoint         string
	headers          map[string]string
	capacity         int
	maxQueue         int
	queueTimeoutMS   int
	requestTimeoutMS int
	client           *http.Client
	logger           *zap.Logger
	// profile is the resolved concrete protocol profile snapshot. When non-nil,
	// the adapter resolves operation URLs from the profile's operation paths
	// instead of the legacy endpoint + /v1 suffix heuristic.
	profile *config.ConcreteProtocolProfile
}

// New creates an OpenAI-compatible adapter. The optional instanceName is the
// registry instance key used to disambiguate multiple instances on a node.
func New(cfg config.OpenAICompatConf, logger *zap.Logger, instanceName ...string) *Adapter {
	endpoint := strings.TrimRight(cfg.Endpoint, "/")
	name := Name
	if len(instanceName) > 0 && instanceName[0] != "" {
		name = instanceName[0]
	}
	var headers map[string]string
	if len(cfg.Headers) > 0 {
		headers = make(map[string]string, len(cfg.Headers))
		for k, v := range cfg.Headers {
			headers[k] = v
		}
	}
	var profile *config.ConcreteProtocolProfile
	if cfg.RuntimeProfile != nil {
		cloned := cfg.RuntimeProfile.Clone()
		profile = &cloned
	}
	return &Adapter{
		instanceName:     name,
		provider:         cfg.Provider,
		endpoint:         endpoint,
		headers:          headers,
		capacity:         cfg.Capacity,
		maxQueue:         cfg.MaxQueue,
		queueTimeoutMS:   cfg.QueueTimeoutMS,
		requestTimeoutMS: cfg.RequestTimeoutMS,
		client:           &http.Client{},
		logger:           logger,
		profile:          profile,
	}
}

func (a *Adapter) Name() string { return Name }

// operationURL resolves an operation id to its full URL. When the adapter has
// a concrete profile, the profile's operation path is used. When no profile
// is present (legacy payload), the endpoint is used with the legacy path
// appended.
func (a *Adapter) operationURL(op string, legacyPath string) (string, error) {
	if a.profile != nil {
		return a.profile.ResolveOperationURL(op)
	}
	if a.endpoint == "" {
		return "", errEndpointRequired
	}
	return resolveLegacyURL(a.endpoint, legacyPath), nil
}

// resolveLegacyURL appends a legacy path to the endpoint, stripping a
// duplicate /v1 suffix to avoid /v1/v1.
func resolveLegacyURL(endpoint, path string) string {
	u, err := url.Parse(endpoint)
	if err != nil {
		return strings.TrimRight(endpoint, "/") + path
	}
	basePath := strings.TrimRight(u.Path, "/")
	if strings.HasSuffix(basePath, "/v1") && strings.HasPrefix(path, "/v1/") {
		path = strings.TrimPrefix(path, "/v1")
	}
	u.Path = basePath + path
	return u.String()
}

var errEndpointRequired = fmtError("openai_compat adapter: endpoint is required")

// fmtError is a small helper to avoid importing fmt just for error creation
// in the legacy fallback path.
func fmtError(msg string) error {
	return &adapterError{msg: msg}
}

type adapterError struct{ msg string }

func (e *adapterError) Error() string { return e.msg }
