// Package alibaba provides the Alibaba Cloud SSO provider implementation.
// It implements the provider.Provider interface for managing Alibaba Cloud SSO permissions.
//
// Requirements: 3.2, 3.3, 3.4, 3.6
package alibaba

import (
	"context"
	"fmt"
	"strings"
	"sync"

	cloudsso20210515 "github.com/alibabacloud-go/cloudsso-20210515/client"
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
	util "github.com/alibabacloud-go/tea-utils/v2/service"
	"github.com/alibabacloud-go/tea/tea"
	credential "github.com/aliyun/credentials-go/credentials"

	"AutoSSO/internal/config"
	apperrors "AutoSSO/internal/pkg/errors"
	"AutoSSO/internal/pkg/logger"
	"AutoSSO/internal/provider"
)

// AlibabaProvider implements the provider.Provider interface for Alibaba Cloud SSO.
// It manages permissions through Alibaba Cloud SSO (Cloud Single Sign-On).
//
// Requirements: 3.4, 3.6
type AlibabaProvider struct {
	// config holds the Alibaba Cloud configuration
	config *config.AlibabaConfig

	// client is the Alibaba Cloud SSO API client
	client *cloudsso20210515.Client

	// accessConfigCache caches access configuration name to ID mappings
	accessConfigCache map[string]string

	// userCache caches user email to user ID mappings
	userCache map[string]string

	// cacheMu protects the caches
	cacheMu sync.RWMutex

	// closed indicates if the provider has been closed
	closed bool

	// closeMu protects the closed flag
	closeMu sync.RWMutex

	// logger is the logger instance
	logger *logger.Logger
}

// NewAlibabaProvider creates a new Alibaba Cloud provider instance from the configuration.
// It initializes the Alibaba Cloud SDK client with the provided credentials.
// Supports multiple authentication types:
//   - ecs_ram_role: ECS instance RAM role authentication (default)
//   - ecs_ram_role: ECS instance RAM role authentication
//   - ram_role_arn: RAM role ARN with STS token
//   - oidc_role_arn: OIDC-based role authentication (for Kubernetes)
//
// Parameters:
//   - cfg: The Alibaba Cloud configuration containing authentication settings
//
// Returns:
//   - *AlibabaProvider: The initialized Alibaba Cloud provider
//   - error: Any error that occurred during initialization
//
// Requirements: 3.4, 3.6
func NewAlibabaProvider(cfg *config.AlibabaConfig) (*AlibabaProvider, error) {
	if cfg == nil {
		return nil, fmt.Errorf("Alibaba config cannot be nil")
	}

	if cfg.DirectoryID == "" {
		return nil, fmt.Errorf("Alibaba DirectoryID is required")
	}

	// Determine auth type (default to ecs_ram_role)
	authType := cfg.AuthType
	if authType == "" {
		authType = "ecs_ram_role"
	}

	// Create credential based on auth type
	var cred credential.Credential
	var err error

	switch authType {
	case "ecs_ram_role":
		// ECS instance RAM role authentication
		if cfg.RoleName == "" {
			return nil, fmt.Errorf("Alibaba RoleName is required for ecs_ram_role auth type")
		}

		cred, err = credential.NewCredential(&credential.Config{
			Type:     tea.String("ecs_ram_role"),
			RoleName: tea.String(cfg.RoleName),
		})

	case "ram_role_arn":
		// RAM role ARN with STS AssumeRole (supports cross-account access)
		// Flow: ECS Role credentials → AssumeRole → Target Role in another account
		if cfg.RoleArn == "" {
			return nil, fmt.Errorf("Alibaba RoleArn is required for ram_role_arn auth type")
		}

		sessionName := cfg.RoleSessionName
		if sessionName == "" {
			sessionName = "autosso-session"
		}

		// Check if we need to use ECS role as source credential (role chaining)
		if cfg.RoleName != "" {
			// Role Chaining: ECS Role → Target Role (cross-account)
			// Step 1: Get ECS role credentials first
			ecsCredConfig := &credential.Config{
				Type:     tea.String("ecs_ram_role"),
				RoleName: tea.String(cfg.RoleName),
			}
			ecsCred, ecsErr := credential.NewCredential(ecsCredConfig)
			if ecsErr != nil {
				return nil, fmt.Errorf("failed to create ECS role credential for role chaining: %w", ecsErr)
			}

			// Step 2: Get the temporary credentials from ECS role
			ecsCredential, getErr := ecsCred.GetCredential()
			if getErr != nil {
				return nil, fmt.Errorf("failed to get ECS role credential: %w", getErr)
			}

			// Step 3: Use ECS role's temporary credentials to AssumeRole into target role
			credConfig := &credential.Config{
				Type:            tea.String("ram_role_arn"),
				AccessKeyId:     ecsCredential.AccessKeyId,
				AccessKeySecret: ecsCredential.AccessKeySecret,
				SecurityToken:   ecsCredential.SecurityToken,
				RoleArn:         tea.String(cfg.RoleArn),
				RoleSessionName: tea.String(sessionName),
			}
			cred, err = credential.NewCredential(credConfig)
		} else {
			return nil, fmt.Errorf("ram_role_arn auth type requires role_name (ECS role name for role chaining)")
		}

	case "oidc_role_arn":
		// OIDC-based role authentication (for Kubernetes)
		if cfg.RoleArn == "" {
			return nil, fmt.Errorf("Alibaba RoleArn is required for oidc_role_arn auth type")
		}
		if cfg.OIDCProviderArn == "" {
			return nil, fmt.Errorf("Alibaba OIDCProviderArn is required for oidc_role_arn auth type")
		}
		if cfg.OIDCTokenFile == "" {
			return nil, fmt.Errorf("Alibaba OIDCTokenFile is required for oidc_role_arn auth type")
		}

		sessionName := cfg.RoleSessionName
		if sessionName == "" {
			sessionName = "autosso-session"
		}

		cred, err = credential.NewCredential(&credential.Config{
			Type:              tea.String("oidc_role_arn"),
			RoleArn:           tea.String(cfg.RoleArn),
			OIDCProviderArn:   tea.String(cfg.OIDCProviderArn),
			OIDCTokenFilePath: tea.String(cfg.OIDCTokenFile),
			RoleSessionName:   tea.String(sessionName),
		})

	default:
		return nil, fmt.Errorf("unsupported Alibaba auth type: %s (supported: ecs_ram_role, ram_role_arn, oidc_role_arn)", authType)
	}

	if err != nil {
		return nil, fmt.Errorf("failed to create Alibaba credential: %w", err)
	}

	// Determine endpoint
	endpoint := cfg.Endpoint
	if endpoint == "" {
		endpoint = "cloudsso.cn-shanghai.aliyuncs.com"
	}

	// Extract region from endpoint (e.g. cloudsso.cn-shanghai.aliyuncs.com -> cn-shanghai)
	var regionID string
	if strings.Contains(endpoint, "cloudsso.") && strings.Contains(endpoint, ".aliyuncs.com") {
		parts := strings.Split(endpoint, ".")
		if len(parts) >= 4 {
			regionID = parts[1]
		}
	}
	if regionID == "" {
		// Fallback or default
		regionID = "cn-shanghai"
	}

	// Create OpenAPI config
	openAPIConfig := &openapi.Config{
		Credential: cred,
		Endpoint:   tea.String(endpoint),
		RegionId:   tea.String(regionID),
	}

	// Create Cloud SSO client
	client, err := cloudsso20210515.NewClient(openAPIConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to create Alibaba Cloud SSO client: %w", err)
	}

	return &AlibabaProvider{
		config:            cfg,
		client:            client,
		accessConfigCache: make(map[string]string),
		userCache:         make(map[string]string),
		logger:            logger.NewLogger(logger.DefaultConfig()).WithModule("alibaba-provider"),
	}, nil
}

