//go:generate generate_permissions permissions.yaml permissions.go datadog
package datadog

import (
	"errors"
	"fmt"
	"net/url"
	"os"

	"github.com/fatih/color"
	"github.com/jedib0t/go-pretty/v6/table"

	"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
	"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
	"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)

var _ analyzers.Analyzer = (*Analyzer)(nil)

type Analyzer struct {
	Cfg *config.Config
}

func (a Analyzer) Type() analyzers.AnalyzerType {
	return analyzers.AnalyzerTypeDatadog
}

// Analyze performs the analysis of the Datadog API key and returns the analyzer result.
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
	apiKey := credInfo["api_key"]
	appKey := credInfo["app_key"]
	endpoint := credInfo["endpoint"]

	info, err := AnalyzePermissions(a.Cfg, apiKey, appKey, endpoint)
	if err != nil {
		return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationAnalyzePermissions, analyzers.ServiceAPI, "", err)
	}
	return secretInfoToAnalyzerResult(info), nil
}

func AnalyzeAndPrintPermissions(cfg *config.Config, apiKey, appKey, endpoint string) {
	info, err := AnalyzePermissions(cfg, apiKey, appKey, endpoint)
	if err != nil {
		// just print the error in cli and continue as a partial success
		color.Red("[x] Error : %s", err.Error())
	}

	if info == nil {
		color.Red("[x] No information retrieved")
		return
	}

	color.Green("[i] Valid Datadog API Key\n")
	printUser(info.User)
	printResources(info.Resources)
	printPermissions(info.Permissions)
}

// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
func AnalyzePermissions(cfg *config.Config, apiKey, appKey, endpoint string) (*SecretInfo, error) {
	if apiKey == "" {
		return nil, errors.New("api key not found in credentials info")
	}

	// create the http client
	client := analyzers.NewAnalyzeClient(cfg)

	var secretInfo = &SecretInfo{}

	var baseURL string
	var err error

	// If endpoint is provided, use it directly; otherwise detect domain
	if endpoint != "" {
		baseURL, err = url.JoinPath(endpoint, "api")
		if err != nil {
			return nil, fmt.Errorf("failed to join path: %w", err)
		}
	} else {
		baseURL, err = DetectDomain(client, apiKey, appKey)
		if err != nil {
			return nil, fmt.Errorf("[x] %v", err)
		}
	}

	if appKey == "" {
		// If no application key is provided, we can only validate the API key
		if endpoint != "" {
			// If endpoint is empty we don't need to validate again because DetectDomain would have already validated the API key against detected domain
			// But if endpoint is provided, we should validate the API key against the provided endpoint to ensure it's valid before proceeding
			isValidApiKey, err := ValidateApiKey(client, baseURL, apiKey)
			if err != nil {
				return nil, fmt.Errorf("failed to validate api key: %v", err)
			}
			if !isValidApiKey {
				return nil, errors.New("invalid api key provided")
			}
		}
		if err := CaptureApiKeyPermissions(secretInfo); err != nil {
			return nil, fmt.Errorf("failed to fetch permissions: %v", err)
		}
		return secretInfo, nil
	}

	// capture user information in secretInfo
	// If the application key is scoped, user information cannot be retrieved even if all the permissions are granted
	// This is a non-documented Endpoint and can lead to unexpected behavior in future updates
	// If user information is not retrieved, we will move ahead with the rest of the analysis and print the error
	_ = CaptureUserInformation(client, baseURL, apiKey, appKey, secretInfo)

	// capture resources in secretInfo
	if err := CaptureResources(client, baseURL, apiKey, appKey, secretInfo); err != nil {
		return nil, fmt.Errorf("failed to fetch resources: %v", err)
	}

	// capture permissions in secretInfo
	if err := CapturePermissions(client, baseURL, apiKey, appKey, secretInfo); err != nil {
		return nil, fmt.Errorf("failed to fetch permissions: %v", err)
	}

	// Capture API key permissions
	if err := CaptureApiKeyPermissions(secretInfo); err != nil {
		return nil, fmt.Errorf("failed to fetch permissions: %v", err)
	}
	return secretInfo, nil
}

// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
	if info == nil {
		return nil
	}

	result := analyzers.AnalyzerResult{
		AnalyzerType: analyzers.AnalyzerTypeDatadog,
		Metadata:     map[string]any{},
		Bindings:     make([]analyzers.Binding, 0),
	}

	// Create user resource to use as parent
	var userResource *analyzers.Resource
	if info.User.Id != "" {
		userResource = &analyzers.Resource{
			FullyQualifiedName: info.User.Id,
			Name:               info.User.Name,
			Type:               "User",
			Metadata: map[string]any{
				"email": info.User.Email,
			},
		}
	}

	permissionBindings := secretInfoPermissionsToAnalyzerPermission(info.Permissions)
	if userResource != nil && len(*permissionBindings) > 0 {
		result.Bindings = analyzers.BindAllPermissions(*userResource, *permissionBindings...)
	}
	if userResource == nil && len(*permissionBindings) > 0 {
		result.Bindings = analyzers.BindAllPermissions(analyzers.Resource{
			FullyQualifiedName: "Unknown User",
			Name:               "Unknown User",
			Type:               "User",
			Metadata:           map[string]any{},
		}, *permissionBindings...)
	}

	// Extract information from resources to create bindings
	for _, resource := range info.Resources {
		resource := secretInfoResourceToAnalyzerResource(resource)

		// Set the user resource as parent if available
		if userResource != nil {
			resource.Parent = userResource
		}

		binding := analyzers.Binding{
			Resource: *resource,
		}

		result.Bindings = append(result.Bindings, binding)
	}

	return &result
}

// secretInfoPermissionsToAnalyzerPermission translate secret info Permission to analyzer resource for binding
func secretInfoPermissionsToAnalyzerPermission(perms []Permission) *[]analyzers.Permission {
	permissions := make([]analyzers.Permission, 0, len(perms))
	for _, perm := range perms {
		permissions = append(permissions, analyzers.Permission{
			Value: perm.Title,
		})
	}
	return &permissions
}

// secretInfoResourceToAnalyzerResource translate secret info Resource to analyzer resource for binding
func secretInfoResourceToAnalyzerResource(resource Resource) *analyzers.Resource {
	analyzerRes := analyzers.Resource{
		FullyQualifiedName: resource.ID,
		Name:               resource.Name,
		Type:               resource.Type,
		Metadata:           map[string]any{},
	}

	for key, value := range resource.MetaData {
		analyzerRes.Metadata[key] = value
	}

	return &analyzerRes
}

func printUser(user User) {
	if user.Id == "" {
		color.Red("\n[x] User information not available")
		return
	}

	color.Green("\n[i] User Information:")
	userTable := table.NewWriter()
	userTable.SetOutputMirror(os.Stdout)
	userTable.AppendHeader(table.Row{"User Id", "Name", "Email"})
	userTable.AppendRow(table.Row{color.GreenString(user.Id), color.GreenString(user.Name), color.GreenString(user.Email)})
	userTable.Render()
}

func printResources(resources []Resource) {
	if len(resources) == 0 {
		color.Red("[x] No resources found")
		return
	}

	color.Green("\n[i] Resources:")
	resourceTable := table.NewWriter()
	resourceTable.SetOutputMirror(os.Stdout)
	resourceTable.AppendHeader(table.Row{"Name", "Type"})
	for _, resource := range resources {
		resourceTable.AppendRow(table.Row{
			color.GreenString(resource.Name),
			color.GreenString(resource.Type),
		})
	}
	resourceTable.Render()
}

func printPermissions(permissions []Permission) {
	if len(permissions) == 0 {
		color.Red("[x] No permissions found")
		return
	}

	color.Green("\n[i] Permissions:")
	permissionTable := table.NewWriter()
	permissionTable.SetOutputMirror(os.Stdout)
	permissionTable.AppendHeader(table.Row{"Title", "Name", "Description"})

	// Set wrapping for long descriptions
	permissionTable.SetColumnConfigs([]table.ColumnConfig{
		{Number: 3, WidthMax: 50},
	})

	for _, permission := range permissions {
		permissionTable.AppendRow(table.Row{
			color.GreenString(permission.Title),
			color.GreenString(permission.Name),
			color.GreenString(permission.Description),
		})
	}
	permissionTable.Render()
}
