// Package agentrouter is the hand-written Go SDK for the Tetrate Agent
// Router (TARS) control plane.
//
// The SDK wraps the Connect-Go clients generated under
// github.com/tetrateio/fraser/api/tars/<svc>/v1/<svc>v1connect with idiomatic
// Go ergonomics: functional options, context-first signatures, transparent
// auth injection, retries.
//
// Quickstart:
//
//	c, err := agentrouter.New(ctx,
//	    agentrouter.WithBaseURL(os.Getenv("AGENTROUTER_BASE_URL")),
//	    agentrouter.WithSessionToken(os.Getenv("AGENTROUTER_SESSION_TOKEN")),
//	)
//	providers, err := c.Catalog().ListProviders(ctx)
package agentrouter

import (
	"context"
	"errors"
	"net/http"
	"os"
	"strings"
	"time"

	"connectrpc.com/connect"

	"github.com/tetrateio/agentrouter-go/genapi/api/tars/aikeys/v1/aikeysv1connect"
	"github.com/tetrateio/agentrouter-go/genapi/api/tars/catalog/v1/catalogv1connect"
	"github.com/tetrateio/agentrouter-go/genapi/api/tars/clients/v1/clientsv1connect"
	"github.com/tetrateio/agentrouter-go/genapi/api/tars/identity/v1/identityv1connect"
	"github.com/tetrateio/agentrouter-go/genapi/api/tars/inference/v1/inferencev1connect"
	"github.com/tetrateio/agentrouter-go/genapi/api/tars/mcp/v1/mcpv1connect"
	"github.com/tetrateio/agentrouter-go/genapi/api/tars/telemetry/v1/telemetryv1connect"
	"github.com/tetrateio/agentrouter-go/genapi/api/tars/tenancy/v1/tenancyv1connect"
)

// Client is the top-level Tetrate Agent Router SDK handle. It is safe for
// concurrent use by multiple goroutines.
type Client struct {
	cfg config

	// Generated Connect-Go clients (memoized at construction).
	me         identityv1connect.MeServiceClient
	identity   identityv1connect.IdentityServiceClient
	tenancy    tenancyv1connect.TenancyServiceClient
	projects   tenancyv1connect.ProjectsServiceClient
	catalog    catalogv1connect.CatalogServiceClient
	aikeys     aikeysv1connect.AiKeysServiceClient
	inference  inferencev1connect.InferenceIngressServiceClient
	clients    clientsv1connect.ClientsServiceClient
	telemetry  telemetryv1connect.TelemetryServiceClient
	mcpProfile mcpv1connect.MCPProfilesServiceClient
	mcpCatalog mcpv1connect.MCPCatalogServiceClient
}

// Endpoints lets a caller route each service to a separate URL. Useful for
// local development where each service binds a different port. Production
// behind a single facade leaves this empty and uses WithBaseURL.
//
// Per-service URLs override the base URL when non-empty; empty fields fall
// through to WithBaseURL.
type Endpoints struct {
	Identity  string // identity-svc -- IdentityService + MeService
	Tenancy   string // tenancy-svc -- TenancyService + ProjectsService
	Catalog   string // catalog-svc -- CatalogService
	AIKeys    string // aikeys-svc -- AiKeysService
	Inference string // inference-ingress -- InferenceIngressService
	Clients   string // clients-svc -- ClientsService
	Telemetry string // telemetry-svc -- TelemetryService
	MCP       string // mcp-svc -- MCPProfilesService + MCPCatalogService
}

// config carries the resolved options. Internal; built by the WithX options.
type config struct {
	baseURL      string
	endpoints    Endpoints
	apiKey       string
	sessionToken string
	httpClient   *http.Client
	retry        RetryPolicy
	userAgent    string
}

// Option configures a Client.
type Option func(*config)

// WithBaseURL sets the API base URL (e.g. "https://router.example.com").
// Per-service routes from WithEndpoints take precedence when set.
func WithBaseURL(u string) Option {
	return func(c *config) { c.baseURL = strings.TrimRight(u, "/") }
}

// StandardBaseURLEnv is the environment variable read by [defaultBaseURL].
const StandardBaseURLEnv = "AGENTROUTER_BASE_URL"

// defaultBaseURL returns the base URL used when WithBaseURL / WithEndpoints
// is not supplied. Honors AGENTROUTER_BASE_URL for dev/staging overrides.
func defaultBaseURL() string {
	if u := strings.TrimRight(os.Getenv(StandardBaseURLEnv), "/"); u != "" {
		return u
	}
	return "https://api.tetrate.ai"
}

