package common

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"path"
	"slices"
	"strings"

	"src.opensuse.org/autogits/common/gitea-generated/models"
)

type PRInfo struct {
	PR      *models.PullRequest
	Reviews *PRReviews
}

type PRSet struct {
	PRs    []PRInfo
	Config *AutogitConfig
}

func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []PRInfo, config *AutogitConfig) ([]PRInfo, error) {
	for _, p := range currentSet {
		if pr.Index == p.PR.Index && pr.Base.Repo.Name == p.PR.Base.Repo.Name && pr.Base.Repo.Owner.UserName == p.PR.Base.Repo.Owner.UserName {
			return nil, nil
		}
	}
	retSet := []PRInfo{PRInfo{PR: pr}}

	// only need to extact there on PrjGit PR
	if pr.Base.Repo.Name == config.GitProjectName && pr.Base.Repo.Owner.UserName == config.Organization {
		_, refPRs := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(pr.Body)))
		for _, prdata := range refPRs {
			pr, err := gitea.GetPullRequest(prdata.Org, prdata.Repo, prdata.Num)
			if err != nil {
				return nil, err
			}
			data, err := readPRData(gitea, pr, slices.Concat(currentSet, retSet), config)
			if err != nil {
				return nil, err
			}
			retSet = slices.Concat(retSet, data)
		}
	}

	return retSet, nil
}

func FetchPRSet(gitea GiteaPRFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
	var pr *models.PullRequest
	var err error

	if org != config.Organization || repo != config.GitProjectName {
		if pr, err = gitea.GetAssociatedPrjGitPR(config.Organization, config.GitProjectName, org, repo, num); err != nil {
			return nil, err
		}

		if pr == nil {
			if pr, err = gitea.GetPullRequest(org, repo, num); err != nil {
				return nil, err
			}
		}
	} else {
		if pr, err = gitea.GetPullRequest(org, repo, num); err != nil {
			return nil, err
		}
	}

	prs, err := readPRData(gitea, pr, nil, config)
	if err != nil {
		return nil, err
	}

	return &PRSet{PRs: prs, Config: config}, nil
}

func (rs *PRSet) IsPrjGitPR(pr *models.PullRequest) bool {
	return pr.Base.Repo.Name == rs.Config.GitProjectName && pr.Base.Repo.Owner.UserName == rs.Config.Organization
}

func (rs *PRSet) GetPrjGitPR() (*models.PullRequest, error) {
	var ret *models.PullRequest

	for _, prinfo := range rs.PRs {
		if rs.IsPrjGitPR(prinfo.PR) {
			if ret == nil {
				ret = prinfo.PR
			} else {
				return nil, errors.New("Multiple PrjGit PRs in one review set")
			}
		}
	}

	if ret != nil {
		return ret, nil
	}

	return nil, errors.New("No PrjGit PR found")
}

func (rs *PRSet) IsConsistent() bool {
	prjpr, err := rs.GetPrjGitPR()
	if err != nil {
		return false
	}
	_, prjpr_set := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(prjpr.Body)))
	if len(prjpr_set) != len(rs.PRs)-1 { // 1 to many mapping
		return false
	}

next_rs:
	for _, prinfo := range rs.PRs {
		if prjpr == prinfo.PR {
			continue
		}

		for _, pr := range prjpr_set {
			if prinfo.PR.Base.Repo.Owner.UserName == pr.Org && prinfo.PR.Base.Repo.Name == pr.Repo && prinfo.PR.Index == pr.Num {
				continue next_rs
			}
		}
		return false
	}
	return true
}

func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintainers MaintainershipData) error {
	configReviewers := ParseReviewers(rs.Config.Reviewers)

	for _, pr := range rs.PRs {
		reviewers := []string{}
		if rs.IsPrjGitPR(pr.PR) {
			reviewers = configReviewers.Prj
			if len(rs.PRs) == 1 {
				reviewers = slices.Concat(reviewers, maintainers.ListProjectMaintainers())
			}
		} else {
			pkg := pr.PR.Base.Repo.Name
			reviewers = slices.Concat(configReviewers.Pkg, maintainers.ListProjectMaintainers(), maintainers.ListPackageMaintainers(pkg))
		}

		// submitters do not need to review their own work
		if idx := slices.Index(reviewers, pr.PR.User.UserName); idx != -1 {
			reviewers = slices.Delete(reviewers, idx, idx+1)
		}

		// remove reviewers that were already requested and are not stale
		reviews, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
		if err != nil {
			return err
		}

		for idx := 0; idx < len(reviewers); {
			user := reviewers[idx]
			if reviews.HasPendingReviewBy(user) || reviews.IsReviewedBy(user) {
				reviewers = slices.Delete(reviewers, idx, idx+1)
			} else {
				idx++
			}
		}

		// get maintainers associated with the PR too
		if len(reviewers) > 0 {
			if _, err := gitea.RequestReviews(pr.PR, reviewers...); err != nil {
				return fmt.Errorf("Cannot create reviews on %s/%s#%d for [%s]: %w", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index, strings.Join(reviewers, ", "), err)
			}
		}
	}
	return nil
}

