package github

import (
	"context"
	"dataservices/commons/config"
	"dataservices/commons/utils"
	"encoding/base64"
	"fmt"
	"github.com/bradleyfalzon/ghinstallation/v2"
	"net/http"
	"strings"

	"github.com/google/go-github/v50/github"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"golang.org/x/oauth2"
)

func newGithubInstallationClient(ghappId, installationId int64) (*github.Client, error) {
	tr := http.DefaultTransport
	itr, err := ghinstallation.NewKeyFromFile(tr, ghappId, installationId, config.Global().GhappConf.PrivateKeyFile)
	if err != nil {
		return nil, err
	}
	client := github.NewClient(&http.Client{Transport: itr})
	return client, nil
}

// exchange github user info with oauth2.0 code
func Code2UserAndInstallations(ctx context.Context, code string) (*github.User, []*github.Installation, error) {
	conf := &oauth2.Config{
		ClientID:     config.Global().GhappConf.ClientId,
		ClientSecret: config.Global().GhappConf.ClientSecret,
		Endpoint: oauth2.Endpoint{
			AuthURL:  "https://github.com/login/oauth/authorize",
			TokenURL: "https://github.com/login/oauth/access_token",
		},
	}
	tok, err := conf.Exchange(ctx, code)
	if err != nil {
		return nil, nil, err
	}
	oauthClient := conf.Client(ctx, tok)
	userClient := github.NewClient(oauthClient)
	user, _, err := userClient.Users.Get(ctx, "")
	if err != nil {
		return nil, nil, err
	}
	// installations are hard to exceed 100, so we only concern the first page
	installations, _, err := userClient.Apps.ListUserInstallations(ctx, &github.ListOptions{PerPage: 100})
	if err != nil {
		return nil, nil, err
	}
	return user, installations, nil
}

func ListGithubRepositoriesByInstallationId(ctx context.Context, ghappId, installationId int64) ([]*github.Repository, error) {
	client, err := newGithubInstallationClient(ghappId, installationId)
	if err != nil {
		return nil, err
	}
	var fetched int
	result := make([]*github.Repository, 0)
	// At most 1000 repos to avoid too slow.
	for i := 1; i <= 10; i++ {
		repoList, _, err := client.Apps.ListRepos(ctx, &github.ListOptions{Page: i, PerPage: 100})
		if err != nil {
			return nil, err
		}
		for _, r := range repoList.Repositories {
			if !*r.Archived {
				result = append(result, r)
			}
		}
		fetched += len(repoList.Repositories)
		if fetched >= *repoList.TotalCount {
			break
		}
	}
	return result, nil
}

func ListGithubRepositoryBranches(ctx context.Context, ghappId, installationId int64, owner, repo string) ([]*github.Branch, error) {
	client, err := newGithubInstallationClient(ghappId, installationId)
	if err != nil {
		return nil, err
	}
	result := make([]*github.Branch, 0)
	// At most 1000 repos to avoid too slow.
	for i := 1; i <= 10; i++ {
		branches, _, err := client.Repositories.ListBranches(ctx, owner, repo, &github.BranchListOptions{
			ListOptions: github.ListOptions{Page: i, PerPage: 100},
		})
		if err != nil {
			return nil, err
		}
		result = append(result, branches...)
		if len(branches) < 100 {
			continue
		}
	}
	return result, nil
}

func MakeCommit(
	ctx context.Context,
	ghappId,
	installationId int64,
	owner,
	repo,
	branch,
	directory,
	commitMessage string,
	rootNode *FileTreeNode,
	lastNode *FileTreeNode,
) error {
	directory = strings.TrimPrefix(directory, "/")
	client, err := newGithubInstallationClient(ghappId, installationId)
	if err != nil {
		return err
	}

	// get last commit hash
	branchObj, _, err := client.Repositories.GetBranch(ctx, owner, repo, branch, false)
	if err != nil {
		return err
	}
	lastCommitHash := branchObj.Commit.SHA

	// create blobs
	files := rootNode.TraverseFiles()
	blobs := make([]*github.Blob, 0)
	for _, f := range files {
		content := base64.StdEncoding.EncodeToString(f.Content)
		blob, _, err := client.Git.CreateBlob(ctx, owner, repo, &github.Blob{
			Content:  &content,
			Encoding: utils.Str2Ptr("base64"),
		})
		if err != nil {
			return err
		}
		blobs = append(blobs, blob)
	}
	// delete extra files
	if lastNode != nil {
		diff := lastNode.Diff(rootNode)
		for _, deleted := range diff.Deleted {
			blobs = append(blobs, &github.Blob{SHA: nil})
			files = append(files, deleted)
		}
	}

	// create git tree
	entries := buildGitEntries(directory, files, blobs)
	gitTree, _, err := client.Git.CreateTree(ctx, owner, repo, *lastCommitHash, entries)
	if err != nil {
		return err
	}

	// create the commit
	if commitMessage == "" {
		commitMessage = "Deployment from TiDB Cloud Data Service"
	}
	newCommit, _, err := client.Git.CreateCommit(ctx, owner, repo, &github.Commit{
		Message: utils.Str2Ptr(commitMessage),
		Tree:    gitTree,
		Parents: []*github.Commit{{SHA: lastCommitHash}},
	})
	if err != nil {
		return err
	}

	// update branch reference to point to new commit hash
	newRef := &github.Reference{
		Ref: utils.Str2Ptr(fmt.Sprintf("refs/heads/%s", branch)),
		Object: &github.GitObject{
			SHA: newCommit.SHA,
		},
	}
	_, _, err = client.Git.UpdateRef(ctx, owner, repo, newRef, false)
	if err != nil {
		return err
	}

	return err
}

