// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package convert

import (
	stdCtx "context"
	"time"

	"forgejo.org/models"
	"forgejo.org/models/db"
	"forgejo.org/models/perm"
	access_model "forgejo.org/models/perm/access"
	repo_model "forgejo.org/models/repo"
	unit_model "forgejo.org/models/unit"
	"forgejo.org/modules/log"
	api "forgejo.org/modules/structs"
	"forgejo.org/services/context"
)

// ToRepo converts a Repository to api.Repository
func ToRepo(ctx stdCtx.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission) *api.Repository {
	return innerToRepo(ctx, repo, permissionInRepo, false)
}

func innerToRepo(ctx stdCtx.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository {
	var parent *api.Repository

	if permissionInRepo.Units == nil && permissionInRepo.UnitsMode == nil {
		// If Units and UnitsMode are both nil, it means that it's a hard coded permission,
		// like access_model.Permission{AccessMode: perm.AccessModeAdmin}.
		// So we need to load units for the repo, or UnitAccessMode will always return perm.AccessModeNone.
		_ = repo.LoadUnits(ctx) // the error is not important, so ignore it
		permissionInRepo.Units = repo.Units
	}

	cloneLink := repo.CloneLink()
	permission := &api.Permission{
		Admin: permissionInRepo.AccessMode >= perm.AccessModeAdmin,
		Push:  permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeWrite,
		Pull:  permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead,
	}
	if !isParent {
		err := repo.GetBaseRepo(ctx)
		if err != nil {
			return nil
		}
		if repo.BaseRepo != nil {
			// FIXME: The permission of the parent repo is not correct.
			//        It's the permission of the current repo, so it's probably different from the parent repo.
			//        But there isn't a good way to get the permission of the parent repo, because the doer is not passed in.
			//        Use the permission of the current repo to keep the behavior consistent with the old API.
			//        Maybe the right way is setting the permission of the parent repo to nil, empty is better than wrong.
			parent = innerToRepo(ctx, repo.BaseRepo, permissionInRepo, true)
		}
	}

	// check enabled/disabled units
	hasIssues := false
	var externalTracker *api.ExternalTracker
	var internalTracker *api.InternalTracker
	if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err == nil {
		config := unit.IssuesConfig()
		hasIssues = true
		internalTracker = &api.InternalTracker{
			EnableTimeTracker:                config.EnableTimetracker,
			AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
			EnableIssueDependencies:          config.EnableDependencies,
		}
	} else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalTracker); err == nil {
		config := unit.ExternalTrackerConfig()
		hasIssues = true
		externalTracker = &api.ExternalTracker{
			ExternalTrackerURL:           config.ExternalTrackerURL,
			ExternalTrackerFormat:        config.ExternalTrackerFormat,
			ExternalTrackerStyle:         config.ExternalTrackerStyle,
			ExternalTrackerRegexpPattern: config.ExternalTrackerRegexpPattern,
		}
	}
	hasWiki := false
	globallyEditableWiki := false
	var externalWiki *api.ExternalWiki
	if wikiUnit, err := repo.GetUnit(ctx, unit_model.TypeWiki); err == nil {
		hasWiki = true
		if wikiUnit.DefaultPermissions == repo_model.UnitAccessModeWrite {
			globallyEditableWiki = true
		}
	} else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalWiki); err == nil {
		hasWiki = true
		config := unit.ExternalWikiConfig()
		externalWiki = &api.ExternalWiki{
			ExternalWikiURL: config.ExternalWikiURL,
		}
	}
	hasPullRequests := false
	ignoreWhitespaceConflicts := false
	allowMerge := false
	allowRebase := false
	allowRebaseMerge := false
	allowSquash := false
	allowFastForwardOnly := false
	allowRebaseUpdate := false
	defaultDeleteBranchAfterMerge := false
	defaultMergeStyle := repo_model.MergeStyleMerge
	defaultUpdateStyle := repo_model.UpdateStyleMerge
	defaultAllowMaintainerEdit := false
	if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
		config := unit.PullRequestsConfig()
		hasPullRequests = true
		ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts
		allowMerge = config.AllowMerge
		allowRebase = config.AllowRebase
		allowRebaseMerge = config.AllowRebaseMerge
		allowSquash = config.AllowSquash
		allowFastForwardOnly = config.AllowFastForwardOnly
		allowRebaseUpdate = config.AllowRebaseUpdate
		defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
		defaultMergeStyle = config.GetDefaultMergeStyle()
		defaultUpdateStyle = config.GetDefaultUpdateStyle()
		defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
	}
	hasProjects := false
	if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
		hasProjects = true
	}

	hasReleases := false
	if _, err := repo.GetUnit(ctx, unit_model.TypeReleases); err == nil {
		hasReleases = true
	}

	hasPackages := false
	if _, err := repo.GetUnit(ctx, unit_model.TypePackages); err == nil {
		hasPackages = true
	}

	hasActions := false
	if _, err := repo.GetUnit(ctx, unit_model.TypeActions); err == nil {
		hasActions = true
	}

	if err := repo.LoadOwner(ctx); err != nil {
		return nil
	}

	numReleases, _ := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
		IncludeDrafts: false,
		IncludeTags:   false,
		RepoID:        repo.ID,
	})

	mirrorInterval := ""
	var mirrorUpdated time.Time
	if repo.IsMirror {
		pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
		if err == nil {
			mirrorInterval = pullMirror.Interval.String()
			mirrorUpdated = pullMirror.UpdatedUnix.AsTime()
		}
	}

	var transfer *api.RepoTransfer
	if repo.Status == repo_model.RepositoryPendingTransfer {
		t, err := models.GetPendingRepositoryTransfer(ctx, repo)
		if err != nil && !models.IsErrNoPendingTransfer(err) {
			log.Warn("GetPendingRepositoryTransfer: %v", err)
		} else {
			if err := t.LoadAttributes(ctx); err != nil {
				log.Warn("LoadAttributes of RepoTransfer: %v", err)
			} else {
				transfer = ToRepoTransfer(ctx, t)
			}
		}
	}

	var language string
	if repo.PrimaryLanguage != nil {
		language = repo.PrimaryLanguage.Language
	}

	repoAPIURL := repo.APIURL()

	// Calculate the effective permission for `ToUserWithAccessMode` for the repo owner.  When accessing a public repo,
	// permissionInRepo.AccessMode will be AccessModeRead even for an anonymous user -- in that case, downgrade
	// `ownerViewPerms` to `AccessModeNone`.  `innerToRepo` doesn't have great access to recognize an anonymous user, so
	// the best-effort made here is to check if `ctx` is an `APIContext`.
	ownerViewPerms := permissionInRepo.AccessMode
	apiCtx, ok := ctx.(*context.APIContext)
	if ok && apiCtx.Doer == nil {
		ownerViewPerms = perm.AccessModeNone
	}

	return &api.Repository{
		ID:                            repo.ID,
		Owner:                         ToUserWithAccessMode(ctx, repo.Owner, ownerViewPerms),
		Name:                          repo.Name,
		FullName:                      repo.FullName(),
		Description:                   repo.Description,
		Private:                       repo.IsPrivate,
		Template:                      repo.IsTemplate,
		Empty:                         repo.IsEmpty,
		Archived:                      repo.IsArchived,
		Size:                          int(repo.Size / 1024),
		Fork:                          repo.IsFork,
		Parent:                        parent,
		Mirror:                        repo.IsMirror,
		HTMLURL:                       repo.HTMLURL(),
		URL:                           repoAPIURL,
		SSHURL:                        cloneLink.SSH,
		CloneURL:                      cloneLink.HTTPS,
		OriginalURL:                   repo.SanitizedOriginalURL(),
		Website:                       repo.Website,
		Language:                      language,
		LanguagesURL:                  repoAPIURL + "/languages",
		Stars:                         repo.NumStars,
		Forks:                         repo.NumForks,
		Watchers:                      repo.NumWatches,
		OpenIssues:                    repo.NumOpenIssues(ctx),
		OpenPulls:                     repo.NumOpenPulls(ctx),
		Releases:                      int(numReleases),
		DefaultBranch:                 repo.DefaultBranch,
		Created:                       repo.CreatedUnix.AsTime(),
		Updated:                       repo.UpdatedUnix.AsTime(),
		ArchivedAt:                    repo.ArchivedUnix.AsTime(),
		Permissions:                   permission,
		HasIssues:                     hasIssues,
		ExternalTracker:               externalTracker,
		InternalTracker:               internalTracker,
		HasWiki:                       hasWiki,
		HasWikiContents:               repo.HasWiki(),
		WikiBranch:                    repo.WikiBranch,
		WikiSSHURL:                    repo.WikiCloneLink().SSH,
		WikiCloneURL:                  repo.WikiCloneLink().HTTPS,
		GloballyEditableWiki:          globallyEditableWiki,
		HasProjects:                   hasProjects,
		HasReleases:                   hasReleases,
		HasPackages:                   hasPackages,
		HasActions:                    hasActions,
		ExternalWiki:                  externalWiki,
		HasPullRequests:               hasPullRequests,
		IgnoreWhitespaceConflicts:     ignoreWhitespaceConflicts,
		AllowMerge:                    allowMerge,
		AllowRebase:                   allowRebase,
		AllowRebaseMerge:              allowRebaseMerge,
		AllowSquash:                   allowSquash,
		AllowFastForwardOnly:          allowFastForwardOnly,
		AllowRebaseUpdate:             allowRebaseUpdate,
		DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
		DefaultMergeStyle:             string(defaultMergeStyle),
		DefaultUpdateStyle:            string(defaultUpdateStyle),
		DefaultAllowMaintainerEdit:    defaultAllowMaintainerEdit,
		AvatarURL:                     repo.AvatarLink(ctx),
		Internal:                      !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
		MirrorInterval:                mirrorInterval,
		MirrorUpdated:                 mirrorUpdated,
		RepoTransfer:                  transfer,
		Topics:                        repo.Topics,
		ObjectFormatName:              repo.ObjectFormatName,
	}
}

// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer
func ToRepoTransfer(ctx stdCtx.Context, t *models.RepoTransfer) *api.RepoTransfer {
	teams, _ := ToTeams(ctx, t.Teams, false)

	return &api.RepoTransfer{
		Doer:      ToUser(ctx, t.Doer, nil),
		Recipient: ToUser(ctx, t.Recipient, nil),
		Teams:     teams,
	}
}
