// Package gateway provides an MCP gateway toolkit that proxies tools from
// an upstream MCP server through the platform's auth, persona, and audit pipeline.
package gateway

import (
	"errors"
	"fmt"
	"time"
)

const (
	// Kind is the toolkit kind identifier. Each connection of this kind is
	// a remote MCP server that the platform's gateway feature proxies. The
	// kind value is what operators see in the connection picker; the
	// gateway terminology is reserved for the platform-side feature
	// (admin endpoints, internal package, DB tables) that does the proxying.
	Kind = "mcp"

	// AuthModeNone disables outbound authentication.
	AuthModeNone = "none"
	// AuthModeBearer sends "Authorization: Bearer <credential>" on upstream requests.
	AuthModeBearer = "bearer"
	// AuthModeAPIKey sends "X-API-Key: <credential>" on upstream requests.
	AuthModeAPIKey = "api_key"
	// AuthModeOAuth acquires a bearer token via an OAuth 2.1 grant
	// (client_credentials in v1) and refreshes it before expiry.
	AuthModeOAuth = "oauth"

	// OAuthGrantClientCredentials is the machine-to-machine grant type.
	// Requires a token URL, client id, and client secret. No user
	// interaction — the platform exchanges the credentials for a token on
	// behalf of all platform users.
	OAuthGrantClientCredentials = "client_credentials"

	// OAuthGrantAuthorizationCode is the user-driven grant. The operator
	// completes a one-time browser flow at setup; the resulting refresh
	// token is persisted (encrypted) so subsequent platform restarts
	// and background workloads keep working without further interaction.
	OAuthGrantAuthorizationCode = "authorization_code"

	// TrustLevelUntrusted is the default. Upstream responses are treated as
	// untrusted content (reserved for future enforcement).
	TrustLevelUntrusted = "untrusted"
	// TrustLevelTrusted bypasses future content sanitization. Use only for
	// first-party upstreams under the operator's control.
	TrustLevelTrusted = "trusted"

	// DefaultConnectTimeout is the default timeout for initial upstream connection + tool discovery.
	DefaultConnectTimeout = 10 * time.Second
	// DefaultCallTimeout is the default per-tool-call timeout for upstream forwarding.
	DefaultCallTimeout = 60 * time.Second

	// NamespaceSeparator joins the connection name and remote tool name (e.g. "crm__get_contact").
	NamespaceSeparator = "__"
)

// Config holds gateway toolkit configuration for a single upstream MCP connection.
type Config struct {
	// Endpoint is the streamable HTTP URL of the upstream MCP server. Required.
	Endpoint string
	// AuthMode is "none", "bearer", "api_key", or "oauth".
	AuthMode string
	// Credential is the bearer token or API key. Ignored when AuthMode is "none" or "oauth".
	Credential string
	// OAuth carries the OAuth-specific configuration used when AuthMode is "oauth".
	OAuth OAuthConfig
	// ConnectionName is the audit-visible connection identifier and also the
	// tool-name prefix. Defaults to the toolkit instance name when unset.
	ConnectionName string
	// ConnectTimeout caps the initial connection + ListTools call.
	ConnectTimeout time.Duration
	// CallTimeout caps each forwarded tool invocation.
	CallTimeout time.Duration
	// TrustLevel is "untrusted" (default) or "trusted".
	TrustLevel string
}

