// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package driver

import (
	"context"
	"fmt"
	"time"

	"forgejo.org/models/db"
	issues_model "forgejo.org/models/issues"
	repo_model "forgejo.org/models/repo"
	user_model "forgejo.org/models/user"
	"forgejo.org/modules/git"
	"forgejo.org/modules/timeutil"
	issue_service "forgejo.org/services/issue"

	"code.forgejo.org/f3/gof3/v3/f3"
	f3_id "code.forgejo.org/f3/gof3/v3/id"
	f3_kind "code.forgejo.org/f3/gof3/v3/kind"
	f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
	"code.forgejo.org/f3/gof3/v3/tree/generic"
	f3_util "code.forgejo.org/f3/gof3/v3/util"
)

var _ f3_tree.ForgeDriverInterface = &pullRequest{}

type pullRequest struct {
	common

	forgejoPullRequest *issues_model.Issue
	headRepository     *f3.Reference
	baseRepository     *f3.Reference
	fetchFunc          f3.PullRequestFetchFunc
}

func (o *pullRequest) SetNative(pullRequest any) {
	o.forgejoPullRequest = pullRequest.(*issues_model.Issue)
}

func (o *pullRequest) GetNativeID() string {
	return fmt.Sprintf("%d", o.forgejoPullRequest.Index)
}

func (o *pullRequest) NewFormat() f3.Interface {
	node := o.GetNode()
	return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
}

func (o *pullRequest) repositoryToReference(ctx context.Context, repository *repo_model.Repository) *f3.Reference {
	if repository == nil {
		panic("unexpected nil repository")
	}
	forge := o.getTree().GetRoot().GetChild(f3_id.NewNodeID(f3_kind.KindForge)).GetDriver().(*forge)
	owners := forge.getOwnersPath(ctx, fmt.Sprintf("%d", repository.OwnerID))
	return f3_tree.NewRepositoryReference(owners.String(), f3_util.ToString(repository.OwnerID), f3_util.ToString(repository.ID), f3.RepositoryNameDefault)
}

func (o *pullRequest) referenceToRepository(reference *f3.Reference) int64 {
	var project int64
	if reference.Get() == "../../repositories/vcs" {
		project = f3_tree.GetProjectID(o.GetNode())
	} else {
		p := generic.PathAbsolute(generic.NewNode, o.GetNode().GetCurrentPath().String(), reference.Get())
		o.Trace("%v %v", o.GetNode().GetCurrentPath().String(), p)
		_, project = p.OwnerAndProjectID()
	}
	return project
}

func (o *pullRequest) ToFormat() f3.Interface {
	if o.forgejoPullRequest == nil {
		return o.NewFormat()
	}

	var milestone *f3.Reference
	if o.forgejoPullRequest.Milestone != nil {
		milestone = f3_tree.NewIssueMilestoneReference(f3_util.ToString(o.forgejoPullRequest.Milestone.ID))
	}

	var mergedTime *time.Time
	if o.forgejoPullRequest.PullRequest.HasMerged {
		mergedTime = o.forgejoPullRequest.PullRequest.MergedUnix.AsTimePtr()
	}

	var closedTime *time.Time
	if o.forgejoPullRequest.IsClosed {
		closedTime = o.forgejoPullRequest.ClosedUnix.AsTimePtr()
	}

	makePullRequestBranch := func(repo *repo_model.Repository, branch string) f3.PullRequestBranch {
		r, err := git.OpenRepository(context.Background(), repo.RepoPath())
		if err != nil {
			panic(err)
		}
		defer r.Close()

		b, err := r.GetBranch(branch)
		if err != nil {
			panic(err)
		}

		c, err := b.GetCommit()
		if err != nil {
			panic(err)
		}

		return f3.PullRequestBranch{
			Ref: branch,
			SHA: c.ID.String(),
		}
	}
	if err := o.forgejoPullRequest.PullRequest.LoadHeadRepo(db.DefaultContext); err != nil {
		panic(err)
	}
	head := makePullRequestBranch(o.forgejoPullRequest.PullRequest.HeadRepo, o.forgejoPullRequest.PullRequest.HeadBranch)
	head.Repository = o.headRepository
	if err := o.forgejoPullRequest.PullRequest.LoadBaseRepo(db.DefaultContext); err != nil {
		panic(err)
	}
	base := makePullRequestBranch(o.forgejoPullRequest.PullRequest.BaseRepo, o.forgejoPullRequest.PullRequest.BaseBranch)
	base.Repository = o.baseRepository

	return &f3.PullRequest{
		Common:         f3.NewCommon(o.GetNativeID()),
		PosterID:       f3_tree.NewUserReference(f3_util.ToString(o.forgejoPullRequest.Poster.ID)),
		Title:          o.forgejoPullRequest.Title,
		Content:        o.forgejoPullRequest.Content,
		Milestone:      milestone,
		State:          string(o.forgejoPullRequest.State()),
		IsLocked:       o.forgejoPullRequest.IsLocked,
		Created:        o.forgejoPullRequest.CreatedUnix.AsTime(),
		Updated:        o.forgejoPullRequest.UpdatedUnix.AsTime(),
		Closed:         closedTime,
		Merged:         o.forgejoPullRequest.PullRequest.HasMerged,
		MergedTime:     mergedTime,
		MergeCommitSHA: o.forgejoPullRequest.PullRequest.MergedCommitID,
		Head:           head,
		Base:           base,
		FetchFunc:      o.fetchFunc,
	}
}

