package main

/*
 * This file is part of Autogits.
 *
 * Copyright © 2024 SUSE LLC
 *
 * Autogits is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 2 of the License, or (at your option) any later
 * version.
 *
 * Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * Foobar. If not, see <https://www.gnu.org/licenses/>.
 */

import (
	"flag"
	"fmt"
	"log"
	"math/rand"
	"path"
	"slices"
	"strings"
	"time"

	"src.opensuse.org/autogits/common"
)

const (
	AppName   = "workflow-pr"
	GitAuthor = "AutoGits - pr-review"
	GitEmail  = "adam+autogits-pr@zombino.com"
)

var configuredRepos map[string][]*common.AutogitConfig
var gitea *common.GiteaTransport

/*
func fetchPrGit(h *common.RequestHandler, pr *models.PullRequest) error {
	// clone PR head and base and return path
	if h.HasError() {
		return h.Error
	}
	if _, err := os.Stat(path.Join(h.GitPath, pr.Head.Sha)); os.IsNotExist(err) {
		h.GitExec("", "clone", "--depth", "1", pr.Head.Repo.CloneURL, pr.Head.Sha)
		h.GitExec(pr.Head.Sha, "fetch", "--depth", "1", "origin", pr.Head.Sha, pr.Base.Sha)
	} else if err != nil {
		h.Error = err
	}

	return h.Error
}*/

func processPullRequestClosed(req *common.PullRequestWebhookEvent, git *common.GitHandler, config *common.AutogitConfig) error {
	if req.Repository.Name != config.GitProjectName {
		return nil
	}

	log.Println("request was:", req.Pull_Request.State)

	return nil
	/*
	   req := h.Data.(*common.PullRequestAction)

	   	if req.Repository.Name != common.DefaultGitPrj {
	   		// we only handle project git PR updates here
	   		return nil
	   	}

	   	if err := fetchPrGit(h, req.Pull_Request); err != nil {
	   		return err
	   	}

	   headSubmodules := h.GitSubmoduleList(dir, pr.Head.Sha)
	   baseSubmodules := h.GitSubmoduleList(dir, pr.Base.Sha)
	   return nil
	*/
}

func processPrjGitPullRequestSync(req *common.PullRequestWebhookEvent) error {
	//	req := h.Data.(*common.PullRequestAction)

	return nil
}

func prGitBranchNameForPR(req *common.PullRequestWebhookEvent) string {
	return fmt.Sprintf("PR_%s#%d", req.Repository.Name, req.Pull_Request.Number)
}

func updateOrCreatePRBranch(req *common.PullRequestWebhookEvent, git *common.GitHandler, commitMsg, branchName string) error {
	common.PanicOnError(git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", req.Repository.Name))
	common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, req.Repository.Name), "fetch", "--depth", "1", "origin", req.Pull_Request.Head.Sha))
	common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, req.Repository.Name), "checkout", req.Pull_Request.Head.Sha))
	common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
	common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", "-f", "origin", branchName))
	return nil
}

func processPullRequestSync(req *common.PullRequestWebhookEvent, git *common.GitHandler, config *common.AutogitConfig) error {
	if req.Repository.Name == config.GitProjectName {
		return processPrjGitPullRequestSync(req)
	}

	// need to verify that submodule in the PR for prjgit
	// is still pointing to the HEAD of the PR
	prjPr, err := gitea.GetAssociatedPrjGitPR(req)
	if err != nil {
		return fmt.Errorf("Cannot get associated PrjGit PR in processPullRequestSync. Err: %w", err)
	}

	log.Println("associated pr:", prjPr)

	common.PanicOnError(git.GitExec("", "clone", "--branch", prjPr.Head.Name, "--depth", "1", prjPr.Head.Repo.SSHURL, common.DefaultGitPrj))
	commitId, ok := git.GitSubmoduleCommitId(common.DefaultGitPrj, req.Repository.Name, prjPr.Head.Sha)

	if !ok {
		return fmt.Errorf("Cannot fetch submodule commit id in prjgit for '%s'", req.Repository.Name)
	}

	// nothing changed, still in sync
	if commitId == req.Pull_Request.Head.Sha {
		log.Println("commitID already match - nothing to do")
		return nil
	}

	log.Printf("different ids: '%s' vs. '%s'\n", req.Pull_Request.Head.Sha, commitId)

	commitMsg := fmt.Sprintf(`Sync PR

Update to %s`, req.Pull_Request.Head.Sha)

	log.Println("will create new commit msg:", commitMsg)

	// we need to update prjgit PR with the new head hash
	branchName := prGitBranchNameForPR(req)
	return updateOrCreatePRBranch(req, git, commitMsg, branchName)
}

