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

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

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

	_ "embed"

	"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)

//go:embed scopes.json
var scopeConfigJson []byte

type Analyzer struct {
	Cfg *config.Config
}
type PermissionStatus string

const (
	StatusGranted    PermissionStatus = "Granted"
	StatusDenied     PermissionStatus = "Denied"
	StatusUnverified PermissionStatus = "Unverified"
)

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

func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
	token, exist := credInfo["token"]
	if !exist {
		return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationValidateCredentials, analyzers.ServiceConfig, "", errors.New("token not found in credentials info"))
	}

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

	return secretInfoToAnalyzerResult(info), nil
}

func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
	info, err := AnalyzePermissions(cfg, token)
	if err != nil {
		color.Red("[x] Invalid Dropbox Token\n")
		color.Red("[x] Error : %s", err.Error())
		return
	}

	if info == nil {
		color.Red("[x] Error : %s", "No information found")
		return
	}

	color.Green("[i] Valid Dropbox OAuth2 Credentials\n")
	printAccountAndPermissions(info)
}

func AnalyzePermissions(cfg *config.Config, token string) (*secretInfo, error) {
	// Dropbox API uses POST requests for all requests, so we need to use an unrestricted client
	client := analyzers.NewAnalyzeClientUnrestricted(cfg)
	scopeConfigMap, err := getScopeConfigMap()
	if err != nil {
		return nil, err
	}

	secretInfo := &secretInfo{}

	accountInfoPermission := PermissionStrings[AccountInfoRead]
	for _, perm := range PermissionStrings {
		scopeDetails := scopeConfigMap.Scopes[perm]
		status := StatusUnverified
		if perm == accountInfoPermission {
			// Account Info Read permission is always enabled
			status = StatusGranted
		}
		secretInfo.Permissions = append(secretInfo.Permissions, accountPermission{
			Name:    perm,
			Status:  status,
			Actions: scopeDetails.Actions,
		})
	}

	if err := populateAccountInfo(client, secretInfo, token); err != nil {
		return nil, err
	}

	if err := testAllPermissions(client, secretInfo, scopeConfigMap, token); err != nil {
		return nil, err
	}

	return secretInfo, nil
}

func populateAccountInfo(client *http.Client, info *secretInfo, token string) error {
	endpoint := "/2/users/get_current_account"
	body, statusCode, err := callDropboxAPIEndpoint(client, endpoint, token)
	if err != nil {
		return err
	}
	switch statusCode {
	case http.StatusOK:
		if err := json.Unmarshal([]byte(body), &info.Account); err != nil {
			return fmt.Errorf("failed to unmarshal account info: %w", err)
		}
		return nil
	default:
		return fmt.Errorf("failed to validate scope. Status %d: %s", statusCode, body)
	}
}

func testAllPermissions(client *http.Client, info *secretInfo, scopeConfigMap *scopeConfig, token string) error {
	permissionStatuses := make(map[string]PermissionStatus)

	for _, perm := range PermissionStrings {
		scopeDetails := scopeConfigMap.Scopes[perm]

		if _, ok := permissionStatuses[perm]; ok || scopeDetails.TestEndpoint == "" {
			// Skip if the scope has already been determined or has no test endpoint
			continue
		}

		if perm == PermissionStrings[Openid] {
			// The OpenID permission can be validated using the "/2/users/get_current_account" endpoint
			// If the response contains the "email" key, that implies that the "email" permission is also granted
			// Similar case for the "given_name" key and the "profile" permission
			body, statusCode, err := callDropboxAPIEndpoint(client, scopeDetails.TestEndpoint, token)
			if err != nil {
				return err
			}
			switch statusCode {
			case http.StatusOK, http.StatusConflict:
				// The endpoint responds with 409 Conflict if the openid scope
				// is granted but the email and profile scopes are not granted
				permissionStatuses[perm] = StatusGranted

				// Check for the "email" key in the response body
				if strings.Contains(body, "\"email\":") {
					permissionStatuses[PermissionStrings[Email]] = StatusGranted
				} else {
					permissionStatuses[PermissionStrings[Email]] = StatusDenied
				}

				// Check for the "given_name" key in the response body
				if strings.Contains(body, "\"given_name\":") {
					permissionStatuses[PermissionStrings[Profile]] = StatusGranted
				} else {
					permissionStatuses[PermissionStrings[Profile]] = StatusDenied
				}
			case http.StatusUnauthorized:
				permissionStatuses[perm] = StatusDenied
				permissionStatuses[PermissionStrings[Email]] = StatusDenied
				permissionStatuses[PermissionStrings[Profile]] = StatusDenied
			}
			continue
		}

		isGranted, err := testPermission(client, scopeDetails.TestEndpoint, token)
		if err != nil {
			return err
		}

		if !isGranted {
			permissionStatuses[perm] = StatusDenied
			continue
		}

		permissionStatuses[perm] = StatusGranted
		for _, impliedScope := range scopeDetails.ImpliedScopes {
			permissionStatuses[impliedScope] = StatusGranted
		}
	}

	for idx, permission := range info.Permissions {
		permission.Status = permissionStatuses[permission.Name]
		info.Permissions[idx] = permission
	}

	return nil
}

