package main

import (
	"errors"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"path"
	"slices"
	"strings"
	"time"

	transport "github.com/go-openapi/runtime/client"
	"src.opensuse.org/autogits/common"
	apiclient "src.opensuse.org/autogits/common/gitea-generated/client"
	"src.opensuse.org/autogits/common/gitea-generated/client/organization"
	"src.opensuse.org/autogits/common/gitea-generated/client/repository"
	"src.opensuse.org/autogits/common/gitea-generated/models"
)

const commandLineHelp = `
  SYNTAX
    devel-importer <obs-project> <gitea-org>`

func printHelp() {
	fmt.Println(commandLineHelp)
}

func runObsCommand(args ...string) ([]byte, error) {
	cmd := exec.Command("osc", args...)
	return cmd.Output()
}

func gitExec(pwd string, args ...string) string {
	cmd := exec.Command("git", args...)
	cmd.Dir = pwd
	str, err := cmd.CombinedOutput()

	if err != nil {
		fmt.Printf("git command failed: %v\n", args)
		fmt.Println(" err: %v", err)
		os.Exit(10)
	}
	return string(str)
}

func main() {
	if err := common.RequireGiteaSecretToken(); err != nil {
		fmt.Println("Missing GITEA_TOKEN")
		os.Exit(100)
	}

	if err := common.RequireObsSecretToken(); err != nil {
		fmt.Printf("Missing OBS_PASSWORD and/or OBS_USER\n")
		os.Exit(100)
	}

	webhookBase := os.Getenv("WEBHOOK_BASE")
	if webhookBase == "" {
		fmt.Printf("Missing WEBHOOK_BASE\n")
		os.Exit(100)
	}

	purgeOnly := flag.Bool("purge", false, "Purges package repositories. Use with caution")
	// revNew := flag.Int("nrevs", 20, "Number of new revisions in factory branch. Indicator of broken history import")
	flag.Parse()

	if flag.NArg() != 2 {
		printHelp()
		os.Exit(1)
	}

	prj := flag.Arg(0)
	org := flag.Arg(1)
	packageList, err := runObsCommand("ls", prj)

	if err != nil {
		fmt.Printf("Cannot list packages for project '%s'. Err: %v\n", prj, err)
		os.Exit(2)
	}
	packages := strings.Split(strings.TrimSpace(string(packageList)), "\n")
	fmt.Printf("%d packages: %s\n\n", len(packages), strings.Join(packages, " "))

	r := transport.New("src.opensuse.org", apiclient.DefaultBasePath, [](string){"https"})
	r.DefaultAuthentication = transport.BearerToken(common.GetGiteaToken())
	// r.SetDebug(true)
	client := apiclient.New(r, nil)

	if *purgeOnly {
		fmt.Printf("Purging repositories...\n")
		for _, pkg := range packages {
			client.Repository.RepoDelete(repository.NewRepoDeleteParams().WithOwner(org).WithRepo(pkg), r.DefaultAuthentication)
		}
		os.Exit(10)
	}

	oldPackageRepos := make([]*models.Repository, 0, len(packages))
	newPackages := make([]string, 0, len(packages))
	for _, pkg := range packages {
		repo, err := client.Repository.RepoGet(
			repository.NewRepoGetParams().
				WithDefaults().WithOwner("pool").WithRepo(pkg),
			r.DefaultAuthentication)

		if err != nil {
			if !errors.Is(err, &repository.RepoGetNotFound{}) {
				fmt.Println(err)
				os.Exit(3)
			}

			fmt.Printf("Cannot find package: %s\n", pkg)
			newPackages = append(newPackages, pkg)
		} else {
			oldPackageRepos = append(oldPackageRepos, repo.Payload)
		}
	}
	fmt.Printf("Num repos found: %d\n", len(oldPackageRepos))

	// add webhooks to prjgit-updater
	hooksReponse, err := client.Organization.OrgListHooks(
		organization.NewOrgListHooksParams().WithOrg(org),
		r.DefaultAuthentication,
	)
	if err != nil {
		fmt.Printf("Error fetching hooks for '%s'. Err: %v\n", org, err)
		os.Exit(4)
	}
	hooks := hooksReponse.Payload
	for _, hook := range hooks {
		fmt.Printf("hook %d: %#v", hook.ID, hook.Config)
	}
	/*
		hookActive := true
		hookType := models.CreateHookOptionTypeGitea
		client.Organization.OrgCreateHook(
			organization.NewOrgCreateHookParams().WithOrg(org).WithBody(&models.CreateHookOption{
				Active: &hookActive,
				Type:   &hookType,
				Config: models.CreateHookOptionConfig{
					"method":       "POST",
					"content_type": "application/json",
					"url":          webhookBase + "/prgit-updater",
				},
				Events: []string{
					"push",
				},
			}),
			r.DefaultAuthentication)
	*/

	// fork packags from pool
	cmd := exec.Command("./git-importer", func(r []*models.Repository) []string {
		ret := make([]string, len(r))
		for i := range r {
			ret[i] = r[i].Name
		}
		return ret
	}(oldPackageRepos)...)
	out, err := cmd.CombinedOutput()
	fmt.Print(string(out))
	if err != nil {
		fmt.Printf("Error returned by importer. Err: %v\n", err)
		os.Exit(14)
	}

	reposOK := true
	for i := range oldPackageRepos {
		pkg := oldPackageRepos[i]
		dir := path.Join("repos", pkg.Name)

		// add remote repos
		out := gitExec(dir, "remote", "show", "-n")
		if !slices.Contains(strings.Split(out, "\n"), "factory") {
			out := gitExec(dir, "remote", "add", "factory", pkg.CloneURL)
			if len(strings.TrimSpace(out)) > 1 {
				fmt.Println(out)
			}
		}
		if !slices.Contains(strings.Split(out, "\n"), "rpm") {
			out := gitExec(dir, "remote", "add", "rpm", "https://src.opensuse.org/rpm/"+pkg.Name+".git")
			if len(strings.TrimSpace(out)) > 1 {
				fmt.Println(out)
			}
		}

		out = gitExec(dir, "fetch", "--multiple", "factory", "rpm")
		if len(strings.TrimSpace(out)) > 1 {
			fmt.Println(out)
		}

		// check that nothing is broken with the update
		out = gitExec(dir, "rev-list", "factory")
		old_revs := strings.Split(out, "\n")
		out = gitExec(dir, "rev-list", "factory", "^factory/factory")
		added_revs := strings.Split(out, "\n")
		out = gitExec(dir, "rev-list", "factory", "^rpm/factory")
		added_rpm_revs := strings.Split(out, "\n")
		if len(added_revs) == len(old_revs) && len(added_rpm_revs) == len(old_revs) {
			fmt.Printf("Something is wrong with rev-ist for (len %d): %s\n", len(added_revs), pkg.Name)
			reposOK = false
		}
	}

	args := make([]string, 2, len(newPackages)+2)
	args[0] = "-p"
	args[1] = prj
	args = append(args, newPackages...)
	cmd = exec.Command("./git-importer", args...)
	out, err = cmd.CombinedOutput()
	fmt.Print(string(out))
	if err != nil {
		fmt.Printf("Error returned by importer. Err: %v\n", err)
		os.Exit(15)
	}

	if !reposOK {
		fmt.Printf("aborting import due to broken repos above...\n")
		os.Exit(100)
	}

	for _, pkg := range oldPackageRepos {
		// update package
		fork, err := client.Repository.CreateFork(repository.NewCreateForkParams().
			WithOwner("pool").
			WithRepo(pkg.Name).
			WithBody(&models.CreateForkOption{
				Organization: org,
			}), r.DefaultAuthentication)
		if err != nil {
			fmt.Printf("Error while trying to create fork from pool/%s. Err: %v\n", pkg.Name, err)
			os.Exit(10)
		}

		repo := fork.Payload
		repoList, err := client.Repository.RepoListBranches(
			repository.NewRepoListBranchesParams().WithOwner(org).WithRepo(pkg.Name),
			r.DefaultAuthentication,
		)
		if err != nil {
			fmt.Printf("Cannot get list of branches for forked repo: %s/%s\n", org, pkg.Name)
			os.Exit(11)
		}
		priorityBranches := []string{
			"devel",
			"factory",
		}
		idx := len(priorityBranches)
		for _, branch := range repoList.Payload {
			i := slices.Index(priorityBranches, branch.Name)
			if i > -1 && i < idx {
				idx = i
			}
		}

		branchName := priorityBranches[idx]
		dir := path.Join("repos", pkg.Name)
		remotes := gitExec(dir, "remote", "show")
		if !slices.Contains(strings.Split(remotes, "\n"), "devel") {
			gitExec(dir, "remote", "add", "devel", repo.SSHURL)
		}
		gitExec(dir, "branch", "main", "-f", branchName)
		time.Sleep(2 * time.Second)
		gitExec(dir, "push", "devel", "main")
		time.Sleep(2 * time.Second)

		_, err = client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(repo.Name).WithBody(&models.EditRepoOption{
			DefaultBranch:     "main",
			DefaultMergeStyle: "fast-forward-only",
		}), r.DefaultAuthentication)

		if err != nil {
			fmt.Printf("Failed to set default branch for package fork: %s/%s  Err: %v", repo.Owner.UserName, repo.Name, err)
			os.Exit(12)
		}
	}

	for _, pkg := range newPackages {
		ret, err := client.Organization.CreateOrgRepo(organization.NewCreateOrgRepoParams().WithOrg(org).WithBody(
			&models.CreateRepoOption{
				ObjectFormatName: "sha256",
				AutoInit:         false,
				Name:             &pkg,
				DefaultBranch:    "main",
			}),
			r.DefaultAuthentication,
		)

		if err != nil {
			fmt.Printf("Error creating new package repository: %s  Err: %v", pkg, err)
			os.Exit(13)
		}

		repo := ret.Payload
		dir := path.Join("repos", pkg)
		remotes := gitExec(dir, "remote", "show")
		if !slices.Contains(strings.Split(remotes, "\n"), "devel") {
			gitExec(dir, "remote", "add", "devel", repo.SSHURL)
		}
		gitExec(dir, "branch", "main", "-f", "factory")
		time.Sleep(2 * time.Second)
		gitExec(dir, "push", "devel", "main")
		time.Sleep(2 * time.Second)

		_, err = client.Repository.RepoEdit(repository.NewRepoEditParams().WithOwner(org).WithRepo(pkg).WithBody(&models.EditRepoOption{
			DefaultBranch:     "main",
			DefaultMergeStyle: "fast-forward-only",
		}), r.DefaultAuthentication)

		if err != nil {
			fmt.Printf("Failed to set default branch for package fork: %s/%s  Err: %v", repo.Owner.UserName, repo.Name, err)
			os.Exit(14)
		}
	}
}