// OAuthConfig describes the OAuth 2.1 parameters used when AuthMode is
// "oauth". The grant determines whether the token is acquired from
// machine-to-machine credentials or via a one-time browser flow that
// hands the platform a refresh token for long-running background use.
type OAuthConfig struct {
	// Grant selects the OAuth flow. One of "client_credentials" or
	// "authorization_code".
	Grant string
	// TokenURL is the upstream's OAuth token endpoint.
	TokenURL string
	// AuthorizationURL is the upstream's authorization endpoint. Only
	// used by the authorization_code grant; the platform redirects the
	// admin's browser here to start the flow.
	AuthorizationURL string
	// ClientID is the platform's registered client id with the upstream.
	ClientID string
	// ClientSecret is the platform's registered client secret. Encrypted
	// at rest (same field-level encryption as Credential).
	ClientSecret string
	// Scope is the operator-supplied space-delimited scope string,
	// sent to the IdP verbatim. Empty scope is permitted — most IdPs
	// apply a client-default scope when none is requested. Operators
	// who need long-lived refresh tokens (refresh that survives
	// platform restarts beyond the IdP's SSO Session Idle window)
	// must add the IdP-specific offline-token scope themselves:
	// `offline_access` for Keycloak/Auth0/Okta, `refresh_token` for
	// Salesforce. The platform does NOT inject any scope automatically
	// — operators' input is the source of truth.
	Scope string
	// Prompt is an optional OIDC prompt parameter (RFC OIDC §3.1.2.1).
	// When non-empty, the platform appends ?prompt=<value> to the
	// authorize URL on every browser-side flow start. Common values:
	//
	//   "login"          — force fresh credential prompt every time
	//                      (defeats stale-Keycloak-form bugs; correct
	//                      posture for admin Reconnect flows).
	//   "consent"        — force the consent screen.
	//   "select_account" — force account picker.
	//   "none"           — silent auth; fail if interaction needed.
	//   ""  (default)    — emit no prompt parameter; let the IdP
	//                      decide. Required for non-OIDC OAuth providers
	//                      that reject unknown parameters with
	//                      invalid_request.
	//
	// Operators of strict OIDC realms (Keycloak, Auth0, Okta) typically
	// set this to "login" for connections an admin holds. Operators
	// running pure-OAuth providers should leave it empty.
	Prompt string
	// EndpointAuthStyle controls how the client credentials are
	// transmitted to the token endpoint. One of:
	//
	//   "header" (default) — HTTP Basic on the token request; the
	//                        OAuth 2.1 recommended style.
	//   "params"           — form-body parameters; required by some
	//                        legacy / strict-spec IdPs (older
	//                        Keycloak, some self-hosted authorization
	//                        servers).
	//
	// Empty defaults to "header". Mirrors the apigateway toolkit's
	// `oauth2_endpoint_auth_style`.
	EndpointAuthStyle string
}

// OAuthAuthStyle values for OAuthConfig.EndpointAuthStyle.
const (
	OAuthAuthStyleHeader = "header"
	OAuthAuthStyleParams = "params"
)

// MultiConfig holds one or more parsed per-connection gateway configs along
// with the aggregate toolkit's default connection name.
type MultiConfig struct {
	DefaultName string
	Instances   map[string]Config
}

// ParseMultiConfig validates and returns the parsed config for every
// instance. Per-instance parse errors are surfaced as fatal (operator must
// fix the config); dial/connectivity errors are handled at load time in
// NewMulti, not here.
func ParseMultiConfig(defaultName string, raw map[string]map[string]any) (MultiConfig, error) {
	parsed := make(map[string]Config, len(raw))
	for name, r := range raw {
		c, err := ParseConfig(r)
		if err != nil {
			return MultiConfig{}, fmt.Errorf("gateway/%s: %w", name, err)
		}
		if c.ConnectionName == "" {
			c.ConnectionName = name
		}
		parsed[name] = c
	}
	return MultiConfig{DefaultName: defaultName, Instances: parsed}, nil
}

// ParseConfig parses a gateway configuration from a map.
func ParseConfig(cfg map[string]any) (Config, error) {
	c := Config{
		AuthMode:       AuthModeNone,
		ConnectTimeout: DefaultConnectTimeout,
		CallTimeout:    DefaultCallTimeout,
		TrustLevel:     TrustLevelUntrusted,
	}

	c.Endpoint = getString(cfg, cfgKeyEndpoint)
	c.AuthMode = getStringDefault(cfg, cfgKeyAuthMode, c.AuthMode)
	c.Credential = getString(cfg, cfgKeyCredential)
	c.OAuth = parseOAuthConfig(cfg)
	c.ConnectionName = getString(cfg, cfgKeyConnectionName)
	c.ConnectTimeout = getDuration(cfg, cfgKeyConnectTimeout, c.ConnectTimeout)
	c.CallTimeout = getDuration(cfg, cfgKeyCallTimeout, c.CallTimeout)
	c.TrustLevel = getStringDefault(cfg, cfgKeyTrustLevel, c.TrustLevel)

	if err := c.Validate(); err != nil {
		return Config{}, err
	}
	return c, nil
}