func processPullRequestOpened(req *common.PullRequestWebhookEvent, git *common.GitHandler, config *common.AutogitConfig) error {
	// requests against project are not handled here
	if req.Repository.Name == config.GitProjectName {
		return nil
	}

	// create PrjGit branch for buidling the pull request
	branchName := prGitBranchNameForPR(req)
	commitMsg := fmt.Sprintf(`auto-created for %s

This commit was autocreated by %s
referencing

PullRequest: %s/%s#%d`, req.Repository.Owner.Username,
		req.Repository.Name, GitAuthor, req.Repository.Name, req.Pull_Request.Number)

	prjGit, err := gitea.CreateRepositoryIfNotExist(git, *req.Repository.Owner, config.GitProjectName)
	if err != nil {
		return err
	}

	common.PanicOnError(git.GitExec("", "clone", "--depth", "1", prjGit.SSHURL, common.DefaultGitPrj))
	common.PanicOnError(git.GitExec(common.DefaultGitPrj, "checkout", "-B", branchName, prjGit.DefaultBranch))
	common.PanicOnError(updateOrCreatePRBranch(req, git, commitMsg, branchName))

	PR, err := gitea.CreatePullRequestIfNotExist(prjGit, branchName, prjGit.DefaultBranch,
		fmt.Sprintf("Forwarded PR: %s", req.Repository.Name),
		fmt.Sprintf(`This is a forwarded pull request by %s
referencing the following pull request:

`+common.PrPattern,
			GitAuthor, req.Repository.Owner.Username, req.Repository.Name, req.Pull_Request.Number),
	)

	if err != nil {
		return err
	}

	// request build review
	_, err = gitea.RequestReviews(PR, common.Bot_BuildReview)
	return err
}

func processPullRequest(request *common.Request) error {
	req, ok := request.Data.(*common.PullRequestWebhookEvent)
	if !ok {
		return fmt.Errorf("*** Invalid data format for PR processing.")
	}

	configs := configuredRepos[req.Repository.Owner.Username]
	if len(configs) < 1 {
		// ignoring pull request against unconfigured project (could be just regular sources?)
		return nil
	}

	var config *common.AutogitConfig
	for _, c := range configs {
		if c.GitProjectName == req.Pull_Request.Base.Repo.Name ||
			c.Branch == req.Pull_Request.Base.Ref {

			config = c
			break
		}
	}
	if config == nil {
		return fmt.Errorf("Cannot find config for branch '%s'", req.Pull_Request.Base.Ref)
	}

	git, err := common.CreateGitHandler(GitAuthor, GitEmail, AppName)
	if err != nil {
		return fmt.Errorf("Error allocating GitHandler. Err: %w", err)
	}

	switch req.Action {
	case "opened", "reopened":
		return processPullRequestOpened(req, git, config)
	case "synchronized":
		return processPullRequestSync(req, git, config)
	case "edited":
		// not need to be handled??
		return nil
	case "closed":
		return processPullRequestClosed(req, git, config)
	}

	return fmt.Errorf("Unhandled pull request action: %s", req.Action)
}

var DebugMode bool
var checkOnStart bool
var checkInterval time.Duration

func verifyProjectState(git *common.GitHandler, orgName string, config *common.AutogitConfig, configs []*common.AutogitConfig) error {
	org := common.Organization{
		Username: orgName,
	}
	repo, err := gitea.CreateRepositoryIfNotExist(git, org, config.GitProjectName)
	if err != nil {
		return fmt.Errorf("Error fetching or creating '%s/%s' -- aborting verifyProjectState(). Err: %w", orgName, config.GitProjectName, err)
	}

	common.PanicOnError(git.GitExec("", "clone", "--depth", "1", repo.SSHURL, config.GitProjectName))
	log.Println("getting submodule list")
	submodules, err := git.GitSubmoduleList(config.GitProjectName, "HEAD")

nextSubmodule:
	for sub, commitID := range submodules {
		log.Println("  + checking", sub, commitID)
		submoduleName := sub
		if n := strings.LastIndex(sub, "/"); n != -1 {
			submoduleName = sub[n+1:]
		}
		commits, err := gitea.GetRecentCommits(config.Organization, submoduleName, config.Branch, 10)
		if err != nil {
			return fmt.Errorf("Error fetching recent commits for %s/%s. Err: %w", config.Organization, submoduleName, err)
		}

		for idx, commit := range commits {
			if commit.SHA == commitID {
				if idx != 0 {
					// commit in past ...
					log.Println("  W -", submoduleName, " is behind the branch by", idx, "This should not happen in PR workflow alone")
				}
				continue nextSubmodule
			}
		}

		// not found in past, check if we should advance the branch label ... pull the submodule
		git.GitExecOrPanic(config.GitProjectName, "submodule", "update", "--init", "--filter", "blob:none", "--", sub)
		subDir := path.Join(config.GitProjectName, sub)
		newCommits := common.SplitStringNoEmpty(git.GitExecWithOutputOrPanic(subDir, "rev-list", "^origin/"+config.Branch, commitID), "\n")

		if len(newCommits) >= 1 {
			if DebugMode {
				log.Println("    - updating branch", config.Branch, "to new head", commitID, " - len:", len(newCommits))
			}
			git.GitExecOrPanic(subDir, "checkout", "-B", config.Branch, commitID)
			url := git.GitExecWithOutputOrPanic(subDir, "remote", "get-url", "origin", "--push")
			sshUrl, err := common.TranslateHttpsToSshUrl(strings.TrimSpace(url))
			if err != nil {
				return fmt.Errorf("Cannot traslate HTTPS git URL to SSH_URL. %w", err)
			}
			git.GitExecOrPanic(subDir, "remote", "set-url", "origin", "--push", sshUrl)
			git.GitExecOrPanic(subDir, "push", "origin", config.Branch)
		}
	}

	// forward any package-gits referred by the project git, but don't go back
	return nil
}