func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData) bool {
	configReviewers := ParseReviewers(rs.Config.Reviewers)

	is_reviewed := false
	for _, pr := range rs.PRs {
		var reviewers []string
		var pkg string
		if rs.IsPrjGitPR(pr.PR) {
			reviewers = configReviewers.Prj
			pkg = ""
		} else {
			reviewers = configReviewers.Pkg
			pkg = pr.PR.Base.Repo.Name
		}

		r, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
		if err != nil {
			return false
		}
		is_reviewed = r.IsApproved()
		if !is_reviewed {
			return false
		}

		if is_reviewed = maintainers.IsApproved(pkg, r.reviews); !is_reviewed {
			return false
		}
	}
	return is_reviewed
}

func (rs *PRSet) Merge(author, email string) error {
	prjgit, err := rs.GetPrjGitPR()
	if err != nil {
		return err
	}

	gh := GitHandlerGeneratorImpl{}
	git, err := gh.CreateGitHandler(author, email, prjgit.Base.Name)
	if err != nil {
		return err
	}
	git.GitExecOrPanic("", "clone", "--depth", "1", prjgit.Base.Repo.SSHURL, DefaultGitPrj)
	git.GitExecOrPanic(DefaultGitPrj, "fetch", "origin", prjgit.Base.Sha, prjgit.Head.Sha)

	// if other changes merged, check if we have conflicts
	rev := strings.TrimSpace(git.GitExecWithOutputOrPanic(DefaultGitPrj, "merge-base", "HEAD", prjgit.Base.Sha, prjgit.Head.Sha))
	if rev != prjgit.Base.Sha {
		return fmt.Errorf("Base.Sha (%s) not yet merged into project-git. Aborting merge.", prjgit.Base.Sha)
	}
	/*
		rev := git.GitExecWithOutputOrPanic(common.DefaultGitPrj, "rev-list", "-1", "HEAD")
		if rev != prjgit.Base.Sha {
			panic("FIXME")
		}
	*/
	msg := "merging"

	err = git.GitExec(DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha)
	if err != nil {
		status, statusErr := git.GitStatus(DefaultGitPrj)
		if statusErr != nil {
			return fmt.Errorf("Failed to merge: %w . Status also failed: %w", err, statusErr)
		}

		// we can only resolve conflicts with .gitmodules
		for _, s := range status {
			if s.Status == GitStatus_Unmerged {
				if s.Path != ".gitmodules" {
					return err
				}

				submodules, err := git.GitSubmoduleList(DefaultGitPrj, "MERGE_HEAD")
				if err != nil {
					return fmt.Errorf("Failed to fetch submodules during merge resolution: %w", err)
				}
				s1, err := git.GitExecWithOutput(DefaultGitPrj, "cat-file", "blob", s.States[0])
				if err != nil {
					return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
				}
				s2, err := git.GitExecWithOutput(DefaultGitPrj, "cat-file", "blob", s.States[1])
				if err != nil {
					return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
				}
				s3, err := git.GitExecWithOutput(DefaultGitPrj, "cat-file", "blob", s.States[2])
				if err != nil {
					return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
				}

				subs1, err := ParseSubmodulesFile(strings.NewReader(s1))
				if err != nil {
					return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
				}
				subs2, err := ParseSubmodulesFile(strings.NewReader(s2))
				if err != nil {
					return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
				}
				subs3, err := ParseSubmodulesFile(strings.NewReader(s3))
				if err != nil {
					return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
				}

				// merge from subs3 (target), subs1 (orig), subs2 (2-nd base that is missing from target base)
				// this will update submodules
				mergedSubs := slices.Concat(subs1, subs2, subs3)

				var filteredSubs []Submodule = make([]Submodule, 0, max(len(subs1), len(subs2), len(subs3)))
			nextSub:
				for subName := range submodules {

					for i := range mergedSubs {
						if path.Base(mergedSubs[i].Path) == subName {
							filteredSubs = append(filteredSubs, mergedSubs[i])
							continue nextSub
						}
					}
					return fmt.Errorf("Cannot find submodule for path: %s", subName)
				}

				out, err := os.Create(path.Join(git.GetPath(), DefaultGitPrj, ".gitmodules"))
				if err != nil {
					return fmt.Errorf("Can't open .gitmodules for writing: %w", err)
				}
				if err = WriteSubmodules(filteredSubs, out); err != nil {
					return fmt.Errorf("Can't write .gitmodules: %w", err)
				}
				if out.Close(); err != nil {
					return fmt.Errorf("Can't close .gitmodules: %w", err)
				}

				os.CopyFS("/tmp/test", os.DirFS(git.GetPath()))

				git.GitExecOrPanic(DefaultGitPrj, "add", ".gitmodules")
				git.GitExecOrPanic(DefaultGitPrj, "-c", "core.editor=true", "merge", "--continue")
			}
		}
	}

	// FF all non-prj git
	for _, prinfo := range rs.PRs {
		if rs.IsPrjGitPR(prinfo.PR) {
			continue
		}
		git.GitExecOrPanic("", "clone", prinfo.PR.Base.Repo.SSHURL, prinfo.PR.Base.Name)
		git.GitExecOrPanic(prinfo.PR.Base.Name, "fetch", "origin", prinfo.PR.Head.Sha)
		git.GitExecOrPanic(prinfo.PR.Base.Name, "merge", "--ff", prinfo.PR.Head.Sha)
	}

	// push changes
	git.GitExecOrPanic(DefaultGitPrj, "push", "origin")
	for _, prinfo := range rs.PRs {
		if rs.IsPrjGitPR(prinfo.PR) {
			continue
		}
		git.GitExecOrPanic(prinfo.PR.Base.Name, "push", "origin")
	}

	return nil
}
