package providers

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/wangjia/pangolin/server/internal/provision"
)

// Factory builds a CloudAdapter, reading credentials from independent secrets
// (env). It returns provision.ErrNoCredentials when the required secret is unset.
type Factory func() (provision.CloudAdapter, error)

// builtins maps api_kind → Factory. Extend here when onboarding a vendor.
var builtins = map[string]Factory{
	"vultr":   newVultrFromEnv,
	"hetzner": newHetznerFromEnv,
}

// Registry resolves provision.Provider rows to live adapters. It satisfies
// provision.AdapterFactory and caches one adapter per api_kind.
type Registry struct {
	factories map[string]Factory
	cache     map[string]provision.CloudAdapter
}

// NewRegistry returns a Registry backed by the built-in vendor factories.
func NewRegistry() *Registry {
	fs := make(map[string]Factory, len(builtins))
	for k, v := range builtins {
		fs[k] = v
	}
	return &Registry{factories: fs, cache: map[string]provision.CloudAdapter{}}
}

// Register adds or overrides a factory for api_kind (used in tests / extension).
func (r *Registry) Register(apiKind string, f Factory) { r.factories[apiKind] = f }

// For resolves the adapter for a provider row.
func (r *Registry) For(p *provision.Provider) (provision.CloudAdapter, error) {
	if a, ok := r.cache[p.APIKind]; ok {
		return a, nil
	}
	f, ok := r.factories[p.APIKind]
	if !ok {
		return nil, fmt.Errorf("providers: no adapter registered for api_kind %q", p.APIKind)
	}
	a, err := f()
	if err != nil {
		return nil, err
	}
	r.cache[p.APIKind] = a
	return a, nil
}

var _ provision.AdapterFactory = (*Registry)(nil)

// --- shared HTTP helper ---

// httpClient is a small JSON REST helper shared by the adapters.
type httpClient struct {
	base   string
	bearer string
	hc     *http.Client
}

func newHTTPClient(base, bearer string) *httpClient {
	return &httpClient{base: base, bearer: bearer, hc: &http.Client{Timeout: 30 * time.Second}}
}

// do issues an authenticated JSON request and decodes the response into out
// (out may be nil). It returns an error on any non-2xx status.
func (c *httpClient) do(ctx context.Context, method, path string, body, out any) error {
	var rdr io.Reader
	if body != nil {
		buf, err := json.Marshal(body)
		if err != nil {
			return fmt.Errorf("providers: marshal body: %w", err)
		}
		rdr = bytes.NewReader(buf)
	}
	req, err := http.NewRequestWithContext(ctx, method, c.base+path, rdr)
	if err != nil {
		return fmt.Errorf("providers: build request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+c.bearer)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	resp, err := c.hc.Do(req)
	if err != nil {
		return fmt.Errorf("providers: %s %s: %w", method, path, err)
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		// Never echo credentials; only status + vendor body (no auth header).
		return fmt.Errorf("providers: %s %s: status %d: %s", method, path, resp.StatusCode, string(data))
	}
	if out != nil && len(data) > 0 {
		if err := json.Unmarshal(data, out); err != nil {
			return fmt.Errorf("providers: decode response: %w", err)
		}
	}
	return nil
}

// secret reads an env-injected credential, returning ErrNoCredentials if unset.
func secret(env string) (string, error) {
	v := os.Getenv(env)
	if v == "" {
		return "", fmt.Errorf("%w (env %s)", provision.ErrNoCredentials, env)
	}
	return v, nil
}
