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

package repo

import (
	"context"

	"gitea.dev/models/db"
	"gitea.dev/models/perm"
	"gitea.dev/models/unit"
	"gitea.dev/modules/json"
	"gitea.dev/modules/setting"
	"gitea.dev/modules/timeutil"
	"gitea.dev/modules/util"

	"xorm.io/xorm"
	"xorm.io/xorm/convert"
)

// ErrUnitTypeNotExist represents a "UnitTypeNotExist" kind of error.
type ErrUnitTypeNotExist struct {
	UT unit.Type
}

// IsErrUnitTypeNotExist checks if an error is a ErrUnitNotExist.
func IsErrUnitTypeNotExist(err error) bool {
	_, ok := err.(ErrUnitTypeNotExist)
	return ok
}

func (err ErrUnitTypeNotExist) Error() string {
	return "Unit type does not exist: " + err.UT.LogString()
}

func (err ErrUnitTypeNotExist) Unwrap() error {
	return util.ErrNotExist
}

// RepoUnit describes all units of a repository
type RepoUnit struct { //revive:disable-line:exported
	ID                  int64
	RepoID              int64              `xorm:"INDEX(s)"`
	Type                unit.Type          `xorm:"INDEX(s)"`
	Config              convert.Conversion `xorm:"TEXT"`
	CreatedUnix         timeutil.TimeStamp `xorm:"INDEX CREATED"`
	AnonymousAccessMode perm.AccessMode    `xorm:"NOT NULL DEFAULT 0"`
	EveryoneAccessMode  perm.AccessMode    `xorm:"NOT NULL DEFAULT 0"`
}

func init() {
	db.RegisterModel(new(RepoUnit))
}

// UnitConfig describes common unit config
type UnitConfig struct{}

// FromDB fills up a UnitConfig from serialized format.
func (cfg *UnitConfig) FromDB(bs []byte) error {
	return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}

// ToDB exports a UnitConfig to a serialized format.
func (cfg *UnitConfig) ToDB() ([]byte, error) {
	return json.Marshal(cfg)
}

// ExternalWikiConfig describes external wiki config
type ExternalWikiConfig struct {
	ExternalWikiURL string
}

// FromDB fills up a ExternalWikiConfig from serialized format.
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
	return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}

// ToDB exports a ExternalWikiConfig to a serialized format.
func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
	return json.Marshal(cfg)
}

// ExternalTrackerConfig describes external tracker config
type ExternalTrackerConfig struct {
	ExternalTrackerURL           string
	ExternalTrackerFormat        string
	ExternalTrackerStyle         string
	ExternalTrackerRegexpPattern string
}

// FromDB fills up a ExternalTrackerConfig from serialized format.
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
	return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}

// ToDB exports a ExternalTrackerConfig to a serialized format.
func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
	return json.Marshal(cfg)
}

// IssuesConfig describes issues config
type IssuesConfig struct {
	EnableTimetracker                bool
	AllowOnlyContributorsToTrackTime bool
	EnableDependencies               bool
}

// FromDB fills up a IssuesConfig from serialized format.
func (cfg *IssuesConfig) FromDB(bs []byte) error {
	return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}

// ToDB exports a IssuesConfig to a serialized format.
func (cfg *IssuesConfig) ToDB() ([]byte, error) {
	return json.Marshal(cfg)
}

// PullRequestsConfig describes pull requests config
type PullRequestsConfig struct {
	IgnoreWhitespaceConflicts     bool
	AllowMerge                    bool
	AllowRebase                   bool
	AllowRebaseMerge              bool
	AllowSquash                   bool
	AllowFastForwardOnly          bool
	AllowManualMerge              bool
	AutodetectManualMerge         bool
	AllowMergeUpdate              bool
	AllowRebaseUpdate             bool
	DefaultUpdateStyle            UpdateStyle
	DefaultDeleteBranchAfterMerge bool
	DefaultMergeStyle             MergeStyle
	DefaultAllowMaintainerEdit    bool
	DefaultTargetBranch           string
}

