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

package git

import (
	"context"
	"fmt"
	"strings"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"

	"xorm.io/builder"
)

// OrgProtectedBranch represents an org-level branch protection ruleset.
// These rules cascade to all repositories within the organization.
type OrgProtectedBranch struct {
	ID                            int64              `xorm:"pk autoincr"`
	OrgID                         int64              `xorm:"UNIQUE(s) index"`
	RuleName                      string             `xorm:"UNIQUE(s)"` // branch glob pattern
	Priority                      int64              `xorm:"NOT NULL DEFAULT 0"`
	globRule                      glob.Glob          `xorm:"-"`
	isPlainName                   bool               `xorm:"-"`
	CanPush                       bool               `xorm:"NOT NULL DEFAULT false"`
	EnableWhitelist               bool               `xorm:"NOT NULL DEFAULT false"`
	WhitelistTeamIDs              []int64            `xorm:"JSON TEXT"`
	EnableMergeWhitelist          bool               `xorm:"NOT NULL DEFAULT false"`
	MergeWhitelistTeamIDs         []int64            `xorm:"JSON TEXT"`
	CanForcePush                  bool               `xorm:"NOT NULL DEFAULT false"`
	EnableForcePushAllowlist      bool               `xorm:"NOT NULL DEFAULT false"`
	ForcePushAllowlistTeamIDs     []int64            `xorm:"JSON TEXT"`
	EnableStatusCheck             bool               `xorm:"NOT NULL DEFAULT false"`
	StatusCheckContexts           []string           `xorm:"JSON TEXT"`
	RequiredApprovals             int64              `xorm:"NOT NULL DEFAULT 0"`
	EnableApprovalsWhitelist      bool               `xorm:"NOT NULL DEFAULT false"`
	ApprovalsWhitelistTeamIDs     []int64            `xorm:"JSON TEXT"`
	BlockOnRejectedReviews        bool               `xorm:"NOT NULL DEFAULT false"`
	BlockOnOfficialReviewRequests bool               `xorm:"NOT NULL DEFAULT false"`
	BlockOnOutdatedBranch         bool               `xorm:"NOT NULL DEFAULT false"`
	DismissStaleApprovals         bool               `xorm:"NOT NULL DEFAULT false"`
	IgnoreStaleApprovals          bool               `xorm:"NOT NULL DEFAULT false"`
	RequireSignedCommits          bool               `xorm:"NOT NULL DEFAULT false"`
	ProtectedFilePatterns         string             `xorm:"TEXT"`
	UnprotectedFilePatterns       string             `xorm:"TEXT"`
	BlockAdminMergeOverride       bool               `xorm:"NOT NULL DEFAULT false"`
	CreatedUnix                   timeutil.TimeStamp `xorm:"created"`
	UpdatedUnix                   timeutil.TimeStamp `xorm:"updated"`
}

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

func (o *OrgProtectedBranch) loadGlob() {
	if o.isPlainName || o.globRule != nil {
		return
	}
	if !IsRuleNameSpecial(o.RuleName) {
		o.isPlainName = true
		return
	}
	var err error
	o.globRule, err = glob.Compile(o.RuleName, '/')
	if err != nil {
		log.Warn("Invalid glob rule for OrgProtectedBranch[%d]: %s %v", o.ID, o.RuleName, err)
		o.globRule = glob.MustCompile(glob.QuoteMeta(o.RuleName), '/')
	}
}

// Match tests if branchName matches this org rule
func (o *OrgProtectedBranch) Match(branchName string) bool {
	o.loadGlob()
	if o.isPlainName {
		return strings.EqualFold(o.RuleName, branchName)
	}
	return o.globRule.Match(branchName)
}

