package github

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/bradleyfalzon/ghinstallation/v2"
	"github.com/gin-gonic/gin"
	"github.com/google/go-github/v50/github"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/ecosystem-service/config"
	"github.com/tidbcloud/ecosystem-service/internal/metric"
	"github.com/tidbcloud/ecosystem-service/internal/service/github/util"
	"github.com/tidbcloud/ecosystem-service/internal/service/grpc/branch"
	gitDB "github.com/tidbcloud/ecosystem-service/internal/storage/db/git"
	"github.com/tidbcloud/tiinf/monitor/tilog"

	"go.uber.org/zap"
	"golang.org/x/oauth2"
)

type Conclusion string

const (
	Fail    Conclusion = "failure"
	Timeout Conclusion = "timed_out"
	Success Conclusion = "success"
	Skip    Conclusion = "skipped"
)

const checkName = "TiDB Cloud Branching"

type UnauthorizedError struct {
	message string
}

func (e *UnauthorizedError) Error() string {
	return e.message
}

type GitHubApp struct {
	ghClientMap           map[int64]*github.Client
	tiDBGitIntegrationDAO gitDB.TiDBGitIntegrationDAO
	conf                  *config.ConfigWrapper
	BranchSvc             branch.BranchSvc
	gitHubAppTaskDao      gitDB.GitHubAppTaskDAO
	mutexes               *util.KeyedMutex
}

func NewGitHubApp(tiDBGitIntegrationDAO gitDB.TiDBGitIntegrationDAO, conf *config.ConfigWrapper, branchSvc branch.BranchSvc, gitHubAppTaskDao gitDB.GitHubAppTaskDAO) *GitHubApp {
	return &GitHubApp{
		ghClientMap:           make(map[int64]*github.Client),
		tiDBGitIntegrationDAO: tiDBGitIntegrationDAO,
		conf:                  conf,
		BranchSvc:             branchSvc,
		gitHubAppTaskDao:      gitHubAppTaskDao,
		mutexes:               &util.KeyedMutex{},
	}
}

func (g *GitHubApp) getGitHubAppSecret() (*config.GitHubAppConfig, error) {
	githubAppSecret := g.conf.GitConfig.GitHubAppConfig
	if githubAppSecret.AppIdentifier == 0 || githubAppSecret.WebhookSecret == "" || githubAppSecret.PrivateKey == "" {
		return nil, errors.New("Empty GitHub App Config")
	}
	return &githubAppSecret, nil
}

func (g *GitHubApp) GetGitHubClient(ctx context.Context, installationID int64) (*github.Client, error) {
	if g.ghClientMap[installationID] == nil {
		githubAppSecret, err := g.getGitHubAppSecret()
		if err != nil {
			return nil, err
		}
		ghClient, err := initGitHubClient(installationID, githubAppSecret.AppIdentifier, []byte(githubAppSecret.PrivateKey))
		if err != nil {
			return nil, err
		}
		// test client
		_, _, err = ghClient.Apps.ListRepos(ctx, &github.ListOptions{})
		if err != nil {
			return nil, err
		}
		g.ghClientMap[installationID] = ghClient
	}

	return g.ghClientMap[installationID], nil
}

// ValidatePayload validate payload webhook signature
func (g *GitHubApp) ValidatePayload(c *gin.Context) ([]byte, error) {
	githubAppSecret, err := g.getGitHubAppSecret()
	if err != nil {
		return nil, err
	}
	webhookSecret := githubAppSecret.WebhookSecret
	xHubSignature := c.GetHeader("X-Hub-Signature")

	payload, err := github.ValidatePayloadFromBody(
		c.GetHeader("content-type"),
		c.Request.Body,
		xHubSignature,
		[]byte(webhookSecret))
	if err != nil {
		return nil, err
	}
	return payload, nil
}

// initGitHubClient initializes a GitHub client to respond back to GitHub after events are received
func initGitHubClient(installationId int64, githubAppID int64, githubPrivateKey []byte) (*github.Client, error) {
	tr := http.DefaultTransport
	itr, err := ghinstallation.New(
		tr,
		githubAppID,
		installationId,
		githubPrivateKey,
	)
	if err != nil {
		return nil, err
	}
	c := github.NewClient(&http.Client{Transport: itr})

	return c, nil
}

// ListRepositories list all repos
func (g *GitHubApp) ListRepositories(ctx context.Context, installationID int64) ([]*github.Repository, error) {
	ghClient, err := g.GetGitHubClient(ctx, installationID)
	if err != nil {
		tilog.Error(ctx, "get GitHub client error", zap.Error(err))
		return nil, err
	}

	repositories := make([]*github.Repository, 0)

	nextPage := 1
	for nextPage > 0 {
		listRepositories, resp, err := ghClient.Apps.ListRepos(ctx, &github.ListOptions{
			Page:    nextPage,
			PerPage: 100,
		})
		if err != nil {
			return nil, err
		}
		nextPage = resp.NextPage
		repositories = append(repositories, listRepositories.Repositories...)
	}
	return repositories, nil
}

func (g *GitHubApp) CommentError(ctx context.Context, installationID int64, owner, repo string, number int, summary string, details string, action string) (err error) {

	defer func() {
		if err != nil {
			metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.GitCommentError).Inc()
		}
	}()

	client, err := g.GetGitHubClient(ctx, installationID)
	if err != nil {
		tilog.Error(ctx, "get GitHub client error", zap.Error(err))
		return err
	}
	_, _, err = client.Issues.CreateComment(ctx, owner, repo, number, &github.IssueComment{
		Body: Ptr(fmt.Sprintf("**%s**\n\nDetails:\n ```\n%s\n```", summary, details)),
	})
	if err != nil {
		return err
	}
	return nil
}

