package externaladmin

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"time"

	"hyapp-admin-server/internal/model"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

// PermissionCatalogItem is the only assignable permission source for external accounts.
// Keeping route permissions, capability aliases and form metadata in one catalog prevents
// a client from persisting arbitrary main-admin permissions into an external session.
type PermissionCatalogItem struct {
	Code           string   `json:"code"`
	Label          string   `json:"label"`
	Group          string   `json:"group"`
	Dependencies   []string `json:"dependencies"`
	Capabilities   []string `json:"capabilities"`
	DefaultGranted bool     `json:"defaultGranted"`
}

type AccountPermissionView struct {
	AccountID   uint64   `json:"accountId,string"`
	Permissions []string `json:"permissions"`
	Revision    uint64   `json:"revision"`
}

type UpdatePermissionsInput struct {
	Permissions      []string
	ExpectedRevision uint64
}

type PermissionUpdateResult struct {
	Account             AccountDTO
	PreviousPermissions []string
	Changed             bool
	SessionsRevoked     bool
}

type PermissionValidationError struct {
	Permission string
	Dependency string
}

func (err *PermissionValidationError) Error() string {
	if err.Dependency != "" {
		return fmt.Sprintf("%s: %s requires %s", ErrInvalidPermissions, err.Permission, err.Dependency)
	}
	return fmt.Sprintf("%s: unsupported permission %s", ErrInvalidPermissions, err.Permission)
}

func (err *PermissionValidationError) Unwrap() error { return ErrInvalidPermissions }

var permissionCatalog = []PermissionCatalogItem{
	{Code: "user:list", Label: "用户列表", Group: "用户管理", Capabilities: []string{"user:list"}, DefaultGranted: true},
	{Code: "user:update", Label: "用户信息编辑", Group: "用户管理", Capabilities: []string{"user:update"}, DefaultGranted: true},
	{Code: "user-ban:list", Label: "账号封禁列表", Group: "用户管理", Capabilities: []string{"user-ban:list"}, DefaultGranted: true},
	{Code: "user:ban", Label: "执行封禁", Group: "用户管理", Capabilities: []string{"user:ban"}, DefaultGranted: true},
	{Code: "user:unban", Label: "解除封禁", Group: "用户管理", Capabilities: []string{"user:unban"}, DefaultGranted: true},
	{Code: "host:list", Label: "主播列表", Group: "组织管理", Capabilities: []string{"host:list"}, DefaultGranted: true},
	{Code: "host:invite", Label: "邀请主播", Group: "组织管理", Capabilities: []string{"host:invite"}, DefaultGranted: true},
	{Code: "agency:list", Label: "公会列表", Group: "组织管理", Capabilities: []string{"agency:list"}, DefaultGranted: true},
	{Code: "agency:invite", Label: "邀请公会", Group: "组织管理", Capabilities: []string{"agency:invite"}, DefaultGranted: true},
	{Code: "bd:list", Label: "BD 列表", Group: "组织管理", Capabilities: []string{"bd:list"}, DefaultGranted: true},
	{Code: "bd:invite", Label: "邀请 BD", Group: "组织管理", Capabilities: []string{"bd:invite"}, DefaultGranted: true},
	{Code: "external-super-admin:list", Label: "外管SuperAdmin列表", Group: "组织管理", Capabilities: []string{"external-super-admin:list"}, DefaultGranted: true},
	{Code: "room:list", Label: "房间管理", Group: "房间管理", Capabilities: []string{"room:list"}, DefaultGranted: true},
	{Code: "room:update", Label: "房间编辑", Group: "房间管理", Capabilities: []string{"room:update"}, DefaultGranted: true},
	{Code: "room:ban", Label: "房间封禁与解封", Group: "房间管理", Capabilities: []string{"room:ban"}, DefaultGranted: true},
	{Code: "privilege:list", Label: "用户特权道具列表", Group: "资源与发放", Capabilities: []string{"privilege:list"}, DefaultGranted: true},
	{Code: "privilege:grant", Label: "特权道具发放", Group: "资源与发放", Dependencies: []string{"user-title:grant", "room-background:grant"}, Capabilities: []string{"privilege:grant"}, DefaultGranted: true},
	{Code: "pretty-id:grant", Label: "靓号下发", Group: "资源与发放", Capabilities: []string{"pretty-id:grant"}, DefaultGranted: true},
	// Wallet resources currently have no authoritative user-title/room-background semantic type.
	// Keep the three labels requested by the product, but make them one fail-closed assignment
	// bundle until the owner model can classify them without guessing from names or URLs.
	{Code: "user-title:grant", Label: "用户称号下发", Group: "资源与发放", Dependencies: []string{"privilege:grant", "room-background:grant"}, Capabilities: []string{"user-title:grant"}, DefaultGranted: true},
	{Code: "room-background:grant", Label: "房间背景图下发", Group: "资源与发放", Dependencies: []string{"privilege:grant", "user-title:grant"}, Capabilities: []string{"room-background:grant"}, DefaultGranted: true},
	{Code: "user-level:grant", Label: "财富/VIP等级下发", Group: "资源与发放", Capabilities: []string{"user-level:grant"}, DefaultGranted: true},
	{Code: "banner:create", Label: "Banner创建", Group: "运营管理", Capabilities: []string{"banner:create"}, DefaultGranted: true},
	{Code: "banner:update", Label: "Banner编辑与删除", Group: "运营管理", Capabilities: []string{"banner:update"}, DefaultGranted: true},
}