func (o *pullRequest) FromFormat(content f3.Interface) {
	pullRequest := content.(*f3.PullRequest)
	var milestone *issues_model.Milestone
	if pullRequest.Milestone != nil {
		milestone = &issues_model.Milestone{
			ID: pullRequest.Milestone.GetIDAsInt(),
		}
	}

	o.headRepository = pullRequest.Head.Repository
	o.baseRepository = pullRequest.Base.Repository
	pr := issues_model.PullRequest{
		HeadBranch: pullRequest.Head.Ref,
		HeadRepoID: o.referenceToRepository(o.headRepository),
		BaseBranch: pullRequest.Base.Ref,
		BaseRepoID: o.referenceToRepository(o.baseRepository),

		MergeBase: pullRequest.Base.SHA,
		Index:     f3_util.ParseInt(pullRequest.GetID()),
		HasMerged: pullRequest.Merged,
	}

	o.forgejoPullRequest = &issues_model.Issue{
		Index:    f3_util.ParseInt(pullRequest.GetID()),
		PosterID: pullRequest.PosterID.GetIDAsInt(),
		Poster: &user_model.User{
			ID: pullRequest.PosterID.GetIDAsInt(),
		},
		Title:       pullRequest.Title,
		Content:     pullRequest.Content,
		Milestone:   milestone,
		IsClosed:    pullRequest.State == f3.PullRequestStateClosed,
		CreatedUnix: timeutil.TimeStamp(pullRequest.Created.Unix()),
		UpdatedUnix: timeutil.TimeStamp(pullRequest.Updated.Unix()),
		IsLocked:    pullRequest.IsLocked,
		PullRequest: &pr,
		IsPull:      true,
	}

	if pullRequest.Closed != nil {
		o.forgejoPullRequest.ClosedUnix = timeutil.TimeStamp(pullRequest.Closed.Unix())
	}
}

func (o *pullRequest) Get(ctx context.Context) bool {
	node := o.GetNode()
	o.Trace("%s", node.GetID())

	project := f3_tree.GetProjectID(o.GetNode())
	id := node.GetID().Int64()

	issue, err := issues_model.GetIssueByIndex(ctx, project, id)
	if issues_model.IsErrIssueNotExist(err) {
		return false
	}
	if err != nil {
		panic(fmt.Errorf("issue %v %w", id, err))
	}
	if err := issue.LoadAttributes(ctx); err != nil {
		panic(err)
	}
	if err := issue.PullRequest.LoadHeadRepo(ctx); err != nil {
		panic(err)
	}
	o.headRepository = o.repositoryToReference(ctx, issue.PullRequest.HeadRepo)
	if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil {
		panic(err)
	}
	o.baseRepository = o.repositoryToReference(ctx, issue.PullRequest.BaseRepo)

	o.forgejoPullRequest = issue
	o.Trace("ID = %s", o.forgejoPullRequest.ID)
	return true
}

