// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package task

import (
	stdctx "context"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"

	gitea "gitea.dev/sdk"

	"gitea.dev/tea/modules/config"
	"gitea.dev/tea/modules/utils"
)

// SetupHelper add tea helper to config global
func SetupHelper(login config.Login) (ok bool, err error) {
	// Check that the URL is not blank
	if login.URL == "" {
		return false, fmt.Errorf("invalid Gitea URL")
	}

	// get all helper to URL in git config
	helperKey := fmt.Sprintf("credential.%s.helper", login.URL)
	var currentHelpers []byte
	if currentHelpers, err = exec.Command("git", "config", "--global", "--get-all", helperKey).Output(); err != nil {
		currentHelpers = []byte{}
	}

	// Check if ared added tea helper
	for _, line := range strings.Split(strings.ReplaceAll(string(currentHelpers), "\r", ""), "\n") {
		if strings.HasSuffix(strings.TrimSpace(line), "login helper") {
			return false, nil
		}
	}

	// Add tea helper
	if _, err = exec.Command("git", "config", "--global", helperKey, "").Output(); err != nil {
		return false, fmt.Errorf("git config --global %s, error: %s", helperKey, err)
	} else if _, err = exec.Command("git", "config", "--global", "--add", helperKey, "!tea login helper").Output(); err != nil {
		return false, fmt.Errorf("git config --global --add %s %s, error: %s", helperKey, "!tea login helper", err)
	}

	return true, nil
}

// CreateLogin create a login to be stored in config
func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string, insecure, sshAgent, versionCheck, addHelper bool) error {
	// checks ...
	// ... if we have a url
	if len(giteaURL) == 0 {
		return fmt.Errorf("Gitea server URL is required")
	}

	// ... if there already exist a login with same name
	if login, err := config.GetLoginByName(name); err != nil {
		return err
	} else if login != nil {
		return fmt.Errorf("login name '%s' has already been used", login.Name)
	}
	// ... if we already use this token
	if shouldCheckTokenUniqueness(token, sshAgent, sshKey, sshCertPrincipal, sshKeyFingerprint) {
		login, err := config.GetLoginByToken(token)
		if err != nil {
			return err
		}
		if login != nil {
			return fmt.Errorf("token already been used, delete login '%s' first", login.Name)
		}
	}

	serverURL, err := utils.ValidateAuthenticationMethod(
		giteaURL,
		token,
		user,
		passwd,
		sshAgent,
		sshKey,
		sshCertPrincipal,
	)
	if err != nil {
		return err
	}

	// check if it's a certificate the principal doesn't matter as the user
	// has explicitly selected this private key
	if _, err := os.Stat(sshKey + "-cert.pub"); err == nil {
		sshCertPrincipal = "yes"
	}

	login := config.Login{
		Name:              name,
		URL:               serverURL.String(),
		Token:             token,
		Insecure:          insecure,
		SSHKey:            sshKey,
		SSHCertPrincipal:  sshCertPrincipal,
		SSHKeyFingerprint: sshKeyFingerprint,
		SSHAgent:          sshAgent,
		Created:           time.Now().Unix(),
		VersionCheck:      versionCheck,
	}

	if len(token) == 0 && sshCertPrincipal == "" && !sshAgent && sshKey == "" {
		if login.Token, err = generateToken(ctx, login, user, passwd, otp, scopes); err != nil {
			return err
		}
	}

	client := login.Client()

	// Verify if authentication works and get user info
	u, _, err := client.Users.GetMyUserInfo(ctx)
	if err != nil {
		return err
	}
	login.User = u.UserName

	if len(login.Name) == 0 {
		if login.Name, err = GenerateLoginName(giteaURL, login.User); err != nil {
			return err
		}
	}

	// we do not have a method to get SSH config from api,
	// so we just use the host
	login.SSHHost = serverURL.Host

	if len(sshKey) == 0 {
		login.SSHKey, err = findSSHKey(ctx, client)
		if err != nil {
			fmt.Printf("Warning: problem while finding a SSH key: %s\n", err)
		}
	}

	if err = config.AddLogin(&login); err != nil {
		return err
	}

	fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name)
	if addHelper {
		if _, err := SetupHelper(login); err != nil {
			return err
		}
	} else {
		fmt.Println("Tip: pass --git-credentials (or run 'tea login helper setup') to authenticate 'git push' and 'git clone' over HTTPS with this token.")
	}

	return nil
}

func shouldCheckTokenUniqueness(token string, sshAgent bool, sshKey, sshCertPrincipal, sshKeyFingerprint string) bool {
	if sshAgent || sshKey != "" || sshCertPrincipal != "" || sshKeyFingerprint != "" {
		return false
	}

	return true
}

// generateToken creates a new token when given BasicAuth credentials
func generateToken(ctx stdctx.Context, login config.Login, user, pass, otp, scopes string) (string, error) {
	opts := []gitea.ClientOption{gitea.SetBasicAuth(user, pass)}
	if otp != "" {
		opts = append(opts, gitea.SetOTP(otp))
	}
	client := login.Client(opts...)

	var tl []*gitea.AccessToken
	for page := 1; ; {
		page_tokens, resp, err := client.Users.ListAccessTokens(ctx, gitea.ListAccessTokensOptions{
			ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
		})
		if err != nil {
			return "", err
		}
		tl = append(tl, page_tokens...)
		if resp == nil || resp.NextPage == 0 {
			break
		}
		page = resp.NextPage
	}
	host, _ := os.Hostname()
	tokenName := host + "-tea"

	// append timestamp, if a token with this hostname already exists
	for i := range tl {
		if tl[i].Name == tokenName {
			tokenName += time.Now().Format("2006-01-02_15-04-05")
			break
		}
	}

	var tokenScopes []gitea.AccessTokenScope
	if len(scopes) == 0 {
		tokenScopes = []gitea.AccessTokenScope{gitea.AccessTokenScopeAll}
	} else {
		for _, scope := range strings.Split(scopes, ",") {
			tokenScopes = append(tokenScopes, gitea.AccessTokenScope(strings.TrimSpace(scope)))
		}
	}

	t, _, err := client.Users.CreateAccessToken(ctx, gitea.CreateAccessTokenOption{
		Name:   tokenName,
		Scopes: tokenScopes,
	})
	return t.Token, err
}

// GenerateLoginName generates a name string based on instance URL & adds username if the result is not unique
func GenerateLoginName(url, user string) (string, error) {
	parsedURL, err := utils.NormalizeURL(url)
	if err != nil {
		return "", err
	}
	name := parsedURL.Host

	// append user name if login name already exists
	if len(user) != 0 {
		if login, err := config.GetLoginByName(name); err != nil {
			return "", err
		} else if login != nil {
			return name + "_" + user, nil
		}
	}

	return name, nil
}
