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

package setting

import (
	"os/exec"
	"path/filepath"
	"strings"

	"code.gitea.io/gitea/modules/log"
)

// enumerates all the policy repository creating
const (
	RepoCreatingLastUserVisibility = "last"
	RepoCreatingPrivate            = "private"
	RepoCreatingPublic             = "public"
)

// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
const ItemsPerPage = 40

// Repository settings
var (
	Repository = struct {
		DetectedCharsetsOrder                   []string
		DetectedCharsetScore                    map[string]int `ini:"-"`
		AnsiCharset                             string
		ForcePrivate                            bool
		DefaultPrivate                          string
		DefaultPushCreatePrivate                bool
		MaxCreationLimit                        int
		PreferredLicenses                       []string
		DisableHTTPGit                          bool
		AccessControlAllowOrigin                string
		UseCompatSSHURI                         bool
		GoGetCloneURLProtocol                   string
		DefaultCloseIssuesViaCommitsInAnyBranch bool
		EnablePushCreateUser                    bool
		EnablePushCreateOrg                     bool
		DisabledRepoUnits                       []string
		DefaultRepoUnits                        []string
		DefaultForkRepoUnits                    []string
		DefaultMirrorRepoUnits                  []string
		DefaultTemplateRepoUnits                []string
		PrefixArchiveFiles                      bool
		DisableMigrations                       bool
		DisableStars                            bool `ini:"DISABLE_STARS"`
		DefaultBranch                           string
		AllowAdoptionOfUnadoptedRepositories    bool
		AllowDeleteOfUnadoptedRepositories      bool
		DisableDownloadSourceArchives           bool
		AllowForkWithoutMaximumLimit            bool
		AllowForkIntoSameOwner                  bool

		// StreamArchives makes Gitea stream git archive files to the client directly instead of creating an archive first.
		// Ideally all users should use this streaming method. However, at the moment we don't know whether there are
		// any users who still need the old behavior, so we introduce this option, intentionally not documenting it.
		// After one or two releases, if no one complains, we will remove this option and always use streaming.
		StreamArchives bool

		// Repository editor settings
		Editor struct {
			LineWrapExtensions []string
		} `ini:"-"`

		// Repository upload settings
		Upload struct {
			Enabled      bool
			AllowedTypes string
			FileMaxSize  int64
			MaxFiles     int
		} `ini:"-"`

		// Pull request settings
		PullRequest struct {
			WorkInProgressPrefixes                   []string
			CloseKeywords                            []string
			ReopenKeywords                           []string
			DefaultMergeStyle                        string
			DefaultMergeMessageCommitsLimit          int
			DefaultMergeMessageSize                  int
			DefaultMergeMessageAllAuthors            bool
			DefaultMergeMessageMaxApprovers          int
			DefaultMergeMessageOfficialApproversOnly bool
			PopulateSquashCommentWithCommitMessages  bool
			AddCoCommitterTrailers                   bool
			TestConflictingPatchesWithGitApply       bool
			RetargetChildrenOnMerge                  bool
			DelayCheckForInactiveDays                int
		} `ini:"repository.pull-request"`

		// Issue Setting
		Issue struct {
			LockReasons []string
			MaxPinned   int
		} `ini:"repository.issue"`

		Release struct {
			AllowedTypes     string
			DefaultPagingNum int
		} `ini:"repository.release"`

		Signing struct {
			SigningKey        string
			SigningName       string
			SigningEmail      string
			SigningFormat     string
			InitialCommit     []string
			CRUDActions       []string `ini:"CRUD_ACTIONS"`
			Merges            []string
			Wiki              []string
			DefaultTrustModel string
			TrustedSSHKeys    []string `ini:"TRUSTED_SSH_KEYS"`
		} `ini:"repository.signing"`
	}{
		DetectedCharsetsOrder: []string{
			"UTF-8",
			"UTF-16BE",
			"UTF-16LE",
			"UTF-32BE",
			"UTF-32LE",
			"ISO-8859-1",
			"windows-1252",
			"ISO-8859-2",
			"windows-1250",
			"ISO-8859-5",
			"ISO-8859-6",
			"ISO-8859-7",
			"windows-1253",
			"ISO-8859-8-I",
			"windows-1255",
			"ISO-8859-8",
			"windows-1251",
			"windows-1256",
			"KOI8-R",
			"ISO-8859-9",
			"windows-1254",
			"Shift_JIS",
			"GB18030",
			"EUC-JP",
			"EUC-KR",
			"Big5",
			"ISO-2022-JP",
			"ISO-2022-KR",
			"ISO-2022-CN",
			"IBM424_rtl",
			"IBM424_ltr",
			"IBM420_rtl",
			"IBM420_ltr",
		},
		DetectedCharsetScore:                    map[string]int{},
		AnsiCharset:                             "",
		ForcePrivate:                            false,
		DefaultPrivate:                          RepoCreatingLastUserVisibility,
		DefaultPushCreatePrivate:                true,
		MaxCreationLimit:                        -1,
		PreferredLicenses:                       []string{"Apache License 2.0", "MIT License"},
		DisableHTTPGit:                          false,
		AccessControlAllowOrigin:                "",
		UseCompatSSHURI:                         false,
		DefaultCloseIssuesViaCommitsInAnyBranch: false,
		EnablePushCreateUser:                    false,
		EnablePushCreateOrg:                     false,
		DisabledRepoUnits:                       []string{},
		DefaultRepoUnits:                        []string{},
		DefaultForkRepoUnits:                    []string{},
		DefaultMirrorRepoUnits:                  []string{},
		DefaultTemplateRepoUnits:                []string{},
		PrefixArchiveFiles:                      true,
		DisableMigrations:                       false,
		DisableStars:                            false,
		DefaultBranch:                           "main",
		AllowForkWithoutMaximumLimit:            true,
		StreamArchives:                          true,

		// Repository editor settings
		Editor: struct {
			LineWrapExtensions []string
		}{
			LineWrapExtensions: strings.Split(".txt,.md,.markdown,.mdown,.mkd,.livemd,", ","),
		},

		// Repository upload settings
		Upload: struct {
			Enabled      bool
			AllowedTypes string
			FileMaxSize  int64
			MaxFiles     int
		}{
			Enabled:      true,
			AllowedTypes: "",
			FileMaxSize:  50,
			MaxFiles:     5,
		},

		// Pull request settings
		PullRequest: struct {
			WorkInProgressPrefixes                   []string
			CloseKeywords                            []string
			ReopenKeywords                           []string
			DefaultMergeStyle                        string
			DefaultMergeMessageCommitsLimit          int
			DefaultMergeMessageSize                  int
			DefaultMergeMessageAllAuthors            bool
			DefaultMergeMessageMaxApprovers          int
			DefaultMergeMessageOfficialApproversOnly bool
			PopulateSquashCommentWithCommitMessages  bool
			AddCoCommitterTrailers                   bool
			TestConflictingPatchesWithGitApply       bool
			RetargetChildrenOnMerge                  bool
			DelayCheckForInactiveDays                int
		}{
			WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
			// Same as GitHub. See
			// https://help.github.com/articles/closing-issues-via-commit-messages
			CloseKeywords:                            strings.Split("close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved", ","),
			ReopenKeywords:                           strings.Split("reopen,reopens,reopened", ","),
			DefaultMergeStyle:                        "merge",
			DefaultMergeMessageCommitsLimit:          50,
			DefaultMergeMessageSize:                  5 * 1024,
			DefaultMergeMessageAllAuthors:            false,
			DefaultMergeMessageMaxApprovers:          10,
			DefaultMergeMessageOfficialApproversOnly: true,
			PopulateSquashCommentWithCommitMessages:  false,
			AddCoCommitterTrailers:                   true,
			RetargetChildrenOnMerge:                  true,
			DelayCheckForInactiveDays:                7,
		},

		// Issue settings
		Issue: struct {
			LockReasons []string
			MaxPinned   int
		}{
			LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
			MaxPinned:   3,
		},

		Release: struct {
			AllowedTypes     string
			DefaultPagingNum int
		}{
			AllowedTypes:     "",
			DefaultPagingNum: 10,
		},

		// Signing settings
		Signing: struct {
			SigningKey        string
			SigningName       string
			SigningEmail      string
			SigningFormat     string
			InitialCommit     []string
			CRUDActions       []string `ini:"CRUD_ACTIONS"`
			Merges            []string
			Wiki              []string
			DefaultTrustModel string
			TrustedSSHKeys    []string `ini:"TRUSTED_SSH_KEYS"`
		}{
			SigningKey:        "default",
			SigningName:       "",
			SigningEmail:      "",
			SigningFormat:     "openpgp", // git.SigningKeyFormatOpenPGP
			InitialCommit:     []string{"always"},
			CRUDActions:       []string{"pubkey", "twofa", "parentsigned"},
			Merges:            []string{"pubkey", "twofa", "basesigned", "commitssigned"},
			Wiki:              []string{"never"},
			DefaultTrustModel: "collaborator",
			TrustedSSHKeys:    []string{},
		},
	}
	RepoRootPath string
	ScriptType   = "bash"
)

