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

package organization

import (
	"context"

	"code.gitea.io/gitea/models/db"
	"code.gitea.io/gitea/modules/timeutil"
)

// OrgPinnedGroup represents a named group of pinned repositories for an organization
type OrgPinnedGroup struct {
	ID           int64              `xorm:"pk autoincr"`
	OrgID        int64              `xorm:"INDEX NOT NULL"`
	Name         string             `xorm:"NOT NULL"`
	DisplayOrder int                `xorm:"DEFAULT 0"`
	Collapsed    bool               `xorm:"DEFAULT false"`
	CreatedUnix  timeutil.TimeStamp `xorm:"created"`
	UpdatedUnix  timeutil.TimeStamp `xorm:"updated"`
}

// TableName returns the table name for OrgPinnedGroup
func (g *OrgPinnedGroup) TableName() string {
	return "org_pinned_group"
}

func init() {
	db.RegisterModel(new(OrgPinnedGroup))
	db.RegisterModel(new(OrgPinnedRepo))
}

// OrgPinnedRepo represents a pinned repository for an organization
type OrgPinnedRepo struct {
	ID           int64              `xorm:"pk autoincr"`
	OrgID        int64              `xorm:"INDEX NOT NULL"`
	RepoID       int64              `xorm:"INDEX NOT NULL"`
	GroupID      int64              `xorm:"INDEX"` // 0 = ungrouped
	DisplayOrder int                `xorm:"DEFAULT 0"`
	CreatedUnix  timeutil.TimeStamp `xorm:"created"`

	Repo  any             `xorm:"-"` // Will be set by caller (repo_model.Repository)
	Group *OrgPinnedGroup `xorm:"-"`
}

// TableName returns the table name for OrgPinnedRepo
func (p *OrgPinnedRepo) TableName() string {
	return "org_pinned_repo"
}

// GetOrgPinnedGroups returns all pinned groups for an organization
func GetOrgPinnedGroups(ctx context.Context, orgID int64) ([]*OrgPinnedGroup, error) {
	groups := make([]*OrgPinnedGroup, 0, 10)
	return groups, db.GetEngine(ctx).
		Where("org_id = ?", orgID).
		OrderBy("display_order ASC, id ASC").
		Find(&groups)
}

// GetOrgPinnedGroup returns a pinned group by ID
func GetOrgPinnedGroup(ctx context.Context, id int64) (*OrgPinnedGroup, error) {
	group := new(OrgPinnedGroup)
	has, err := db.GetEngine(ctx).ID(id).Get(group)
	if err != nil {
		return nil, err
	}
	if !has {
		return nil, ErrOrgPinnedGroupNotExist{ID: id}
	}
	return group, nil
}

// CreateOrgPinnedGroup creates a new pinned group for an organization
func CreateOrgPinnedGroup(ctx context.Context, group *OrgPinnedGroup) error {
	_, err := db.GetEngine(ctx).Insert(group)
	return err
}

// UpdateOrgPinnedGroup updates a pinned group
func UpdateOrgPinnedGroup(ctx context.Context, group *OrgPinnedGroup) error {
	_, err := db.GetEngine(ctx).ID(group.ID).Cols("name", "display_order", "collapsed").Update(group)
	return err
}

// DeleteOrgPinnedGroup deletes a pinned group and moves its repos to ungrouped
func DeleteOrgPinnedGroup(ctx context.Context, groupID int64) error {
	ctx, committer, err := db.TxContext(ctx)
	if err != nil {
		return err
	}
	defer committer.Close()

	// Move all repos in this group to ungrouped (group_id = 0)
	if _, err := db.GetEngine(ctx).
		Where("group_id = ?", groupID).
		Cols("group_id").
		Update(&OrgPinnedRepo{GroupID: 0}); err != nil {
		return err
	}

	// Delete the group
	if _, err := db.GetEngine(ctx).ID(groupID).Delete(new(OrgPinnedGroup)); err != nil {
		return err
	}

	return committer.Commit()
}

// GetOrgPinnedRepos returns all pinned repos for an organization
func GetOrgPinnedRepos(ctx context.Context, orgID int64) ([]*OrgPinnedRepo, error) {
	pinnedRepos := make([]*OrgPinnedRepo, 0, 20)
	return pinnedRepos, db.GetEngine(ctx).
		Where("org_id = ?", orgID).
		OrderBy("group_id ASC, display_order ASC, id ASC").
		Find(&pinnedRepos)
}

// GetOrgPinnedRepoIDs returns the repo IDs of all pinned repos for an organization
func GetOrgPinnedRepoIDs(ctx context.Context, orgID int64) ([]int64, error) {
	pinnedRepos, err := GetOrgPinnedRepos(ctx, orgID)
	if err != nil {
		return nil, err
	}

	repoIDs := make([]int64, len(pinnedRepos))
	for i, p := range pinnedRepos {
		repoIDs[i] = p.RepoID
	}
	return repoIDs, nil
}