func (g *GitHubApp) UpdateCheckRun(ctx context.Context, installationID int64, owner, repo, sha string, prNumber int, conclusion Conclusion, text *CheckRunOutputDetail, externalID *ExternalID, checkRunID int64, summary, action string) (err error) {

	defer func() {
		if err != nil {
			metric.GitHubAppErrorCounter.WithLabelValues(metric.PullRequestEvent, action, metric.GitUpdateCheckRun).Inc()
		}
	}()

	tilog.Debug(ctx, fmt.Sprintf("update check run for %s on pr number %d and commitID %s. conclusion: %s", owner+repo, prNumber, sha, conclusion))
	githubClient, err := g.GetGitHubClient(ctx, installationID)
	if err != nil {
		tilog.Error(ctx, "get GitHub client error", zap.Error(err))
		return err
	}

	externalIDJson, err := externalID.MarshallExternalID()
	if err != nil {
		tilog.Error(ctx, "marshall externalID error", zap.Any("externalID", externalID), zap.Error(err))
		return err
	}

	option := github.UpdateCheckRunOptions{
		Name:        checkName,
		DetailsURL:  Ptr("https://tidbcloud.com/"),
		ExternalID:  &externalIDJson,
		Status:      Ptr("completed"),
		Conclusion:  Ptr(string(conclusion)),
		CompletedAt: &github.Timestamp{Time: time.Now()},
		Output: &github.CheckRunOutput{
			Title:   Ptr(checkName + " Check"),
			Summary: &summary,
		},
	}
	if text != nil {
		option.Output.Text = Ptr(text.MarshalJSONText())
	}

	_, _, err = githubClient.Checks.UpdateCheckRun(ctx, owner, repo, checkRunID, option)
	return
}

func (g *GitHubApp) GetUserInstalledAccounts(ctx context.Context, token string) ([]*github.Installation, error) {
	tilog.Debug(ctx, "get user installed accounts")

	tc := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}))
	client := github.NewClient(tc)

	installations, resp, err := client.Apps.ListUserInstallations(ctx, &github.ListOptions{
		Page:    1,
		PerPage: 100,
	})
	if err != nil {
		if resp.StatusCode == 401 {
			return nil, &UnauthorizedError{message: err.Error()}
		}
		return nil, err
	}
	return installations, nil
}

func (g *GitHubApp) GetUserAccessToken(ctx context.Context, code string) (*UserAccessToken, error) {
	tilog.Debug(ctx, fmt.Sprintf("get user access token check with code %s", code))
	githubSecret := g.conf.GitConfig.GitHubAppConfig
	if githubSecret.ClientSecret == "" || githubSecret.ClientID == "" {
		return nil, errors.New("github client secret or client id is empty")
	}

	url := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&code=%s", githubSecret.ClientID, githubSecret.ClientSecret, code)
	resp, err := http.Get(url) //nolint
	if err != nil {
		return nil, err
	}
	content, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	return ResolveAccessTokenResponse(content)
}

func (g *GitHubApp) RefreshUserAccessToken(ctx context.Context, refreshToken string) (*UserAccessToken, error) {
	tilog.Debug(ctx, fmt.Sprintf("refresh user access token check with refresh token %s", refreshToken))

	githubSecret := g.conf.GitConfig.GitHubAppConfig
	if githubSecret.ClientSecret == "" || githubSecret.ClientID == "" {
		return nil, errors.New("github client secret or client id is empty")
	}

	url := fmt.Sprintf("https://github.com/login/oauth/access_token?client_id=%s&client_secret=%s&grant_type=refresh_token&refresh_token=%s", githubSecret.ClientID, githubSecret.ClientSecret, refreshToken)
	resp, err := http.Get(url) //nolint
	if err != nil {
		return nil, err
	}
	content, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return ResolveAccessTokenResponse(content)
}

func ResolveAccessTokenResponse(body []byte) (*UserAccessToken, error) {
	content := string(body)
	if content == "" {
		return nil, errors.New("response is empty")
	}
	contents := strings.Split(content, "&")

	if strings.Contains(contents[0], "error") {
		if strings.Contains(contents[0], "bad_verification_code") || strings.Contains(contents[0], "bad_refresh_token") {
			return nil, &UnauthorizedError{message: contents[0]}
		}
		return nil, errors.New(contents[0])
	}

	userAccessToken := &UserAccessToken{}
	for _, c := range contents {
		if strings.Contains(c, "access_token=") {
			userAccessToken.AccessToken = strings.Split(c, "=")[1]
		}
		if strings.Contains(c, "expires_in=") {
			userAccessToken.ExpiresIn = strings.Split(c, "=")[1]
		}
		if strings.Contains(c, "refresh_token=") {
			userAccessToken.RefreshToken = strings.Split(c, "=")[1]
		}
		if strings.Contains(c, "refresh_token_expires_in=") {
			userAccessToken.RefreshTokenExpiresIn = strings.Split(c, "=")[1]
		}
		if strings.Contains(c, "token_type=") {
			userAccessToken.TokenType = strings.Split(c, "=")[1]
		}
	}
	return userAccessToken, nil
}

func (g *GitHubApp) getGitConfig(ctx context.Context, installationID int64, head Head) (*Config, error) {
	client, err := g.GetGitHubClient(ctx, installationID)
	if err != nil {
		return nil, err
	}
	contents, _, _, err := client.Repositories.GetContents(ctx, head.Repo.Owner.Login, head.Repo.Name, Path, &github.RepositoryContentGetOptions{
		Ref: head.Ref,
	})
	if err != nil {
		return nil, err
	}
	gitConfig, err := UnmarshallGitConfig(contents)
	if err != nil {
		return nil, err
	}
	return gitConfig, nil
}