func DefaultPullRequestsConfig() *PullRequestsConfig {
	cfg := &PullRequestsConfig{
		AllowMerge:                 true,
		AllowRebase:                true,
		AllowRebaseMerge:           true,
		AllowSquash:                true,
		AllowFastForwardOnly:       true,
		AllowMergeUpdate:           true,
		AllowRebaseUpdate:          true,
		DefaultUpdateStyle:         UpdateStyleMerge,
		DefaultAllowMaintainerEdit: true,
	}
	cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
	cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
	cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge)
	return cfg
}

// FromDB fills up a PullRequestsConfig from serialized format.
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
	// set default values for existing PullRequestConfig in DB
	*cfg = *DefaultPullRequestsConfig()
	_ = json.UnmarshalHandleDoubleEncode(bs, &cfg) // don't let corrupted database value cause unnecessary 500 error
	cfg.DefaultUpdateStyle = util.IfZero(cfg.DefaultUpdateStyle, UpdateStyleMerge)
	return nil
}

// ToDB exports a PullRequestsConfig to a serialized format.
func (cfg *PullRequestsConfig) ToDB() ([]byte, error) {
	return json.Marshal(cfg)
}

// IsMergeStyleAllowed returns if merge style is allowed
func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool {
	return mergeStyle == MergeStyleMerge && cfg.AllowMerge ||
		mergeStyle == MergeStyleRebase && cfg.AllowRebase ||
		mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge ||
		mergeStyle == MergeStyleSquash && cfg.AllowSquash ||
		mergeStyle == MergeStyleFastForwardOnly && cfg.AllowFastForwardOnly ||
		mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge
}

// IsUpdateStyleAllowed returns if a pull request branch update style is allowed
func (cfg *PullRequestsConfig) IsUpdateStyleAllowed(updateStyle UpdateStyle) bool {
	switch updateStyle {
	case UpdateStyleMerge:
		return cfg.AllowMergeUpdate
	case UpdateStyleRebase:
		return cfg.AllowRebaseUpdate
	default:
		return false
	}
}

// ValidateUpdateSettings checks that the AllowMerge/RebaseUpdate flags and DefaultUpdateStyle are mutually consistent.
func (cfg *PullRequestsConfig) ValidateUpdateSettings() error {
	if cfg.DefaultUpdateStyle != UpdateStyleMerge && cfg.DefaultUpdateStyle != UpdateStyleRebase {
		return util.NewInvalidArgumentErrorf("default update style must be merge or rebase")
	}
	if !cfg.AllowMergeUpdate && !cfg.AllowRebaseUpdate {
		return util.NewInvalidArgumentErrorf("at least one pull request branch update style must be enabled")
	}
	if !cfg.IsUpdateStyleAllowed(cfg.DefaultUpdateStyle) {
		return util.NewInvalidArgumentErrorf("default update style must be enabled")
	}
	return nil
}

func DefaultPullRequestsUnit(repoID int64) RepoUnit {
	return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()}
}

// ProjectsMode represents the projects enabled for a repository
type ProjectsMode string

const (
	// ProjectsModeRepo allows only repo-level projects
	ProjectsModeRepo ProjectsMode = "repo"
	// ProjectsModeOwner allows only owner-level projects
	ProjectsModeOwner ProjectsMode = "owner"
	// ProjectsModeAll allows both kinds of projects
	ProjectsModeAll ProjectsMode = "all"
	// ProjectsModeNone doesn't allow projects
	ProjectsModeNone ProjectsMode = "none"
)

// ProjectsConfig describes projects config
type ProjectsConfig struct {
	ProjectsMode ProjectsMode
}

// FromDB fills up a ProjectsConfig from serialized format.
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
	// TODO: remove GetProjectsMode, only use ProjectsMode
	cfg.ProjectsMode = ProjectsModeAll
	return json.UnmarshalHandleDoubleEncode(bs, &cfg)
}