// ToProtectedBranch converts an org-level rule to a ProtectedBranch for use
// in the standard enforcement path. Fields that are user-scoped (WhitelistUserIDs etc.)
// are left empty because org rules only reference teams.
func (o *OrgProtectedBranch) ToProtectedBranch() *ProtectedBranch {
	return &ProtectedBranch{
		ID:                            o.ID,
		RuleName:                      o.RuleName,
		Priority:                      o.Priority,
		CanPush:                       o.CanPush,
		EnableWhitelist:               o.EnableWhitelist,
		WhitelistTeamIDs:              o.WhitelistTeamIDs,
		EnableMergeWhitelist:          o.EnableMergeWhitelist,
		MergeWhitelistTeamIDs:         o.MergeWhitelistTeamIDs,
		CanForcePush:                  o.CanForcePush,
		EnableForcePushAllowlist:      o.EnableForcePushAllowlist,
		ForcePushAllowlistTeamIDs:     o.ForcePushAllowlistTeamIDs,
		EnableStatusCheck:             o.EnableStatusCheck,
		StatusCheckContexts:           o.StatusCheckContexts,
		RequiredApprovals:             o.RequiredApprovals,
		EnableApprovalsWhitelist:      o.EnableApprovalsWhitelist,
		ApprovalsWhitelistTeamIDs:     o.ApprovalsWhitelistTeamIDs,
		BlockOnRejectedReviews:        o.BlockOnRejectedReviews,
		BlockOnOfficialReviewRequests: o.BlockOnOfficialReviewRequests,
		BlockOnOutdatedBranch:         o.BlockOnOutdatedBranch,
		DismissStaleApprovals:         o.DismissStaleApprovals,
		IgnoreStaleApprovals:          o.IgnoreStaleApprovals,
		RequireSignedCommits:          o.RequireSignedCommits,
		ProtectedFilePatterns:         o.ProtectedFilePatterns,
		UnprotectedFilePatterns:       o.UnprotectedFilePatterns,
		BlockAdminMergeOverride:       o.BlockAdminMergeOverride,
		CreatedUnix:                   o.CreatedUnix,
		UpdatedUnix:                   o.UpdatedUnix,
	}
}

// GetOrgProtectedBranchByName retrieves a single org rule by org ID and rule name
func GetOrgProtectedBranchByName(ctx context.Context, orgID int64, ruleName string) (*OrgProtectedBranch, error) {
	rule, exist, err := db.Get[OrgProtectedBranch](ctx, builder.Eq{"org_id": orgID, "rule_name": ruleName})
	if err != nil {
		return nil, err
	} else if !exist {
		return nil, nil //nolint:nilnil
	}
	return rule, nil
}

// GetOrgProtectedBranchByID retrieves a single org rule by ID
func GetOrgProtectedBranchByID(ctx context.Context, orgID, id int64) (*OrgProtectedBranch, error) {
	rule, exist, err := db.Get[OrgProtectedBranch](ctx, builder.Eq{"org_id": orgID, "id": id})
	if err != nil {
		return nil, err
	} else if !exist {
		return nil, nil //nolint:nilnil
	}
	return rule, nil
}

// FindOrgProtectedBranchRules loads all org-level branch protection rules
func FindOrgProtectedBranchRules(ctx context.Context, orgID int64) ([]*OrgProtectedBranch, error) {
	var rules []*OrgProtectedBranch
	err := db.GetEngine(ctx).Where("org_id = ?", orgID).Asc("priority", "created_unix").Find(&rules)
	return rules, err
}

// CreateOrgProtectedBranch creates a new org-level branch protection rule
func CreateOrgProtectedBranch(ctx context.Context, rule *OrgProtectedBranch) error {
	if rule.Priority == 0 {
		var maxPrio int64
		if _, err := db.GetEngine(ctx).SQL(`SELECT MAX(priority) FROM org_protected_branch WHERE org_id = ?`, rule.OrgID).
			Get(&maxPrio); err != nil {
			return err
		}
		rule.Priority = maxPrio + 1
	}
	_, err := db.GetEngine(ctx).Insert(rule)
	if err != nil {
		return fmt.Errorf("Insert OrgProtectedBranch: %v", err)
	}
	return nil
}

// UpdateOrgProtectedBranch updates an existing org-level branch protection rule
func UpdateOrgProtectedBranch(ctx context.Context, rule *OrgProtectedBranch) error {
	_, err := db.GetEngine(ctx).ID(rule.ID).AllCols().Update(rule)
	if err != nil {
		return fmt.Errorf("Update OrgProtectedBranch: %v", err)
	}
	return nil
}

// DeleteOrgProtectedBranch deletes an org-level branch protection rule
func DeleteOrgProtectedBranch(ctx context.Context, orgID, id int64) error {
	affected, err := db.GetEngine(ctx).Where("org_id = ? AND id = ?", orgID, id).Delete(new(OrgProtectedBranch))
	if err != nil {
		return err
	}
	if affected == 0 {
		return fmt.Errorf("org branch protection rule ID(%d) not found", id)
	}
	return nil
}

// FindOrgBranchRuleForBranch finds the first matching org rule for a given branch name
func FindOrgBranchRuleForBranch(ctx context.Context, orgID int64, branchName string) (*OrgProtectedBranch, error) {
	rules, err := FindOrgProtectedBranchRules(ctx, orgID)
	if err != nil {
		return nil, err
	}
	for _, rule := range rules {
		if rule.Match(branchName) {
			return rule, nil
		}
	}
	return nil, nil //nolint:nilnil
}
