package action

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

// AssumeRoleInput is the input to AssumeRoleActivity
type AssumeRoleInput struct {
	// Identity is the user/service identity requesting access
	Identity string `json:"identity"`
	
	// ClientID is the OAuth2/OIDC client ID (from vault or env)
	ClientID string `json:"clientId,omitempty"`
	
	// ClientSecret is the OAuth2/OIDC client secret (from vault or env)
	ClientSecret string `json:"clientSecret,omitempty"`
	
	// Scope defines what APIs this token can access (e.g., "llm:read llm:write")
	Scope string `json:"scope"`
	
	// DurationSeconds is how long the token is valid (default: 3600 = 1 hour)
	DurationSeconds int `json:"durationSeconds,omitempty"`
	
	// AuthServerURL is the auth server endpoint (from env if not provided)
	AuthServerURL string `json:"authServerUrl,omitempty"`
}

// AssumeRoleOutput is the output from AssumeRoleActivity
type AssumeRoleOutput struct {
	// Token is the JWT token for calling api.riotpiao.com
	Token string `json:"token"`
	
	// ExpiresAt is when the token expires (Unix timestamp)
	ExpiresAt int64 `json:"expiresAt"`
	
	// ExpiresIn is the duration in seconds until expiration
	ExpiresIn int `json:"expiresIn"`
	
	// TokenType is typically "Bearer"
	TokenType string `json:"tokenType"`
}

// oauthTokenRequest is sent to the auth server
type oauthTokenRequest struct {
	GrantType    string `json:"grant_type"`
	ClientID     string `json:"client_id"`
	ClientSecret string `json:"client_secret"`
	Scope        string `json:"scope"`
	Subject      string `json:"subject,omitempty"` // The identity being assumed
}

// oauthTokenResponse is returned from the auth server
type oauthTokenResponse struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	ExpiresIn   int    `json:"expires_in"`
	Scope       string `json:"scope"`
}

// AssumeRoleActivity requests a temporary JWT token for accessing LLM APIs
//
// This activity works like AWS AssumeRole:
// 1. User provides identity + scope of access needed
// 2. Activity exchanges credentials with auth server
// 3. Returns JWT token valid for a limited time
// 4. Caller uses token in subsequent LLM API calls
//
// Security: Credentials should come from vault/secrets, never hardcoded
func AssumeRoleActivity(ctx context.Context, input *AssumeRoleInput) (*AssumeRoleOutput, error) {
	// Validate inputs
	if err := validateAssumeRoleInput(input); err != nil {
		return nil, err
	}

	// Resolve configuration from input + environment
	config, err := resolveAssumeRoleConfig(input)
	if err != nil {
		return nil, err
	}

	// Request token from auth server
	tokenResp, err := requestAuthToken(ctx, config, input)
	if err != nil {
		return nil, err
	}

	// Build output
	return buildAssumeRoleOutput(tokenResp), nil
}

// validateAssumeRoleInput checks required fields
func validateAssumeRoleInput(input *AssumeRoleInput) error {
	if input.Identity == "" {
		return fmt.Errorf("identity is required")
	}
	if input.Scope == "" {
		return fmt.Errorf("scope is required (e.g., 'llm:read' or 'llm:read llm:write')")
	}
	return nil
}

// assumeRoleConfig holds resolved configuration
type assumeRoleConfig struct {
	authServerURL string
	clientID      string
	clientSecret  string
	duration      int
}

// resolveAssumeRoleConfig gets config from input or environment
func resolveAssumeRoleConfig(input *AssumeRoleInput) (*assumeRoleConfig, error) {
	cfg := &assumeRoleConfig{}

	// Helper function to avoid DRY violation
	getOrEnv := func(val, envKey, fieldName string) (string, error) {
		if val != "" {
			return val, nil
		}
		if val = os.Getenv(envKey); val != "" {
			return val, nil
		}
		return "", fmt.Errorf("%s not provided and %s not set", fieldName, envKey)
	}

	var err error
	if cfg.authServerURL, err = getOrEnv(input.AuthServerURL, "AUTH_SERVER_URL", "authServerUrl"); err != nil {
		return nil, err
	}
	if cfg.clientID, err = getOrEnv(input.ClientID, "OAUTH_CLIENT_ID", "clientId"); err != nil {
		return nil, err
	}
	if cfg.clientSecret, err = getOrEnv(input.ClientSecret, "OAUTH_CLIENT_SECRET", "clientSecret"); err != nil {
		return nil, err
	}

	// Validate and set duration
	cfg.duration = input.DurationSeconds
	if cfg.duration == 0 {
		cfg.duration = 3600 // 1 hour default
	}
	if cfg.duration > 86400 {
		cfg.duration = 86400 // Max 24 hours
	}

	return cfg, nil
}

// requestAuthToken calls the auth server and returns the token response
func requestAuthToken(ctx context.Context, config *assumeRoleConfig, input *AssumeRoleInput) (*oauthTokenResponse, error) {
	tokenReq := oauthTokenRequest{
		GrantType:    "client_credentials",
		ClientID:     config.clientID,
		ClientSecret: config.clientSecret,
		Scope:        input.Scope,
		Subject:      input.Identity,
	}

	reqBody, err := json.Marshal(tokenReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal token request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, "POST",
		fmt.Sprintf("%s/oauth/token", config.authServerURL),
		bytes.NewReader(reqBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	httpReq.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("failed to call auth server: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("auth server returned status %d: %s", resp.StatusCode, string(respBody))
	}

	var tokenResp oauthTokenResponse
	if err := json.Unmarshal(respBody, &tokenResp); err != nil {
		return nil, fmt.Errorf("failed to unmarshal token response: %w", err)
	}

	if tokenResp.AccessToken == "" {
		return nil, fmt.Errorf("auth server returned empty access token")
	}

	return &tokenResp, nil
}

// buildAssumeRoleOutput constructs the output from token response
func buildAssumeRoleOutput(tokenResp *oauthTokenResponse) *AssumeRoleOutput {
	expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Unix()
	return &AssumeRoleOutput{
		Token:     tokenResp.AccessToken,
		ExpiresAt: expiresAt,
		ExpiresIn: tokenResp.ExpiresIn,
		TokenType: tokenResp.TokenType,
	}
}