// ToDB exports a ProjectsConfig to a serialized format.
func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
	return json.Marshal(cfg)
}

func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
	if cfg.ProjectsMode != "" {
		return cfg.ProjectsMode
	}

	return ProjectsModeAll
}

func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
	projectsMode := cfg.GetProjectsMode()

	if m == ProjectsModeNone {
		return true
	}

	return projectsMode == m || projectsMode == ProjectsModeAll
}

// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
	switch colName {
	case "type":
		var err error
		r.Type, _, err = db.CellToInt(val, unit.TypeInvalid)
		if err != nil {
			setting.PanicInDevOrTesting("Unable to convert repo unit (id=%d) type: %v", r.ID, err)
		}
		switch r.Type {
		case unit.TypeExternalWiki:
			r.Config = new(ExternalWikiConfig)
		case unit.TypeExternalTracker:
			r.Config = new(ExternalTrackerConfig)
		case unit.TypePullRequests:
			r.Config = new(PullRequestsConfig)
		case unit.TypeIssues:
			r.Config = new(IssuesConfig)
		case unit.TypeActions:
			r.Config = new(ActionsConfig)
		case unit.TypeProjects:
			r.Config = new(ProjectsConfig)
		case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
			fallthrough
		default:
			r.Config = new(UnitConfig)
		}
	case "config":
		if *val == nil {
			// XROM doesn't call FromDB if the value is nil, but we need to set default values for the config fields
			_ = r.Config.FromDB(nil)
		}
	}
}

// Unit returns Unit
func (r *RepoUnit) Unit() unit.Unit {
	return unit.Units[r.Type]
}

// CodeConfig returns config for unit.TypeCode
func (r *RepoUnit) CodeConfig() *UnitConfig {
	return r.Config.(*UnitConfig)
}

// PullRequestsConfig returns config for unit.TypePullRequests
func (r *RepoUnit) PullRequestsConfig() *PullRequestsConfig {
	return r.Config.(*PullRequestsConfig)
}

// ReleasesConfig returns config for unit.TypeReleases
func (r *RepoUnit) ReleasesConfig() *UnitConfig {
	return r.Config.(*UnitConfig)
}

// ExternalWikiConfig returns config for unit.TypeExternalWiki
func (r *RepoUnit) ExternalWikiConfig() *ExternalWikiConfig {
	return r.Config.(*ExternalWikiConfig)
}

// IssuesConfig returns config for unit.TypeIssues
func (r *RepoUnit) IssuesConfig() *IssuesConfig {
	return r.Config.(*IssuesConfig)
}

// ExternalTrackerConfig returns config for unit.TypeExternalTracker
func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
	return r.Config.(*ExternalTrackerConfig)
}

// ActionsConfig returns config for unit.ActionsConfig
func (r *RepoUnit) ActionsConfig() *ActionsConfig {
	return r.Config.(*ActionsConfig)
}

// ProjectsConfig returns config for unit.ProjectsConfig
func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
	return r.Config.(*ProjectsConfig)
}

func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
	var tmpUnits []*RepoUnit
	if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
		return nil, err
	}

	for _, u := range tmpUnits {
		if !u.Type.UnitGlobalDisabled() {
			units = append(units, u)
		}
	}

	return units, nil
}

// UpdateRepoUnitConfig updates the config of the provided repo unit
func UpdateRepoUnitConfig(ctx context.Context, unit *RepoUnit) error {
	_, err := db.GetEngine(ctx).ID(unit.ID).Cols("config").Update(unit)
	return err
}

func UpdateRepoUnitPublicAccess(ctx context.Context, unit *RepoUnit) error {
	_, err := db.GetEngine(ctx).Where("repo_id=? AND `type`=?", unit.RepoID, unit.Type).
		Cols("anonymous_access_mode", "everyone_access_mode").Update(unit)
	return err
}