// WithLogger sets the logger for the provider.
func (p *AlibabaProvider) WithLogger(log *logger.Logger) *AlibabaProvider {
	p.logger = log.WithModule("alibaba-provider")
	return p
}

func (p *AlibabaProvider) wrapProviderError(op string, err error) error {
	if err == nil {
		return nil
	}

	message := err.Error()
	if strings.Contains(message, "InvalidServiceStatus") || strings.Contains(message, "Cloud SSO is not activated") {
		err = fmt.Errorf("Alibaba Cloud SSO is not activated for directory %s; enable Cloud SSO before retrying: %w", p.config.DirectoryID, err)
	}

	return apperrors.NewProviderError(string(provider.ProviderAlibaba), op, err)
}

// Name returns the provider name.
//
// Returns:
//   - string: "Alibaba"
func (p *AlibabaProvider) Name() string {
	return string(provider.ProviderAlibaba)
}

// ValidateCredentials validates that the Alibaba Cloud credentials are valid
// by attempting to list access configurations.
//
// Parameters:
//   - ctx: Context for cancellation and timeout
//
// Returns:
//   - error: Any error if credentials are invalid or authentication fails
func (p *AlibabaProvider) ValidateCredentials(ctx context.Context) error {
	p.closeMu.RLock()
	if p.closed {
		p.closeMu.RUnlock()
		return fmt.Errorf("provider is closed")
	}
	p.closeMu.RUnlock()

	// Try to list access configurations to validate credentials
	req := &cloudsso20210515.ListAccessConfigurationsRequest{
		DirectoryId: tea.String(p.config.DirectoryID),
		MaxResults:  tea.Int32(1),
	}

	_, err := p.client.ListAccessConfigurationsWithOptions(req, &util.RuntimeOptions{})
	if err != nil {
		return p.wrapProviderError("ValidateCredentials", fmt.Errorf("failed to validate Alibaba credentials: %w", err))
	}

	return nil
}

