// Copyright F3 Authors
// SPDX-License-Identifier: MIT

package forgejo

import (
	"context"
	"fmt"
	"time"

	"code.forgejo.org/f3/gof3/v3/f3"
	helper_pullrequest "code.forgejo.org/f3/gof3/v3/forges/helpers/pullrequest"
	"code.forgejo.org/f3/gof3/v3/id"
	f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
	"code.forgejo.org/f3/gof3/v3/tree/generic"
	"code.forgejo.org/f3/gof3/v3/util"

	forgejo_sdk "code.forgejo.org/f3/gof3/v3/forges/forgejo/sdk"
)

type pullRequest struct {
	common
	h helper_pullrequest.Interface

	forgejoPullRequest *forgejo_sdk.PullRequest
	headRepository     *f3.Reference
	baseRepository     *f3.Reference
	fetchFunc          f3.PullRequestFetchFunc
}

var _ f3_tree.ForgeDriverInterface = &pullRequest{}

func newPullRequest() generic.NodeDriverInterface {
	r := &pullRequest{}
	r.h = helper_pullrequest.NewHelper(r)
	return r
}

func (o *pullRequest) SetNative(pullRequest any) {
	o.forgejoPullRequest = pullRequest.(*forgejo_sdk.PullRequest)
}

func (o *pullRequest) GetNativeID() string {
	return util.ToString(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 *forgejo_sdk.Repository, repositoryID string) *f3.Reference {
	if repository == nil {
		panic("unexpected nil repository")
	}
	ownerID := util.ToString(repository.Owner.ID)
	projectID := util.ToString(repository.ID)
	owners := o.getForge().getOwnersPath(ctx, ownerID)
	return f3_tree.NewRepositoryReference(owners.String(), ownerID, projectID, repositoryID)
}

func (o *pullRequest) referenceToRepository(reference *f3.Reference) *forgejo_sdk.Repository {
	var owner, project int64
	if reference.Get() == "../../repositories/vcs" {
		project = f3_tree.GetProjectID(o.GetNode())
		owner = f3_tree.GetOwnerID(o.GetNode())
	} else {
		p := generic.PathAbsolute(generic.NewNode, o.GetNode().GetCurrentPath().String(), reference.Get())
		o.Trace("reference %v - %v %v", reference.Get(), generic.ReadableString(o.GetNode().GetCurrentPath()), p)
		owner, project = p.OwnerAndProjectID()
	}
	return &forgejo_sdk.Repository{
		ID: project,
		Owner: &forgejo_sdk.User{
			ID: owner,
		},
	}
}

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(util.ToString(o.forgejoPullRequest.Milestone.ID))
	}

	createdAt := time.Time{}
	if o.forgejoPullRequest.Created != nil {
		createdAt = *o.forgejoPullRequest.Created
	}
	updatedAt := time.Time{}
	if o.forgejoPullRequest.Created != nil {
		updatedAt = *o.forgejoPullRequest.Updated
	}

	var (
		headRef string
		headSHA string
	)
	if o.forgejoPullRequest.Head != nil {
		headSHA = o.forgejoPullRequest.Head.Sha
		headRef = o.forgejoPullRequest.Head.Ref
	}

	var (
		baseRef string
		baseSHA string
	)
	if o.forgejoPullRequest.Base != nil {
		baseSHA = o.forgejoPullRequest.Base.Sha
		baseRef = o.forgejoPullRequest.Base.Ref
	}

	var mergeCommitSHA string
	if o.forgejoPullRequest.MergedCommitID != nil {
		mergeCommitSHA = *o.forgejoPullRequest.MergedCommitID
	}

	closedAt := o.forgejoPullRequest.Closed
	if o.forgejoPullRequest.Merged != nil {
		baseSHA = o.forgejoPullRequest.MergeBase
		if closedAt == nil {
			closedAt = o.forgejoPullRequest.Merged
		}
	}

	return &f3.PullRequest{
		Title:          o.forgejoPullRequest.Title,
		Common:         f3.NewCommon(o.GetNativeID()),
		PosterID:       f3_tree.NewUserReference(util.ToString(o.forgejoPullRequest.Poster.ID)),
		Content:        o.forgejoPullRequest.Body,
		State:          string(o.forgejoPullRequest.State),
		Created:        createdAt,
		Updated:        updatedAt,
		Closed:         closedAt,
		Milestone:      milestone,
		Merged:         o.forgejoPullRequest.HasMerged,
		MergedTime:     o.forgejoPullRequest.Merged,
		MergeCommitSHA: mergeCommitSHA,
		IsLocked:       o.forgejoPullRequest.IsLocked,
		Head: f3.PullRequestBranch{
			Ref:        headRef,
			SHA:        headSHA,
			Repository: o.headRepository,
		},
		Base: f3.PullRequestBranch{
			Ref:        baseRef,
			SHA:        baseSHA,
			Repository: o.baseRepository,
		},
		FetchFunc: o.fetchFunc,
	}
}