// Validate returns an error if the configuration is missing required fields
// or contains invalid values.
func (c Config) Validate() error {
	if c.Endpoint == "" {
		return errors.New("gateway: endpoint is required")
	}
	if err := c.validateAuth(); err != nil {
		return err
	}
	switch c.TrustLevel {
	case TrustLevelUntrusted, TrustLevelTrusted:
	default:
		return fmt.Errorf("gateway: invalid trust_level %q (want untrusted or trusted)", c.TrustLevel)
	}
	if c.ConnectTimeout <= 0 {
		return errors.New("gateway: connect_timeout must be positive")
	}
	if c.CallTimeout <= 0 {
		return errors.New("gateway: call_timeout must be positive")
	}
	return nil
}

// validateAuth checks the credential / OAuth shape based on AuthMode.
func (c Config) validateAuth() error {
	switch c.AuthMode {
	case AuthModeNone:
		return nil
	case AuthModeBearer, AuthModeAPIKey:
		if c.Credential == "" {
			return fmt.Errorf("gateway: credential is required when auth_mode is %q", c.AuthMode)
		}
		return nil
	case AuthModeOAuth:
		return validateOAuth(c.OAuth)
	default:
		return fmt.Errorf("gateway: invalid auth_mode %q (want none, bearer, api_key, or oauth)", c.AuthMode)
	}
}

func validateOAuth(o OAuthConfig) error {
	switch o.Grant {
	case OAuthGrantClientCredentials, OAuthGrantAuthorizationCode:
	default:
		return fmt.Errorf("gateway: oauth.grant %q not supported (want %q or %q)",
			o.Grant, OAuthGrantClientCredentials, OAuthGrantAuthorizationCode)
	}
	if o.TokenURL == "" {
		return errors.New("gateway: oauth.token_url is required")
	}
	if o.ClientID == "" {
		return errors.New("gateway: oauth.client_id is required")
	}
	if o.ClientSecret == "" {
		return errors.New("gateway: oauth.client_secret is required")
	}
	if o.Grant == OAuthGrantAuthorizationCode && o.AuthorizationURL == "" {
		return errors.New("gateway: oauth.authorization_url is required for authorization_code grant")
	}
	return nil
}

// parseOAuthConfig extracts the oauth section from the raw config map.
// The raw config nests oauth fields under an "oauth" key (preferred) or
// reads them directly from "oauth_*" prefixed top-level keys (legacy /
// flattened form for simple admin UIs).
func parseOAuthConfig(cfg map[string]any) OAuthConfig {
	var o OAuthConfig
	if nested, ok := cfg["oauth"].(map[string]any); ok {
		o = OAuthConfig{
			Grant:             getStringDefault(nested, "grant", OAuthGrantClientCredentials),
			TokenURL:          getString(nested, "token_url"),
			AuthorizationURL:  getString(nested, "authorization_url"),
			ClientID:          getString(nested, "client_id"),
			ClientSecret:      getString(nested, "client_secret"),
			Scope:             getString(nested, "scope"),
			Prompt:            getString(nested, "prompt"),
			EndpointAuthStyle: getStringDefault(nested, "endpoint_auth_style", OAuthAuthStyleHeader),
		}
	} else {
		o = OAuthConfig{
			Grant:             getStringDefault(cfg, "oauth_grant", OAuthGrantClientCredentials),
			TokenURL:          getString(cfg, "oauth_token_url"),
			AuthorizationURL:  getString(cfg, "oauth_authorization_url"),
			ClientID:          getString(cfg, "oauth_client_id"),
			ClientSecret:      getString(cfg, "oauth_client_secret"),
			Scope:             getString(cfg, "oauth_scope"),
			Prompt:            getString(cfg, "oauth_prompt"),
			EndpointAuthStyle: getStringDefault(cfg, "oauth_endpoint_auth_style", OAuthAuthStyleHeader),
		}
	}
	return o
}

func getString(cfg map[string]any, key string) string {
	if v, ok := cfg[key].(string); ok {
		return v
	}
	return ""
}

func getStringDefault(cfg map[string]any, key, defaultVal string) string {
	if v, ok := cfg[key].(string); ok && v != "" {
		return v
	}
	return defaultVal
}

func getDuration(cfg map[string]any, key string, defaultVal time.Duration) time.Duration {
	raw, ok := cfg[key]
	if !ok {
		return defaultVal
	}
	switch v := raw.(type) {
	case string:
		if d, err := time.ParseDuration(v); err == nil {
			return d
		}
	case time.Duration:
		return v
	case int:
		return time.Duration(v) * time.Second
	case int64:
		return time.Duration(v) * time.Second
	case float64:
		return time.Duration(v) * time.Second
	}
	return defaultVal
}