// Close releases any resources held by the provider.
//
// Returns:
//   - error: Any error that occurred during cleanup
func (p *AlibabaProvider) Close() error {
	p.closeMu.Lock()
	defer p.closeMu.Unlock()

	if p.closed {
		return nil
	}

	p.closed = true

	// Clear the caches
	p.cacheMu.Lock()
	p.accessConfigCache = make(map[string]string)
	p.userCache = make(map[string]string)
	p.cacheMu.Unlock()

	return nil
}

// getAccessConfigurationID retrieves the access configuration ID for a given permission name.
// It uses a cache to avoid repeated API calls.
//
// Parameters:
//   - ctx: Context for cancellation and timeout
//   - permissionName: The name of the access configuration (permission)
//
// Returns:
//   - string: The access configuration ID
//   - error: Any error that occurred during the lookup
func (p *AlibabaProvider) getAccessConfigurationID(ctx context.Context, permissionName string) (string, error) {
	// Check cache first
	p.cacheMu.RLock()
	if id, exists := p.accessConfigCache[permissionName]; exists {
		p.cacheMu.RUnlock()
		return id, nil
	}
	p.cacheMu.RUnlock()

	// List all access configurations and find the matching one
	req := &cloudsso20210515.ListAccessConfigurationsRequest{
		DirectoryId: tea.String(p.config.DirectoryID),
	}

	resp, err := p.client.ListAccessConfigurationsWithOptions(req, &util.RuntimeOptions{})
	if err != nil {
		return "", p.wrapProviderError("GetAccessConfigurationID", fmt.Errorf("failed to list access configurations: %w", err))
	}

	if resp.Body == nil || resp.Body.AccessConfigurations == nil {
		return "", fmt.Errorf("no access configurations found")
	}

	for _, cfg := range resp.Body.AccessConfigurations {
		name := tea.StringValue(cfg.AccessConfigurationName)
		id := tea.StringValue(cfg.AccessConfigurationId)

		// Cache all configurations we find
		p.cacheMu.Lock()
		p.accessConfigCache[name] = id
		p.cacheMu.Unlock()

		if name == permissionName {
			return id, nil
		}
	}

	return "", fmt.Errorf("access configuration not found: %s", permissionName)
}

// GetUserID retrieves the Alibaba Cloud SSO user ID for a given email address.
// It searches through the Cloud SSO directory to find the user.
//
// Parameters:
//   - ctx: Context for cancellation and timeout
//   - email: User's email address (used as UserName in Alibaba Cloud SSO)
//
// Returns:
//   - string: The Alibaba Cloud SSO user ID
//   - error: Any error that occurred during the lookup
func (p *AlibabaProvider) GetUserID(ctx context.Context, email string) (string, error) {
	p.closeMu.RLock()
	if p.closed {
		p.closeMu.RUnlock()
		return "", fmt.Errorf("provider is closed")
	}
	p.closeMu.RUnlock()

	if email == "" {
		return "", fmt.Errorf("email is required")
	}

	// Normalize the email
	email = strings.TrimSpace(email)

	// Check cache first
	p.cacheMu.RLock()
	if id, exists := p.userCache[email]; exists {
		p.cacheMu.RUnlock()
		return id, nil
	}
	p.cacheMu.RUnlock()

	// List users with pagination to find the matching one
	var nextToken *string = nil

	for {
		req := &cloudsso20210515.ListUsersRequest{
			DirectoryId: tea.String(p.config.DirectoryID),
			MaxResults:  tea.Int32(100),
			NextToken:   nextToken,
		}

		resp, err := p.client.ListUsersWithOptions(req, &util.RuntimeOptions{})
		if err != nil {
			return "", p.wrapProviderError("GetUserID", fmt.Errorf("failed to list users: %w", err))
		}

		if resp.Body == nil || resp.Body.Users == nil {
			break
		}

		for _, u := range resp.Body.Users {
			userName := tea.StringValue(u.UserName)
			userID := tea.StringValue(u.UserId)
			userEmail := tea.StringValue(u.Email)

			// Cache all users we find (both UserName and Email for faster lookups)
			p.cacheMu.Lock()
			p.userCache[userName] = userID
			if userEmail != "" {
				p.userCache[userEmail] = userID
			}
			p.cacheMu.Unlock()

			// Match against both UserName and Email fields (case-insensitive)
			// This handles users synced from external IdPs where UserName may differ from Email
			if strings.EqualFold(userName, email) || strings.EqualFold(userEmail, email) {
				return userID, nil
			}
		}

		// Check for more results
		if resp.Body.NextToken == nil || tea.StringValue(resp.Body.NextToken) == "" {
			break
		}
		nextToken = resp.Body.NextToken
	}

	return "", fmt.Errorf("user not found: %s", email)
}

// GetConfig returns the provider configuration.
// This is useful for testing and debugging.
//
// Returns:
//   - *config.AlibabaConfig: The provider configuration
func (p *AlibabaProvider) GetConfig() *config.AlibabaConfig {
	return p.config
}