func buildGitEntries(directory string, files []*FileTreeNode, blobs []*github.Blob) []*github.TreeEntry {
	entries := make([]*github.TreeEntry, 0)
	for i, f := range files {
		blob := blobs[i]
		entries = append(entries, &github.TreeEntry{
			Path: utils.Str2Ptr(strings.TrimPrefix(fmt.Sprintf("%s%s", directory, f.Path()), "/")),
			Mode: utils.Str2Ptr("100644"),
			Type: utils.Str2Ptr("blob"),
			SHA:  blob.SHA,
		})
	}
	return entries
}

func ListCommitEntries(ctx context.Context, ghappId, installationId int64, owner, repo, commitHash string) ([]*github.TreeEntry, error) {
	client, err := newGithubInstallationClient(ghappId, installationId)
	if err != nil {
		return nil, err
	}
	commit, _, err := client.Git.GetCommit(ctx, owner, repo, commitHash)
	if err != nil {
		return nil, err
	}
	tree, _, err := client.Git.GetTree(ctx, owner, repo, *commit.Tree.SHA, true)
	if err != nil {
		return nil, err
	}
	return tree.Entries, err
}

func Entries2FileTreeNode(ctx context.Context, ghappId, installationId int64, owner, repo, directory string, entries []*github.TreeEntry) (*FileTreeNode, error) {
	directory = strings.TrimPrefix(directory, "/")
	result := NewDirectoryNode("root")
	client, err := newGithubInstallationClient(ghappId, installationId)
	if err != nil {
		return nil, err
	}
	for _, entry := range entries {
		if !strings.HasPrefix(*entry.Path, directory) {
			continue
		}
		path := strings.TrimPrefix(*entry.Path, directory)
		path = strings.TrimPrefix(path, "/")
		if !strings.HasPrefix(
			path, "data_sources/cluster.json",
		) && !strings.HasPrefix(
			path, "http_endpoints/sql/",
		) && !strings.HasPrefix(
			path, "http_endpoints/config.json",
		) && !strings.HasPrefix(
			path, "dataapp_config.json",
		) {
			continue
		}
		blob, _, err := client.Git.GetBlob(ctx, owner, repo, *entry.SHA)
		if err != nil {
			tilog.Error(ctx, "get blob failed", zap.Error(err), zap.String("path", path))
			return nil, err
		}
		if *blob.Encoding != "base64" {
			return nil, fmt.Errorf("unknown encoding: %s", *blob.Encoding)
		}
		content, err := base64.StdEncoding.DecodeString(*blob.Content)
		if err != nil {
			return nil, err
		}
		result.AppendRelativePathFile(path, content)
	}

	return result, nil
}

func GetRepoById(ctx context.Context, ghappId, installationId, repoId int64) (*github.Repository, error) {
	client, err := newGithubInstallationClient(ghappId, installationId)
	if err != nil {
		return nil, err
	}
	repo, _, err := client.Repositories.GetByID(ctx, repoId)
	if err != nil {
		return nil, err
	}
	return repo, nil
}

func GetBranch(ctx context.Context, ghappId, installationId int64, owner, repo, branch string) (*github.Branch, error) {
	client, err := newGithubInstallationClient(ghappId, installationId)
	if err != nil {
		return nil, err
	}
	branchObj, _, err := client.Repositories.GetBranch(ctx, owner, repo, branch, false)
	if err != nil {
		return nil, err
	}
	return branchObj, nil
}
