package backendclient

import (
	"errors"
	"fmt"
	"net/url"
	"strings"
	"time"
)

// Config describes the backend endpoint and gateway client identity used
// to construct a Client. All fields are required when the gateway is
// expected to talk to a real backend; the empty value yields an
// always-unavailable client.
type Config struct {
	// HTTPBaseURL is the absolute base URL of the backend HTTP listener
	// (`/api/v1/{public,user,internal}/*`). Required.
	HTTPBaseURL string

	// GRPCPushURL is the dial target of the backend `Push.SubscribePush`
	// listener (`host:port`). Required.
	GRPCPushURL string

	// GatewayClientID is the durable identifier this gateway instance
	// presents to backend in `GatewaySubscribeRequest.gateway_client_id`.
	// Required.
	GatewayClientID string

	// HTTPTimeout bounds individual REST calls. Must be positive.
	HTTPTimeout time.Duration

	// PushReconnectBaseBackoff is the starting delay between reconnect
	// attempts of `Push.SubscribePush`. Must be positive.
	PushReconnectBaseBackoff time.Duration

	// PushReconnectMaxBackoff is the upper bound for exponential
	// reconnect delays. Must be greater than or equal to
	// PushReconnectBaseBackoff.
	PushReconnectMaxBackoff time.Duration
}

// Validate reports a formatted error when cfg is missing required
// values. The empty value is invalid; callers that intentionally omit
// the backend may bypass this check by skipping NewClient entirely.
func (cfg Config) Validate() error {
	trimmed := strings.TrimSpace(cfg.HTTPBaseURL)
	if trimmed == "" {
		return errors.New("backendclient: HTTPBaseURL must not be empty")
	}
	parsed, err := url.Parse(strings.TrimRight(trimmed, "/"))
	if err != nil {
		return fmt.Errorf("backendclient: parse HTTPBaseURL: %w", err)
	}
	if parsed.Scheme == "" || parsed.Host == "" {
		return errors.New("backendclient: HTTPBaseURL must be absolute")
	}
	if strings.TrimSpace(cfg.GRPCPushURL) == "" {
		return errors.New("backendclient: GRPCPushURL must not be empty")
	}
	if strings.TrimSpace(cfg.GatewayClientID) == "" {
		return errors.New("backendclient: GatewayClientID must not be empty")
	}
	if cfg.HTTPTimeout <= 0 {
		return errors.New("backendclient: HTTPTimeout must be positive")
	}
	if cfg.PushReconnectBaseBackoff <= 0 {
		return errors.New("backendclient: PushReconnectBaseBackoff must be positive")
	}
	if cfg.PushReconnectMaxBackoff < cfg.PushReconnectBaseBackoff {
		return errors.New("backendclient: PushReconnectMaxBackoff must be >= PushReconnectBaseBackoff")
	}
	return nil
}

// Client aggregates the REST and gRPC adapters that talk to backend.
// One value is shared across the gateway process; all methods are safe
// for concurrent use.
type Client struct {
	rest *RESTClient
	push *PushClient
}

// NewClient constructs a Client that targets the configured backend.
// REST adapter is always built. The gRPC push adapter is built lazily
// when StartPush is called so unit tests can construct a Client with a
// stubbed push transport.
func NewClient(cfg Config) (*Client, error) {
	if err := cfg.Validate(); err != nil {
		return nil, err
	}
	rest, err := NewRESTClient(cfg)
	if err != nil {
		return nil, err
	}
	push, err := NewPushClient(cfg)
	if err != nil {
		return nil, err
	}
	return &Client{rest: rest, push: push}, nil
}

// REST returns the REST adapter. The returned value is nil when the
// Client was constructed without a backend; callers must guard.
func (c *Client) REST() *RESTClient {
	if c == nil {
		return nil
	}
	return c.rest
}

// Push returns the gRPC push adapter. The returned value is nil when
// the Client was constructed without a backend.
func (c *Client) Push() *PushClient {
	if c == nil {
		return nil
	}
	return c.push
}

// Close releases idle HTTP connections and closes the gRPC push
// connection. Safe to call multiple times.
func (c *Client) Close() error {
	if c == nil {
		return nil
	}
	var firstErr error
	if c.rest != nil {
		if err := c.rest.Close(); err != nil {
			firstErr = err
		}
	}
	if c.push != nil {
		if err := c.push.Close(); err != nil && firstErr == nil {
			firstErr = err
		}
	}
	return firstErr
}