// WithEndpoints overrides per-service URLs. Empty fields fall through to
// WithBaseURL.
func WithEndpoints(e Endpoints) Option {
	return func(c *config) {
		trim := func(s string) string { return strings.TrimRight(s, "/") }
		if e.Identity != "" {
			c.endpoints.Identity = trim(e.Identity)
		}
		if e.Tenancy != "" {
			c.endpoints.Tenancy = trim(e.Tenancy)
		}
		if e.Catalog != "" {
			c.endpoints.Catalog = trim(e.Catalog)
		}
		if e.AIKeys != "" {
			c.endpoints.AIKeys = trim(e.AIKeys)
		}
		if e.Inference != "" {
			c.endpoints.Inference = trim(e.Inference)
		}
		if e.Clients != "" {
			c.endpoints.Clients = trim(e.Clients)
		}
		if e.Telemetry != "" {
			c.endpoints.Telemetry = trim(e.Telemetry)
		}
		if e.MCP != "" {
			c.endpoints.MCP = trim(e.MCP)
		}
	}
}

// WithAPIKey authenticates with a long-lived API key.
func WithAPIKey(k string) Option {
	return func(c *config) { c.apiKey = k }
}

// StandardAPIKeyEnv is the environment variable read by [WithAPIKeyFromStandardEnv].
const StandardAPIKeyEnv = "AGENTROUTER_API_KEY"

// WithAPIKeyFromEnv reads an API key from the named environment variable.
// Empty / unset variables yield an unauthenticated client.
func WithAPIKeyFromEnv(envVar string) Option {
	return func(c *config) { c.apiKey = os.Getenv(envVar) }
}

// WithAPIKeyFromStandardEnv reads the API key from AGENTROUTER_API_KEY.
func WithAPIKeyFromStandardEnv() Option {
	return WithAPIKeyFromEnv(StandardAPIKeyEnv)
}

// WithSessionToken authenticates with a session JWT.
func WithSessionToken(t string) Option {
	return func(c *config) { c.sessionToken = t }
}

// StandardSessionTokenEnv is the environment variable read by
// [WithSessionTokenFromStandardEnv].
const StandardSessionTokenEnv = "AGENTROUTER_SESSION_TOKEN"

// WithSessionTokenFromEnv reads a session token from the named env var.
func WithSessionTokenFromEnv(envVar string) Option {
	return func(c *config) { c.sessionToken = os.Getenv(envVar) }
}

// WithSessionTokenFromStandardEnv reads the session token from
// AGENTROUTER_SESSION_TOKEN.
func WithSessionTokenFromStandardEnv() Option {
	return WithSessionTokenFromEnv(StandardSessionTokenEnv)
}

// WithHTTPClient overrides the default HTTP client (custom transports,
// mTLS, proxies).
func WithHTTPClient(h *http.Client) Option {
	return func(c *config) { c.httpClient = h }
}

// WithRetryPolicy overrides the default retry policy.
func WithRetryPolicy(p RetryPolicy) Option {
	return func(c *config) { c.retry = p }
}

// WithUserAgent appends a token to the SDK's User-Agent header.
func WithUserAgent(s string) Option {
	return func(c *config) { c.userAgent = s }
}

// New constructs a Client. Supplying both API key and session token is an
// error.
func New(ctx context.Context, opts ...Option) (*Client, error) {
	cfg := config{
		baseURL:    defaultBaseURL(),
		httpClient: &http.Client{Timeout: 30 * time.Second},
		retry:      DefaultRetryPolicy,
		userAgent:  "tetrate-agentrouter-sdk-go/" + Version,
	}
	for _, o := range opts {
		o(&cfg)
	}
	if cfg.apiKey != "" && cfg.sessionToken != "" {
		return nil, errors.New("agentrouter: WithAPIKey and WithSessionToken are mutually exclusive")
	}
	c := &Client{cfg: cfg}
	c.bind()
	_ = ctx
	return c, nil
}