func (o *pullRequest) FromFormat(content f3.Interface) {
	pullRequest := content.(*f3.PullRequest)

	var milestone *forgejo_sdk.Milestone
	if pullRequest.Milestone != nil {
		milestone = &forgejo_sdk.Milestone{
			ID: pullRequest.Milestone.GetIDAsInt(),
		}
	}

	o.forgejoPullRequest = &forgejo_sdk.PullRequest{
		Index: util.ParseInt(pullRequest.GetID()),
		Poster: &forgejo_sdk.User{
			ID: pullRequest.PosterID.GetIDAsInt(),
		},
		Title:          pullRequest.Title,
		Body:           pullRequest.Content,
		Milestone:      milestone,
		State:          forgejo_sdk.StateType(pullRequest.State),
		IsLocked:       pullRequest.IsLocked,
		HasMerged:      pullRequest.Merged,
		Merged:         pullRequest.MergedTime,
		MergedCommitID: &pullRequest.MergeCommitSHA,
		Base: &forgejo_sdk.PRBranchInfo{
			Ref:        pullRequest.Base.Ref,
			Sha:        pullRequest.Base.SHA,
			Repository: o.referenceToRepository(pullRequest.Base.Repository),
		},
		Head: &forgejo_sdk.PRBranchInfo{
			Ref:        pullRequest.Head.Ref,
			Sha:        pullRequest.Head.SHA,
			Repository: o.referenceToRepository(pullRequest.Head.Repository),
		},
		Created: &pullRequest.Created,
		Updated: &pullRequest.Updated,
		Closed:  pullRequest.Closed,
	}
	if o.forgejoPullRequest.Base.Repository.ID != o.forgejoPullRequest.Head.Repository.ID {
		o.forgejoPullRequest.Head.Repository.Fork = true
	}
	o.fetchFunc = pullRequest.FetchFunc
}

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

	ownerName := f3_tree.GetOwnerName(o.GetNode())
	projectName := f3_tree.GetProjectName(o.GetNode())

	pr, resp, err := o.getClient().GetPullRequest(ownerName, projectName, node.GetID().Int64())
	if resp.StatusCode == 404 {
		return false
	}
	if err != nil {
		panic(fmt.Errorf("pullRequest %v %w", o, err))
	}

	o.headRepository = o.repositoryToReference(ctx, o.forgejoPullRequest.Head.Repository, f3.RepositoryNameDefault)
	o.baseRepository = o.repositoryToReference(ctx, o.forgejoPullRequest.Base.Repository, f3.RepositoryNameDefault)
	o.forgejoPullRequest = pr
	return true
}

func (o *pullRequest) GetPullRequestHead(ctx context.Context) string {
	return o.forgejoPullRequest.Head.Ref
}

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

func (o *pullRequest) SetFetchFunc(fetchFunc func(ctx context.Context, url, ref string)) {
	o.fetchFunc = fetchFunc
}

func (o *pullRequest) ensureHeadBase(ctx context.Context) (string, string, func()) {
	base := &f3.PullRequestBranch{
		Ref: o.forgejoPullRequest.Base.Ref,
		SHA: o.forgejoPullRequest.Base.Sha,
	}
	undoBase := o.h.EnsureBranch(ctx, base)

	head := &f3.PullRequestBranch{
		SHA: o.forgejoPullRequest.Head.Sha,
	}
	var undoHead func()
	if o.forgejoPullRequest.Head.Repository.Fork && o.forgejoPullRequest.Closed == nil {
		head.Ref = o.getForge().getOwnersName(ctx, util.ToString(o.forgejoPullRequest.Head.Repository.Owner.ID)) + ":" + o.forgejoPullRequest.Head.Ref
		undoHead = func() {}
	} else {
		head.Ref = o.forgejoPullRequest.Head.Ref
		undoHead = o.h.EnsureBranch(ctx, head)
	}

	return head.Ref, base.Ref, func() {
		undoBase()
		undoHead()
	}
}

func (o *pullRequest) Put(ctx context.Context) id.NodeID {
	node := o.GetNode()

	owner := f3_tree.GetOwnerName(node)
	project := f3_tree.GetProjectName(node)

	head, base, undoHeadBase := o.ensureHeadBase(ctx)
	defer undoHeadBase()

	o.maybeSudoID(ctx, o.forgejoPullRequest.Poster.ID)
	defer o.notSudo()

	options := forgejo_sdk.CreatePullRequestOption{
		Head:  head,
		Base:  base,
		Title: o.forgejoPullRequest.Title,
		Body:  o.forgejoPullRequest.Body,
	}

	pr, _, err := o.getClient().CreatePullRequest(owner, project, options)
	if err != nil {
		panic(fmt.Errorf("%+v: %w", options, err))
	}
	o.Trace("%s", o.GetNativeID())

	if o.forgejoPullRequest.State == forgejo_sdk.StateClosed {
		o.Trace("closing %s", o.GetNativeID())
		_, _, err := o.getClient().EditPullRequest(owner, project, pr.Index, forgejo_sdk.EditPullRequestOption{
			State: &o.forgejoPullRequest.State,
		})
		if err != nil {
			panic(fmt.Errorf("closing: %w", err))
		}
		pr.State = forgejo_sdk.StateClosed
	}

	o.forgejoPullRequest = pr
	o.headRepository = o.repositoryToReference(ctx, o.forgejoPullRequest.Head.Repository, f3.RepositoryNameDefault)
	o.baseRepository = o.repositoryToReference(ctx, o.forgejoPullRequest.Base.Repository, f3.RepositoryNameDefault)

	return id.NewNodeID(o.GetNativeID())
}

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

	owner := f3_tree.GetOwnerName(o.GetNode())
	project := f3_tree.GetProjectName(o.GetNode())

	o.maybeSudoID(ctx, o.forgejoPullRequest.Poster.ID)
	defer o.notSudo()

	_, _, err := o.getClient().EditPullRequest(owner, project, node.GetID().Int64(), forgejo_sdk.EditPullRequestOption{
		Title: o.forgejoPullRequest.Title,
		Body:  o.forgejoPullRequest.Body,
		State: &o.forgejoPullRequest.State,
	})
	if err != nil {
		panic(err)
	}
}

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())

	_, err := o.getClient().DeleteIssue(owner, project, node.GetID().Int64())
	if err != nil {
		panic(err)
	}
}