// LoadPinnedRepoGroups loads the groups for pinned repos
func LoadPinnedRepoGroups(ctx context.Context, pinnedRepos []*OrgPinnedRepo, orgID int64) error {
	if len(pinnedRepos) == 0 {
		return nil
	}

	groups, err := GetOrgPinnedGroups(ctx, orgID)
	if err != nil {
		return err
	}

	groupMap := make(map[int64]*OrgPinnedGroup)
	for _, g := range groups {
		groupMap[g.ID] = g
	}

	for _, p := range pinnedRepos {
		if p.GroupID > 0 {
			p.Group = groupMap[p.GroupID]
		}
	}

	return nil
}

// IsRepoPinned checks if a repo is already pinned for an organization
func IsRepoPinned(ctx context.Context, orgID, repoID int64) (bool, error) {
	return db.GetEngine(ctx).
		Where("org_id = ? AND repo_id = ?", orgID, repoID).
		Exist(new(OrgPinnedRepo))
}

// CreateOrgPinnedRepo pins a repository to an organization
func CreateOrgPinnedRepo(ctx context.Context, pinned *OrgPinnedRepo) error {
	// Check if already pinned
	exists, err := IsRepoPinned(ctx, pinned.OrgID, pinned.RepoID)
	if err != nil {
		return err
	}
	if exists {
		return ErrOrgPinnedRepoAlreadyExist{OrgID: pinned.OrgID, RepoID: pinned.RepoID}
	}

	_, err = db.GetEngine(ctx).Insert(pinned)
	return err
}

// UpdateOrgPinnedRepo updates a pinned repo's group or order
func UpdateOrgPinnedRepo(ctx context.Context, pinned *OrgPinnedRepo) error {
	_, err := db.GetEngine(ctx).ID(pinned.ID).Cols("group_id", "display_order").Update(pinned)
	return err
}

// DeleteOrgPinnedRepo unpins a repository from an organization
func DeleteOrgPinnedRepo(ctx context.Context, orgID, repoID int64) error {
	_, err := db.GetEngine(ctx).
		Where("org_id = ? AND repo_id = ?", orgID, repoID).
		Delete(new(OrgPinnedRepo))
	return err
}

// DeleteOrgPinnedRepoByID unpins a repository by pinned ID
func DeleteOrgPinnedRepoByID(ctx context.Context, id int64) error {
	_, err := db.GetEngine(ctx).ID(id).Delete(new(OrgPinnedRepo))
	return err
}

// ReorderOrgPinnedRepos updates the display order of pinned repos
func ReorderOrgPinnedRepos(ctx context.Context, orgID int64, repoOrders []PinnedRepoOrder) error {
	ctx, committer, err := db.TxContext(ctx)
	if err != nil {
		return err
	}
	defer committer.Close()

	for _, order := range repoOrders {
		if _, err := db.GetEngine(ctx).
			Where("org_id = ? AND repo_id = ?", orgID, order.RepoID).
			Cols("group_id", "display_order").
			Update(&OrgPinnedRepo{
				GroupID:      order.GroupID,
				DisplayOrder: order.DisplayOrder,
			}); err != nil {
			return err
		}
	}

	return committer.Commit()
}

// ReorderOrgPinnedGroups updates the display order of pinned groups
func ReorderOrgPinnedGroups(ctx context.Context, orgID int64, groupOrders []PinnedGroupOrder) error {
	ctx, committer, err := db.TxContext(ctx)
	if err != nil {
		return err
	}
	defer committer.Close()

	for _, order := range groupOrders {
		if _, err := db.GetEngine(ctx).
			Where("org_id = ? AND id = ?", orgID, order.GroupID).
			Cols("display_order").
			Update(&OrgPinnedGroup{DisplayOrder: order.DisplayOrder}); err != nil {
			return err
		}
	}

	return committer.Commit()
}

// PinnedRepoOrder represents the order for a pinned repo
type PinnedRepoOrder struct {
	RepoID       int64 `json:"repo_id"`
	GroupID      int64 `json:"group_id"`
	DisplayOrder int   `json:"display_order"`
}

// PinnedGroupOrder represents the order for a pinned group
type PinnedGroupOrder struct {
	GroupID      int64 `json:"group_id"`
	DisplayOrder int   `json:"display_order"`
}

// ErrOrgPinnedGroupNotExist represents a "pinned group not exist" error
type ErrOrgPinnedGroupNotExist struct {
	ID int64
}

func (err ErrOrgPinnedGroupNotExist) Error() string {
	return "pinned group does not exist"
}

// ErrOrgPinnedRepoAlreadyExist represents a "repo already pinned" error
type ErrOrgPinnedRepoAlreadyExist struct {
	OrgID  int64
	RepoID int64
}

func (err ErrOrgPinnedRepoAlreadyExist) Error() string {
	return "repository is already pinned"
}