// bind builds the Connect-Go clients with the SDK's interceptor chain. Each
// service URL is taken from endpoints when set; otherwise it falls through
// to baseURL.
func (c *Client) bind() {
	httpc := c.cfg.httpClient
	pick := func(svc string) string {
		if svc != "" {
			return svc
		}
		return c.cfg.baseURL
	}
	opts := []connect.ClientOption{
		connect.WithInterceptors(
			authInterceptor(c.cfg),
			userAgentInterceptor(c.cfg.userAgent),
			retryInterceptor(c.cfg.retry),
		),
	}
	c.me = identityv1connect.NewMeServiceClient(httpc, pick(c.cfg.endpoints.Identity), opts...)
	c.identity = identityv1connect.NewIdentityServiceClient(httpc, pick(c.cfg.endpoints.Identity), opts...)
	c.tenancy = tenancyv1connect.NewTenancyServiceClient(httpc, pick(c.cfg.endpoints.Tenancy), opts...)
	c.projects = tenancyv1connect.NewProjectsServiceClient(httpc, pick(c.cfg.endpoints.Tenancy), opts...)
	c.catalog = catalogv1connect.NewCatalogServiceClient(httpc, pick(c.cfg.endpoints.Catalog), opts...)
	c.aikeys = aikeysv1connect.NewAiKeysServiceClient(httpc, pick(c.cfg.endpoints.AIKeys), opts...)
	c.inference = inferencev1connect.NewInferenceIngressServiceClient(httpc, pick(c.cfg.endpoints.Inference), opts...)
	c.clients = clientsv1connect.NewClientsServiceClient(httpc, pick(c.cfg.endpoints.Clients), opts...)
	c.telemetry = telemetryv1connect.NewTelemetryServiceClient(httpc, pick(c.cfg.endpoints.Telemetry), opts...)
	c.mcpProfile = mcpv1connect.NewMCPProfilesServiceClient(httpc, pick(c.cfg.endpoints.MCP), opts...)
	c.mcpCatalog = mcpv1connect.NewMCPCatalogServiceClient(httpc, pick(c.cfg.endpoints.MCP), opts...)
}

// Me returns a handle to the MeService surface (/v1/me/*).
func (c *Client) Me() *MeClient { return &MeClient{c: c.me} }

// Identity returns a handle to the IdentityService surface.
func (c *Client) Identity() *IdentityClient { return &IdentityClient{c: c.identity} }

// Customers returns a handle to TenancyService (customer-level surface).
func (c *Client) Customers() *CustomersClient { return &CustomersClient{c: c.tenancy} }

// Tenancy is an alias for Customers (matches the proto service name).
func (c *Client) Tenancy() *CustomersClient { return c.Customers() }

// Projects returns a handle to ProjectsService.
func (c *Client) Projects() *ProjectsClient { return &ProjectsClient{c: c.projects} }

// Catalog returns a handle to CatalogService (providers + models + project
// model assignment).
func (c *Client) Catalog() *CatalogClient { return &CatalogClient{c: c.catalog} }

// AIKeys returns a handle to AiKeysService (BYOK upstream credential
// management).
func (c *Client) AIKeys() *AIKeysClient { return &AIKeysClient{c: c.aikeys} }

// Inference returns a handle to the inference surface.
func (c *Client) Inference() *InferenceClient { return &InferenceClient{c: c.inference} }

// Clients returns a handle to ClientsService (downstream client API keys).
func (c *Client) Clients() *ClientsClient { return &ClientsClient{c: c.clients} }

// Telemetry returns a handle to TelemetryService (OTLP collector configs).
func (c *Client) Telemetry() *TelemetryClient { return &TelemetryClient{c: c.telemetry} }

// MCP returns a handle to the MCP surface (profiles + catalog).
func (c *Client) MCP() *MCPClient {
	return &MCPClient{
		profiles: c.mcpProfile,
		catalog:  c.mcpCatalog,
	}
}

// authInterceptor injects the configured credential. An API key sets
// Authorization: Bearer <key> (the inference ext-authz facade requires the
// Bearer form; management services accept it and also read X-Api-Key) plus
// X-Api-Key; a session token sets Authorization: Bearer ...
func authInterceptor(cfg config) connect.UnaryInterceptorFunc {
	return func(next connect.UnaryFunc) connect.UnaryFunc {
		return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
			switch {
			case cfg.apiKey != "":
				req.Header().Set("Authorization", "Bearer "+cfg.apiKey)
				req.Header().Set("X-Api-Key", cfg.apiKey)
			case cfg.sessionToken != "":
				req.Header().Set("Authorization", "Bearer "+cfg.sessionToken)
			}
			return next(ctx, req)
		}
	}
}

func userAgentInterceptor(ua string) connect.UnaryInterceptorFunc {
	return func(next connect.UnaryFunc) connect.UnaryFunc {
		return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
			req.Header().Set("User-Agent", ua)
			return next(ctx, req)
		}
	}
}