func loadRepositoryFrom(rootCfg ConfigProvider) {
	var err error
	// Determine and create root git repository path.
	sec := rootCfg.Section("repository")
	Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool()
	Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool()
	Repository.GoGetCloneURLProtocol = sec.Key("GO_GET_CLONE_URL_PROTOCOL").MustString("https")
	Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1)
	Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString(Repository.DefaultBranch)
	RepoRootPath = sec.Key("ROOT").MustString(filepath.Join(AppDataPath, "gitea-repositories"))
	if !filepath.IsAbs(RepoRootPath) {
		RepoRootPath = filepath.Join(AppWorkPath, RepoRootPath)
	} else {
		RepoRootPath = filepath.Clean(RepoRootPath)
	}

	checkOverlappedPath("[repository].ROOT", RepoRootPath)

	defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
	for _, charset := range Repository.DetectedCharsetsOrder {
		defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder, strings.ToLower(strings.TrimSpace(charset)))
	}
	ScriptType = sec.Key("SCRIPT_TYPE").MustString("bash")

	if _, err := exec.LookPath(ScriptType); err != nil {
		log.Warn("SCRIPT_TYPE %q is not on the current PATH. Are you sure that this is the correct SCRIPT_TYPE?", ScriptType)
	}

	if err = sec.MapTo(&Repository); err != nil {
		log.Fatal("Failed to map Repository settings: %v", err)
	} else if err = rootCfg.Section("repository.editor").MapTo(&Repository.Editor); err != nil {
		log.Fatal("Failed to map Repository.Editor settings: %v", err)
	} else if err = rootCfg.Section("repository.upload").MapTo(&Repository.Upload); err != nil {
		log.Fatal("Failed to map Repository.Upload settings: %v", err)
	} else if err = rootCfg.Section("repository.pull-request").MapTo(&Repository.PullRequest); err != nil {
		log.Fatal("Failed to map Repository.PullRequest settings: %v", err)
	}

	if !rootCfg.Section("packages").Key("ENABLED").MustBool(Packages.Enabled) {
		Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.packages")
	}

	if !rootCfg.Section("actions").Key("ENABLED").MustBool(Actions.Enabled) {
		Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.actions")
	}

	// Handle default trustmodel settings
	Repository.Signing.DefaultTrustModel = strings.ToLower(strings.TrimSpace(Repository.Signing.DefaultTrustModel))
	if Repository.Signing.DefaultTrustModel == "default" {
		Repository.Signing.DefaultTrustModel = "collaborator"
	}

	// Handle preferred charset orders
	preferred := make([]string, 0, len(Repository.DetectedCharsetsOrder))
	for _, charset := range Repository.DetectedCharsetsOrder {
		canonicalCharset := strings.ToLower(strings.TrimSpace(charset))
		preferred = append(preferred, canonicalCharset)
		// remove it from the defaults
		for i, charset := range defaultDetectedCharsetsOrder {
			if charset == canonicalCharset {
				defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder[:i], defaultDetectedCharsetsOrder[i+1:]...)
				break
			}
		}
	}

	i := 0
	for _, charset := range preferred {
		// Add the defaults
		if charset == "defaults" {
			for _, charset := range defaultDetectedCharsetsOrder {
				canonicalCharset := strings.ToLower(strings.TrimSpace(charset))
				if _, has := Repository.DetectedCharsetScore[canonicalCharset]; !has {
					Repository.DetectedCharsetScore[canonicalCharset] = i
					i++
				}
			}
			continue
		}
		if _, has := Repository.DetectedCharsetScore[charset]; !has {
			Repository.DetectedCharsetScore[charset] = i
			i++
		}
	}

	if err := loadRepoArchiveFrom(rootCfg); err != nil {
		log.Fatal("loadRepoArchiveFrom: %v", err)
	}
}
