package plugin

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
)

// Source is the contract for one deployable shape (image, compose, static,
// ...). Implementations are stateless: every method receives Deps so the
// same value can serve concurrent deploys safely.
//
// A Source owns the full lifecycle of its containers — it is expected to
// reconcile rows in the containers index, register/deregister proxy
// routes via Deps.Proxy, and manage DNS via Deps.DNS. The deployer
// pipeline only chooses the right Source and feeds it a DeploymentIntent.
type Source interface {
	// Kind is the registration key (e.g. "image", "compose", "static").
	Kind() string

	// Validate type-checks a raw config blob before it is persisted.
	// Return a user-friendly error — the message is shown in the UI.
	Validate(cfg json.RawMessage) error

	// Deploy executes one deployment of w using intent. Whether this is a
	// fresh start, an update, or a no-op is the Source's call: e.g. an
	// image source short-circuits if the requested tag already runs.
	Deploy(ctx context.Context, deps Deps, w Workload, intent DeploymentIntent) error

	// Teardown removes everything Deploy created (containers, proxy
	// routes, DNS, source-specific state). Idempotent.
	Teardown(ctx context.Context, deps Deps, w Workload) error

	// Reconcile brings the containers index in sync with reality. Called
	// by the periodic reconciler — must be cheap when nothing changed.
	Reconcile(ctx context.Context, deps Deps, w Workload) error
}

var (
	sourcesMu sync.RWMutex
	sources   = map[string]Source{}
)

// RegisterSource installs s under s.Kind(). Panics on duplicate
// registration: that always indicates a bug in init() ordering, not a
// recoverable runtime condition.
func RegisterSource(s Source) {
	sourcesMu.Lock()
	defer sourcesMu.Unlock()
	k := s.Kind()
	if _, dup := sources[k]; dup {
		panic(fmt.Sprintf("plugin: source %q already registered", k))
	}
	sources[k] = s
}

// GetSource returns the Source registered for kind, or an error mentioning
// the kind that was missing — useful when a workload row references a
// kind whose package was not blank-imported.
func GetSource(kind string) (Source, error) {
	sourcesMu.RLock()
	defer sourcesMu.RUnlock()
	s, ok := sources[kind]
	if !ok {
		return nil, fmt.Errorf("plugin: no source registered for kind %q", kind)
	}
	return s, nil
}

// Schemaer is the optional interface a Source or Trigger may implement
// to surface a sample config blob. The /api/hooks/kinds/{kind}/schema
// endpoint uses this so frontends can render kind-aware forms without
// hardcoding samples per call-site. Plugins that don't implement it
// produce an empty object on the wire.
type Schemaer interface {
	SchemaSample() any
}

// SchemaSampleFor returns the typed sample value declared by the plugin
// registered under kind, or nil if no sample is published.
func SchemaSampleFor(kind string) (any, bool) {
	sourcesMu.RLock()
	if s, ok := sources[kind]; ok {
		sourcesMu.RUnlock()
		if sm, ok := s.(Schemaer); ok {
			return sm.SchemaSample(), true
		}
		return nil, true
	}
	sourcesMu.RUnlock()
	triggersMu.RLock()
	defer triggersMu.RUnlock()
	if t, ok := triggers[kind]; ok {
		if sm, ok := t.(Schemaer); ok {
			return sm.SchemaSample(), true
		}
		return nil, true
	}
	return nil, false
}

// SourceKinds returns all registered source kinds, sorted for stable
// listing in /api/workloads/source-kinds.
func SourceKinds() []string {
	sourcesMu.RLock()
	defer sourcesMu.RUnlock()
	out := make([]string, 0, len(sources))
	for k := range sources {
		out = append(out, k)
	}
	sortStrings(out)
	return out
}