func testPermission(client *http.Client, testEndpoint string, token string) (bool, error) {
	body, statusCode, err := callDropboxAPIEndpoint(client, testEndpoint, token)
	if err != nil {
		return false, err
	}

	switch statusCode {
	case http.StatusUnauthorized:
		return false, nil
	case http.StatusBadRequest:
		if strings.Contains(body, "does not have the required scope") {
			return false, nil
		}
		if strings.Contains(body, "your request body is empty") {
			return true, nil
		}
	}
	return false, fmt.Errorf("failed to validate scope. Status %d: %s", statusCode, body)
}

func callDropboxAPIEndpoint(client *http.Client, endpoint string, token string) (string, int, error) {
	baseURL := "https://api.dropboxapi.com"
	req, err := http.NewRequest(http.MethodPost, baseURL+endpoint, nil)
	if err != nil {
		return "", 0, err
	}
	req.Header.Set("Authorization", "Bearer "+token)
	res, err := client.Do(req)
	if err != nil {
		return "", 0, err
	}

	defer func() {
		_, _ = io.Copy(io.Discard, res.Body)
		_ = res.Body.Close()
	}()

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

	return string(bodyBytes), res.StatusCode, nil
}

func getScopeConfigMap() (*scopeConfig, error) {
	var scopeConfigMap scopeConfig
	if err := json.Unmarshal(scopeConfigJson, &scopeConfigMap); err != nil {
		return nil, errors.New("failed to unmarshal scopes.json: " + err.Error())
	}
	return &scopeConfigMap, nil
}

func secretInfoToAnalyzerResult(info *secretInfo) *analyzers.AnalyzerResult {
	if info == nil {
		return nil
	}

	account := info.Account
	accountID := account.AccountID
	allPermissions := getValidatedPermissions(info)

	resource := analyzers.Resource{
		Name:               fmt.Sprintf("%s %s", account.Name.GivenName, account.Name.Surname),
		FullyQualifiedName: accountID,
		Type:               "account",
		Metadata: map[string]any{
			"email":         account.Email,
			"emailVerified": account.EmailVerified,
			"disabled":      account.Disabled,
			"country":       account.Country,
			"accountType":   account.AccountType.Tag,
		},
	}
	analyzers.BindAllPermissions(resource, allPermissions...)
	result := analyzers.AnalyzerResult{
		AnalyzerType: analyzers.AnalyzerTypeDropbox,
		Metadata:     nil,
		Bindings:     analyzers.BindAllPermissions(resource, allPermissions...),
	}
	return &result
}

func getValidatedPermissions(info *secretInfo) []analyzers.Permission {
	permissions := []analyzers.Permission{}

	for _, permission := range info.Permissions {
		if permission.Status != StatusGranted {
			continue
		}
		permissions = append(permissions, analyzers.Permission{
			Value: permission.Name,
		})
	}

	return permissions
}

func printAccountAndPermissions(info *secretInfo) {
	color.Yellow("\n[i] Accounts Info:")
	t1 := table.NewWriter()
	t1.SetOutputMirror(os.Stdout)
	t1.AppendHeader(table.Row{"ID", "Name", "Email", "Email Verified", "Disabled", "Country", "Account Type"})
	emailVerified := "No"
	disabled := "No"
	if info.Account.EmailVerified {
		emailVerified = "Yes"
	}
	if info.Account.Disabled {
		disabled = "Yes"
	}
	t1.AppendRow(table.Row{
		color.GreenString(info.Account.AccountID),
		color.GreenString(info.Account.Name.GivenName + " " + info.Account.Name.Surname),
		color.GreenString(info.Account.Email),
		color.GreenString(emailVerified),
		color.GreenString(disabled),
		color.GreenString(info.Account.Country),
		color.GreenString(info.Account.AccountType.Tag),
	})
	t1.SetOutputMirror(os.Stdout)
	t1.Render()

	color.Yellow("\n[i] Permissions:")
	t2 := table.NewWriter()
	t2.AppendHeader(table.Row{"Permission", "Access", "Actions"})

	permissions := info.Permissions
	for _, permission := range permissions {
		access := "Denied"
		permissionStatus := permission.Status
		if permissionStatus == StatusGranted {
			access = "Granted"
		}
		if permissionStatus == StatusUnverified {
			access = "Unverified"
		}
		for idx, action := range permission.Actions {
			permissionCell := ""
			accessCell := ""
			if idx == 0 {
				permissionCell = color.GreenString(permission.Name)
				accessCell = color.GreenString(access)
			}

			t2.AppendRow(table.Row{
				permissionCell,
				accessCell,
				action,
			})
		}
		t2.AppendSeparator()
	}

	t2.SetOutputMirror(os.Stdout)
	t2.Render()
	fmt.Printf("%s: https://www.dropbox.com/developers/documentation\n\n", color.GreenString("Ref"))
}