func (o *pullRequest) Patch(ctx context.Context) {
	node := o.GetNode()
	project := f3_tree.GetProjectID(o.GetNode())
	id := node.GetID().Int64()
	o.Trace("repo_id = %d, index = %d", project, id)
	if _, err := db.GetEngine(ctx).Where("`repo_id` = ? AND `index` = ?", project, id).Cols("name", "content").Update(o.forgejoPullRequest); err != nil {
		panic(fmt.Errorf("%v %v", o.forgejoPullRequest, err))
	}
}

func (o *pullRequest) GetPullRequestPushRefs() []string {
	return []string{
		fmt.Sprintf("refs/f3/%s/head", o.GetNativeID()),
		fmt.Sprintf("refs/pull/%s/head", o.GetNativeID()),
	}
}

func (o *pullRequest) GetPullRequestRef() string {
	return fmt.Sprintf("refs/pull/%s/head", o.GetNativeID())
}

func (o *pullRequest) Put(ctx context.Context) f3_id.NodeID {
	node := o.GetNode()
	o.Trace("%s", node.GetID())

	o.forgejoPullRequest.RepoID = f3_tree.GetProjectID(o.GetNode())

	ctx, committer, err := db.TxContext(ctx)
	if err != nil {
		panic(err)
	}
	defer committer.Close()

	idx, err := db.GetNextResourceIndex(ctx, "issue_index", o.forgejoPullRequest.RepoID)
	if err != nil {
		panic(fmt.Errorf("generate issue index failed: %w", err))
	}
	o.forgejoPullRequest.Index = idx

	sess := db.GetEngine(ctx)

	if _, err = sess.NoAutoTime().Insert(o.forgejoPullRequest); err != nil {
		panic(err)
	}

	pr := o.forgejoPullRequest.PullRequest
	pr.Index = o.forgejoPullRequest.Index
	pr.IssueID = o.forgejoPullRequest.ID
	pr.HeadRepoID = o.referenceToRepository(o.headRepository)
	if pr.HeadRepoID == 0 {
		panic(fmt.Errorf("HeadRepoID == 0 in %v", pr))
	}
	pr.BaseRepoID = o.referenceToRepository(o.baseRepository)
	if pr.BaseRepoID == 0 {
		panic(fmt.Errorf("BaseRepoID == 0 in %v", pr))
	}

	if _, err = sess.NoAutoTime().Insert(pr); err != nil {
		panic(err)
	}

	if err = committer.Commit(); err != nil {
		panic(fmt.Errorf("Commit: %w", err))
	}

	if err := pr.LoadBaseRepo(ctx); err != nil {
		panic(err)
	}
	if err := pr.LoadHeadRepo(ctx); err != nil {
		panic(err)
	}

	o.Trace("pullRequest created %d/%d", o.forgejoPullRequest.ID, o.forgejoPullRequest.Index)
	return f3_id.NewNodeID(o.forgejoPullRequest.Index)
}

func (o *pullRequest) Delete(ctx context.Context) {
	node := o.GetNode()
	o.Trace("%s", node.GetID())

	owner := f3_tree.GetOwnerName(o.GetNode())
	project := f3_tree.GetProjectName(o.GetNode())
	repoPath := repo_model.RepoPath(owner, project)
	gitRepo, err := git.OpenRepository(ctx, repoPath)
	if err != nil {
		panic(err)
	}
	defer gitRepo.Close()

	doer, err := user_model.GetAdminUser(ctx)
	if err != nil {
		panic(fmt.Errorf("GetAdminUser %w", err))
	}

	if err := issue_service.DeleteIssue(ctx, doer, gitRepo, o.forgejoPullRequest); err != nil {
		panic(err)
	}
}

func newPullRequest() generic.NodeDriverInterface {
	return &pullRequest{}
}
