// Package gcp provides the Google Cloud Platform provider implementation.
// It implements the provider.Provider interface for managing GCP IAM permissions.
//
// Requirements: 3.2, 3.3, 3.4, 3.6
package gcp

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/go-ini/ini"
	"google.golang.org/api/cloudresourcemanager/v3"
	"google.golang.org/api/option"

	"AutoSSO/internal/config"
	"AutoSSO/internal/provider"
)

var newCRMService = cloudresourcemanager.NewService

const defaultGCPInitTimeout = 10 * time.Second

// GCPProvider implements the provider.Provider interface for GCP.
// It manages permissions through Google Cloud IAM.
//
// Requirements: 3.4, 3.6
type GCPProvider struct {
	// config holds the GCP configuration
	config *config.GCPConfig

	// crmService is the Cloud Resource Manager service client
	crmService *cloudresourcemanager.Service

	// roleMappingCache caches custom role name to GCP role mappings
	roleMappingCache map[string][]string

	// cacheMu protects the role mapping cache
	cacheMu sync.RWMutex

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

	// closeMu protects the closed flag
	closeMu sync.RWMutex
}

// NewGCPProvider creates a new GCP provider instance from the configuration.
// It initializes the GCP Cloud Resource Manager client using the service account credentials.
//
// Parameters:
//   - cfg: The GCP configuration containing credentials file path and role mapping file
//
// Returns:
//   - *GCPProvider: The initialized GCP provider
//   - error: Any error that occurred during initialization
//
// Requirements: 3.4, 3.6
func NewGCPProvider(cfg *config.GCPConfig) (*GCPProvider, error) {
	if cfg == nil {
		return nil, fmt.Errorf("GCP config cannot be nil")
	}

	if cfg.CredentialsFile == "" {
		return nil, fmt.Errorf("GCP credentials file is required")
	}

	ctx, cancel := context.WithTimeout(context.Background(), defaultGCPInitTimeout)
	defer cancel()

	// Create Cloud Resource Manager service with service account credentials
	crmService, err := newCRMService(ctx, option.WithCredentialsFile(cfg.CredentialsFile))
	if err != nil {
		return nil, fmt.Errorf("failed to create Cloud Resource Manager service: %w", err)
	}

	p := &GCPProvider{
		config:           cfg,
		crmService:       crmService,
		roleMappingCache: make(map[string][]string),
	}

	// Pre-load role mappings if role mapping file is specified
	if cfg.RoleMappingFile != "" {
		if err := p.loadRoleMappings(); err != nil {
			// Only ignore file not found errors
			if os.IsNotExist(err) {
				// Log warning (via fmt since logger not init yet or generic)
				// Here we just silently continue as per design for optional file
			} else {
				// Return error for syntax errors or permission issues
				return nil, fmt.Errorf("failed to load role mappings from %s: %w", cfg.RoleMappingFile, err)
			}
		}
	}

	return p, nil
}

// Name returns the provider name.
//
// Returns:
//   - string: "GCP"
func (p *GCPProvider) Name() string {
	return string(provider.ProviderGCP)
}

// ValidateCredentials validates that the GCP credentials are valid
// by attempting to get IAM policy for a project.
//
// Parameters:
//   - ctx: Context for cancellation and timeout
//
// Returns:
//   - error: Any error if credentials are invalid or authentication fails
func (p *GCPProvider) ValidateCredentials(ctx context.Context) error {
	p.closeMu.RLock()
	if p.closed {
		p.closeMu.RUnlock()
		return fmt.Errorf("provider is closed")
	}
	p.closeMu.RUnlock()

	// If no projects are configured, we can't validate
	if len(p.config.Projects) == 0 {
		return fmt.Errorf("no GCP projects configured for validation")
	}

	// Try to get IAM policy for the first project to validate credentials
	projectID := p.config.Projects[0].ID
	req := &cloudresourcemanager.GetIamPolicyRequest{}
	_, err := p.crmService.Projects.GetIamPolicy("projects/"+projectID, req).Context(ctx).Do()
	if err != nil {
		return fmt.Errorf("failed to validate GCP credentials: %w", err)
	}

	return nil
}

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

	if p.closed {
		return nil
	}

	p.closed = true

	// Clear the cache
	p.cacheMu.Lock()
	p.roleMappingCache = make(map[string][]string)
	p.cacheMu.Unlock()

	return nil
}