// effectivePermissions upgrades the original 18 route-oriented snapshot into the
// 20 independently displayed business capabilities. New snapshots already contain
// catalog codes and pass through unchanged. Conditional legacy closures avoid turning
// a manually reduced old snapshot into broader Banner, VIP or grant access.
func effectivePermissions(stored []string) []string {
	stored = normalizePermissions(stored)
	storedSet := make(map[string]struct{}, len(stored))
	for _, permission := range stored {
		storedSet[permission] = struct{}{}
	}
	assignable := make(map[string]struct{}, len(permissionCatalog))
	for _, item := range permissionCatalog {
		assignable[item.Code] = struct{}{}
	}
	effective := make([]string, 0, len(permissionCatalog))
	for _, permission := range stored {
		if _, ok := assignable[permission]; ok {
			effective = append(effective, permission)
		}
	}
	legacyAliases := map[string][]string{
		"app-user:view":         {"user:list", "user-ban:list"},
		"app-user:update":       {"user:update"},
		"app-user:status":       {"user:ban", "user:unban"},
		"host:view":             {"host:list"},
		"agency:view":           {"agency:list"},
		"bd:view":               {"bd:list"},
		"room:view":             {"room:list"},
		"resource-grant:create": {"privilege:grant", "user-title:grant", "room-background:grant"},
	}
	for legacy, aliases := range legacyAliases {
		if _, ok := storedSet[legacy]; ok {
			effective = append(effective, aliases...)
		}
	}
	if containsPermission(storedSet, "resource:view") && containsPermission(storedSet, "resource-grant:view") {
		effective = append(effective, "privilege:list")
	}
	if containsPermission(storedSet, "app-user:level") && containsPermission(storedSet, "vip-config:grant") {
		effective = append(effective, "user-level:grant")
	}
	if containsPermission(storedSet, "app-config:view") && containsPermission(storedSet, "app-config:update") && containsPermission(storedSet, "upload:create") {
		effective = append(effective, "banner:create")
	}
	effective = normalizePermissions(effective)
	// A hand-edited/corrupt new snapshot must not bypass the atomic grant bundle.
	// Unknown codes and wildcard-looking values were already discarded above.
	effectiveSet := make(map[string]struct{}, len(effective))
	for _, permission := range effective {
		effectiveSet[permission] = struct{}{}
	}
	grantBundle := []string{"privilege:grant", "user-title:grant", "room-background:grant"}
	completeGrantBundle := true
	for _, permission := range grantBundle {
		if !containsPermission(effectiveSet, permission) {
			completeGrantBundle = false
		}
	}
	if !completeGrantBundle {
		filtered := effective[:0]
		for _, permission := range effective {
			if permission != "privilege:grant" && permission != "user-title:grant" && permission != "room-background:grant" {
				filtered = append(filtered, permission)
			}
		}
		effective = filtered
	}
	return effective
}

func containsPermission(permissions map[string]struct{}, code string) bool {
	_, ok := permissions[code]
	return ok
}

func PermissionCatalog() []PermissionCatalogItem {
	items := make([]PermissionCatalogItem, 0, len(permissionCatalog))
	for _, item := range permissionCatalog {
		item.Dependencies = append([]string(nil), item.Dependencies...)
		item.Capabilities = append([]string(nil), item.Capabilities...)
		items = append(items, item)
	}
	return items
}

func permissionCapabilityAliases() map[string][]string {
	aliases := make(map[string][]string, len(permissionCatalog))
	for _, item := range permissionCatalog {
		aliases[item.Code] = item.Capabilities
	}
	return aliases
}

// validatePermissionSelection normalizes duplicates/order only after checking every
// code against the fixed catalog and every write dependency against the same snapshot.
// An explicit empty list is valid and represents an account with no business access.
func validatePermissionSelection(permissions []string) ([]string, error) {
	normalized := normalizePermissions(permissions)
	selected := make(map[string]struct{}, len(normalized))
	assignable := make(map[string]PermissionCatalogItem, len(permissionCatalog))
	for _, item := range permissionCatalog {
		assignable[item.Code] = item
	}
	for _, permission := range normalized {
		if _, ok := assignable[permission]; !ok {
			return nil, &PermissionValidationError{Permission: permission}
		}
		selected[permission] = struct{}{}
	}
	for _, permission := range normalized {
		for _, dependency := range assignable[permission].Dependencies {
			if _, ok := selected[dependency]; !ok {
				return nil, &PermissionValidationError{Permission: permission, Dependency: dependency}
			}
		}
	}
	return normalized, nil
}

