package argocd

import (
	"context"
	"fmt"
	"net/url"
	"strconv"
	"strings"

	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
	"github.com/argoproj/argo-cd/v3/util/cert"
	"github.com/argoproj/argo-cd/v3/util/db"
	"github.com/argoproj/argo-cd/v3/util/settings"

	"github.com/argoproj-labs/argocd-image-updater/ext/git"
	"github.com/argoproj-labs/argocd-image-updater/pkg/kube"
	"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
)

// getGitCredsSource returns git credentials source that loads credentials from the secret or from Argo CD settings
func getGitCredsSource(creds string, kubeClient *kube.ImageUpdaterKubernetesClient, wbc *WriteBackConfig) (GitCredsSource, error) {
	switch {
	case creds == "repocreds":
		return func(app *v1alpha1.Application) (git.Creds, error) {
			return getCredsFromArgoCD(wbc, kubeClient, app.Spec.Project)
		}, nil
	case strings.HasPrefix(creds, "secret:"):
		return func(app *v1alpha1.Application) (git.Creds, error) {
			return getCredsFromSecret(wbc, creds[len("secret:"):], kubeClient)
		}, nil
	}
	return nil, fmt.Errorf("unexpected credentials format. Expected 'repocreds' or 'secret:<namespace>/<secret>' but got '%s'", creds)
}

// getCredsFromArgoCD loads repository credentials from Argo CD settings
func getCredsFromArgoCD(wbc *WriteBackConfig, kubeClient *kube.ImageUpdaterKubernetesClient, project string) (git.Creds, error) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	settingsMgr := settings.NewSettingsManager(ctx, kubeClient.KubeClient.Clientset, kubeClient.KubeClient.Namespace)
	argocdDB := db.NewDB(kubeClient.KubeClient.Namespace, settingsMgr, kubeClient.KubeClient.Clientset)
	repo, err := argocdDB.GetRepository(ctx, wbc.GitRepo, project)
	if err != nil {
		return nil, err
	}
	if !repo.HasCredentials() {
		return nil, fmt.Errorf("credentials for '%s' are not configured in Argo CD settings", wbc.GitRepo)
	}
	creds := GetGitCreds(repo, wbc.GitCreds)
	return creds, nil
}

// GetGitCreds returns the credentials from a repository configuration used to authenticate at a Git repository
// This is a slightly modified version of upstream's Repository.GetGitCreds method. We need it so it does not return the upstream type.
// TODO(jannfis): Can be removed once we have the change to the git client's getGitAskPassEnv upstream.
func GetGitCreds(repo *v1alpha1.Repository, store git.CredsStore) git.Creds {
	if repo == nil {
		return git.NopCreds{}
	}
	if repo.Password != "" {
		return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store, repo.ForceHttpBasicAuth)
	}
	if repo.SSHPrivateKey != "" {
		return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure(), store, repo.Proxy)
	}
	if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
		return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.Repo, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store)
	}
	if repo.GCPServiceAccountKey != "" {
		return git.NewGoogleCloudCreds(repo.GCPServiceAccountKey, store)
	}
	return git.NopCreds{}
}

// Taken from upstream Argo CD.
// TODO(jannfis): Can be removed once we have the change to the git client's getGitAskPassEnv upstream.
func getCAPath(repoURL string) string {
	// For git ssh protocol url without ssh://, url.Parse() will fail to parse.
	// However, no warn log is output since ssh scheme url is a possible format.
	if ok, _ := git.IsSSHURL(repoURL); ok {
		return ""
	}

	hostname := ""
	// url.Parse() will happily parse most things thrown at it. When the URL
	// is either https or oci, we use the parsed hostname to retrieve the cert,
	// otherwise we'll use the parsed path (OCI repos are often specified as
	// hostname, without protocol).
	parsedURL, err := url.Parse(repoURL)
	if err != nil {
		log.Warnf("Could not parse repo URL '%s': %v", repoURL, err)
		return ""
	}
	if parsedURL.Scheme == "https" || parsedURL.Scheme == "oci" {
		hostname = parsedURL.Host
	} else if parsedURL.Scheme == "" {
		hostname = parsedURL.Path
	}

	if hostname == "" {
		log.Warnf("Could not get hostname for repository '%s'", repoURL)
		return ""
	}

	caPath, err := cert.GetCertBundlePathForRepository(hostname)
	if err != nil {
		log.Warnf("Could not get cert bundle path for repository '%s': %v", repoURL, err)
		return ""
	}

	return caPath
}

// getCredsFromSecret loads repository credentials from secret
func getCredsFromSecret(wbc *WriteBackConfig, credentialsSecret string, kubeClient *kube.ImageUpdaterKubernetesClient) (git.Creds, error) {
	var credentials map[string][]byte
	var err error
	s := strings.SplitN(credentialsSecret, "/", 2)
	if len(s) == 2 {
		credentials, err = kubeClient.KubeClient.GetSecretData(s[0], s[1])
		if err != nil {
			return nil, err
		}
	} else {
		return nil, fmt.Errorf("secret ref must be in format 'namespace/name', but is '%s'", credentialsSecret)
	}

	if ok, _ := git.IsSSHURL(wbc.GitRepo); ok {
		var sshPrivateKey []byte
		if sshPrivateKey, ok = credentials["sshPrivateKey"]; !ok {
			return nil, fmt.Errorf("invalid secret %s: does not contain field sshPrivateKey", credentialsSecret)
		}
		return git.NewSSHCreds(string(sshPrivateKey), "", true, wbc.GitCreds, ""), nil
	} else if git.IsHTTPSURL(wbc.GitRepo) {
		var username, password, githubAppID, githubAppInstallationID, githubAppPrivateKey []byte
		if githubAppID, ok = credentials["githubAppID"]; ok {
			if githubAppInstallationID, ok = credentials["githubAppInstallationID"]; !ok {
				return nil, fmt.Errorf("invalid secret %s: does not contain field githubAppInstallationID", credentialsSecret)
			}
			if githubAppPrivateKey, ok = credentials["githubAppPrivateKey"]; !ok {
				return nil, fmt.Errorf("invalid secret %s: does not contain field githubAppPrivateKey", credentialsSecret)
			}
			// converting byte array to string and ultimately int64 for NewGitHubAppCreds
			intGithubAppID, err := strconv.ParseInt(string(githubAppID), 10, 64)
			if err != nil {
				return nil, fmt.Errorf("invalid value in field githubAppID: %w", err)
			}
			intGithubAppInstallationID, err := strconv.ParseInt(string(githubAppInstallationID), 10, 64)
			if err != nil {
				return nil, fmt.Errorf("invalid value in field githubAppInstallationID: %w", err)
			}
			return git.NewGitHubAppCreds(intGithubAppID, intGithubAppInstallationID, string(githubAppPrivateKey), "", "", "", "", true, "", wbc.GitCreds), nil
		} else if username, ok = credentials["username"]; ok {
			if password, ok = credentials["password"]; !ok {
				return nil, fmt.Errorf("invalid secret %s: does not contain field password", credentialsSecret)
			}
			return git.NewHTTPSCreds(string(username), string(password), "", "", true, "", wbc.GitCreds, false), nil
		}
		return nil, fmt.Errorf("invalid repository credentials in secret %s: does not contain githubAppID or username", credentialsSecret)
	}
	return nil, fmt.Errorf("unknown repository type")
}