// GetUserID retrieves the GCP user ID for a given email address.
// For GCP, the user ID is simply the email address itself, as GCP IAM
// uses email addresses directly for user identification.
//
// Parameters:
//   - ctx: Context for cancellation and timeout
//   - email: User's email address
//
// Returns:
//   - string: The email address (used as user ID in GCP)
//   - error: Any error that occurred during the lookup
func (p *GCPProvider) 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)

	// For GCP, the user ID is the email address itself
	return email, nil
}

// loadRoleMappings loads the role mappings from the INI file.
// The INI file maps custom role names to actual GCP IAM roles.
//
// Returns:
//   - error: Any error that occurred during loading
func (p *GCPProvider) loadRoleMappings() error {
	if p.config.RoleMappingFile == "" {
		return nil
	}

	cfg, err := ini.Load(p.config.RoleMappingFile)
	if err != nil {
		// Check if it is a path error (file not found)
		if _, ok := err.(*os.PathError); ok {
			return os.ErrNotExist
		}
		// Try stat to be sure if ini.Load wraps it differently
		if _, statErr := os.Stat(p.config.RoleMappingFile); os.IsNotExist(statErr) {
			return os.ErrNotExist
		}
		
		return fmt.Errorf("failed to load role mapping file: %w", err)
	}

	p.cacheMu.Lock()
	defer p.cacheMu.Unlock()

	// Clear existing cache
	p.roleMappingCache = make(map[string][]string)

	// Load all sections (each section is a custom role name)
	for _, section := range cfg.Sections() {
		sectionName := section.Name()
		if sectionName == "DEFAULT" {
			continue
		}

		rolesStr := section.Key("roles").String()
		rolesStr = strings.TrimSpace(rolesStr)
		rolesStr = strings.TrimLeft(rolesStr, "\n")
		rolesStr = strings.TrimRight(rolesStr, "\n")

		if rolesStr != "" {
			roles := strings.Split(rolesStr, "|")
			// Trim whitespace from each role
			for i, role := range roles {
				roles[i] = strings.TrimSpace(role)
			}
			p.roleMappingCache[sectionName] = roles
		}
	}

	return nil
}

// getRoleMappings retrieves the GCP IAM roles for a custom role name.
// It first checks the cache, then loads from the INI file if not found.
//
// Parameters:
//   - customRoleName: The custom role name to look up
//
// Returns:
//   - []string: The list of GCP IAM roles
//   - error: Any error that occurred during the lookup
func (p *GCPProvider) getRoleMappings(customRoleName string) ([]string, error) {
	// Check cache first
	p.cacheMu.RLock()
	if roles, exists := p.roleMappingCache[customRoleName]; exists {
		p.cacheMu.RUnlock()
		return roles, nil
	}
	p.cacheMu.RUnlock()

	// Try to reload the mappings
	if err := p.loadRoleMappings(); err != nil {
		return nil, fmt.Errorf("failed to load role mappings: %w", err)
	}

	// Check cache again after reload
	p.cacheMu.RLock()
	defer p.cacheMu.RUnlock()

	if roles, exists := p.roleMappingCache[customRoleName]; exists {
		return roles, nil
	}

	// If still not found, return the custom role name as-is
	// This allows direct GCP role names to be used
	return []string{customRoleName}, nil
}

// ReloadRoleMappings reloads the role mappings from the INI file.
// This can be called when the INI file is updated to refresh the cache.
//
// Returns:
//   - error: Any error that occurred during reloading
func (p *GCPProvider) ReloadRoleMappings() error {
	p.closeMu.RLock()
	if p.closed {
		p.closeMu.RUnlock()
		return fmt.Errorf("provider is closed")
	}
	p.closeMu.RUnlock()

	return p.loadRoleMappings()
}

// GetRoleMappingCache returns a copy of the current role mapping cache.
// This is useful for debugging and testing.
//
// Returns:
//   - map[string][]string: A copy of the role mapping cache
func (p *GCPProvider) GetRoleMappingCache() map[string][]string {
	p.cacheMu.RLock()
	defer p.cacheMu.RUnlock()

	cache := make(map[string][]string, len(p.roleMappingCache))
	for k, v := range p.roleMappingCache {
		roles := make([]string, len(v))
		copy(roles, v)
		cache[k] = roles
	}
	return cache
}