// createPermissionSnapshot intentionally keeps omitted and explicit-empty requests
// distinct. Existing credential-creation clients omit the field and retain the full
// legacy snapshot; a permission-aware client may submit [] to create a locked-down
// account that can authenticate but cannot enter any business page.
func createPermissionSnapshot(input CreateInput) ([]string, error) {
	if !input.PermissionsSet {
		return DefaultPermissionSnapshot(), nil
	}
	return validatePermissionSelection(input.Permissions)
}

func (s *Service) GetAccountPermissions(ctx context.Context, appCode string, id uint64) (AccountPermissionView, error) {
	appCode = normalizeAppCode(appCode)
	if s.db == nil {
		return AccountPermissionView{}, errors.New("admin mysql is not configured")
	}
	if appCode == "" || id == 0 {
		return AccountPermissionView{}, ErrInvalidInput
	}
	var account model.ExternalAdminAccount
	err := s.db.WithContext(ctx).Select("id", "permissions_json", "permission_revision").Where("id = ? AND app_code = ?", id, appCode).Take(&account).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return AccountPermissionView{}, ErrAccountNotFound
	}
	if err != nil {
		return AccountPermissionView{}, err
	}
	permissions, err := decodePermissions(account.PermissionsJSON)
	if err != nil {
		return AccountPermissionView{}, err
	}
	return AccountPermissionView{AccountID: account.ID, Permissions: permissions, Revision: account.PermissionRevision}, nil
}

func (s *Service) UpdateAccountPermissions(ctx context.Context, appCode string, id uint64, input UpdatePermissionsInput) (PermissionUpdateResult, error) {
	appCode = normalizeAppCode(appCode)
	if s.db == nil {
		return PermissionUpdateResult{}, errors.New("admin mysql is not configured")
	}
	if appCode == "" || id == 0 || input.ExpectedRevision == 0 {
		return PermissionUpdateResult{}, ErrInvalidInput
	}
	normalized, err := validatePermissionSelection(input.Permissions)
	if err != nil {
		return PermissionUpdateResult{}, err
	}
	encoded, err := encodePermissions(normalized)
	if err != nil {
		return PermissionUpdateResult{}, err
	}

	result := PermissionUpdateResult{}
	var account model.ExternalAdminAccount
	err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// The account row lock serializes concurrent permission writers and makes the
		// old/new audit snapshot match the exact value replaced by this transaction.
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND app_code = ?", id, appCode).First(&account).Error; err != nil {
			return err
		}
		if account.PermissionRevision != input.ExpectedRevision {
			return ErrPermissionConflict
		}
		previous, err := decodePermissions(account.PermissionsJSON)
		if err != nil {
			return err
		}
		result.PreviousPermissions = previous
		if reflect.DeepEqual(previous, normalized) {
			return nil
		}
		if err := tx.Model(&account).Updates(map[string]any{
			"permissions_json":    encoded,
			"permission_revision": account.PermissionRevision + 1,
		}).Error; err != nil {
			return err
		}
		// Revocation is committed atomically with the snapshot. No browser can keep a
		// still-valid session after a permission reduction or silently retain a broader UI.
		if err := revokeAllSessions(tx, account.ID, "permissions_changed", time.Now().UTC().UnixMilli()); err != nil {
			return err
		}
		result.Changed = true
		result.SessionsRevoked = true
		return nil
	})
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return PermissionUpdateResult{}, ErrAccountNotFound
	}
	if errors.Is(err, ErrPermissionConflict) {
		return PermissionUpdateResult{}, ErrPermissionConflict
	}
	if err != nil {
		return PermissionUpdateResult{}, err
	}
	// Clear the transaction-loaded primary key before reloading. Otherwise GORM adds a
	// redundant implicit id predicate, which hides the intended App-scope contract and
	// makes future key changes harder to reason about.
	account = model.ExternalAdminAccount{}
	if err := s.db.WithContext(ctx).Where("id = ? AND app_code = ?", id, appCode).First(&account).Error; err != nil {
		return PermissionUpdateResult{}, err
	}
	result.Account, err = s.accountDTOWithRelations(ctx, account)
	if err != nil {
		return PermissionUpdateResult{}, err
	}
	return result, nil
}

func permissionValidationMessage(err error) string {
	var validationErr *PermissionValidationError
	if !errors.As(err, &validationErr) {
		return "外管权限配置不正确"
	}
	permissionLabel, permissionKnown := permissionCatalogLabel(validationErr.Permission)
	if !permissionKnown {
		// Unsupported raw codes are implementation details and may contain attacker-
		// controlled input. Keep the client error stable without reflecting that value.
		return "外管权限中包含不支持的权限"
	}
	if validationErr.Dependency != "" {
		dependencyLabel, dependencyKnown := permissionCatalogLabel(validationErr.Dependency)
		if !dependencyKnown {
			return "外管权限配置不正确"
		}
		return fmt.Sprintf("权限“%s”需同时选择“%s”", permissionLabel, dependencyLabel)
	}
	return "外管权限配置不正确"
}

func permissionCatalogLabel(code string) (string, bool) {
	code = strings.TrimSpace(code)
	for _, item := range permissionCatalog {
		if item.Code == code {
			return item.Label, true
		}
	}
	return "", false
}