func checkRepos() {
	for org, configs := range configuredRepos {
		for _, config := range configs {
			if checkInterval > 0 {
				sleepInterval := checkInterval - checkInterval/2 + time.Duration(rand.Int63n(int64(checkInterval)))
				log.Println(" - sleep interval", sleepInterval, "until next check")
				time.Sleep(sleepInterval)
			}

			log.Printf(" ++ starting verification, org: `%s` config: `%s`\n", org, config.GitProjectName)
			git, err := common.CreateGitHandler(GitAuthor, GitEmail, AppName)
			if err != nil {
				log.Println("Faield to allocate GitHandler:", err)
				return
			}
			if !DebugMode {
				defer git.Close()
			}
			if err := verifyProjectState(git, org, config, configs); err != nil {
				log.Printf(" *** verification failed, org: `%s`, err: %#v\n", org, err)
			}
			log.Printf(" ++ verification complete, org: `%s` config: `%s`\n", org, config.GitProjectName)
		}
	}
}

func consistencyCheckProcess() {
	if checkOnStart {
		savedCheckInterval := checkInterval
		checkInterval = 0
		log.Println("== Startup consistency check begin...")
		checkRepos()
		log.Println("== Startup consistency check done...")
		checkInterval = savedCheckInterval
	}

	for {
		checkRepos()
	}
}

func main() {
	if err := common.RequireGiteaSecretToken(); err != nil {
		log.Fatal(err)
	}
	if err := common.RequireRabbitSecrets(); err != nil {
		log.Fatal(err)
	}

	workflowConfig := flag.String("config", "", "Repository and workflow definition file")
	giteaHost := flag.String("gitea", "src.opensuse.org", "Gitea instance")
	rabbitUrl := flag.String("url", "amqps://rabbit.opensuse.org", "URL for RabbitMQ instance")
	flag.BoolVar(&DebugMode, "debug", false, "Extra debugging information")
	flag.BoolVar(&checkOnStart, "check-on-start", false, "Check all repositories for consistency on start, without delays")
	checkIntervalHours := flag.Float64("check-interval", 5, "Check interval (+-random delay) for repositories for consitency, in hours")
	flag.Parse()

	checkInterval = time.Duration(*checkIntervalHours) * time.Hour

	if len(*workflowConfig) == 0 {
		log.Fatalln("No configuratio file specified. Aborting")
	}

	configs, err := common.ReadWorkflowConfigsFile(*workflowConfig)
	if err != nil {
		log.Fatal(err)
	}

	configuredRepos = make(map[string][]*common.AutogitConfig)
	orgs := make([]string, 0, 1)
	for _, c := range configs {
		if slices.Contains(c.Workflows, "pr") {
			if DebugMode {
				log.Printf("  + adding org: '%s', branch: '%s', prjgit: '%s'\n", c.Organization, c.Branch, c.GitProjectName)
			}
			configs := configuredRepos[c.Organization]
			if configs == nil {
				configs = make([]*common.AutogitConfig, 0, 1)
			}
			configs = append(configs, c)
			configuredRepos[c.Organization] = configs

			orgs = append(orgs, c.Organization)
		}
	}

	gitea = common.AllocateGiteaTransport(*giteaHost)
	go consistencyCheckProcess()

	var defs common.ListenDefinitions

	defs.GitAuthor = GitAuthor
	defs.RabbitURL = *rabbitUrl

	defs.Handlers = make(map[string]common.RequestProcessor)
	defs.Handlers[common.RequestType_PR] = processPullRequest
	defs.Handlers[common.RequestType_PRSync] = processPullRequest

	log.Fatal(common.ProcessRabbitMQEvents(defs, orgs))
}