package roles

import (
	"context"
	"math"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	rolesstore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
)

type RoleVersion struct {
	ID               string
	Version          int
	ContentID        string
	ContentVersionID string
	CreatedBy        string
	CreatedAt        time.Time
	Active           bool
}

type RoleVersionPage struct {
	Items   []RoleVersion
	Page    int
	PerPage int
	HasMore bool
}

func checkedRoleVersionPageOffset(page, perPage int) (int, bool) {
	if page <= 0 || perPage <= 0 || perPage > rolesstore.MaxRoleVersionPageSize {
		return 0, false
	}
	pageIndex := page - 1
	if pageIndex > rolesstore.MaxRoleVersionPageOffset/perPage {
		return 0, false
	}
	offset := pageIndex * perPage
	if offset < 0 || offset > rolesstore.MaxRoleVersionPageOffset || offset > math.MaxInt-perPage {
		return 0, false
	}
	return offset, true
}

func (s *Service) nativeRolePackageRead(ctx context.Context, workspaceID, roleID string) (bool, error) {
	gate, ok := s.contentStore.(rolePackageNativeReadGate)
	if !ok {
		return false, nil
	}
	return gate.NativeRolePackageRead(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(roleID))
}

// ListRoleVersions returns one bounded page of exact native Role projections.
// It does not hydrate manifests or payloads per row.
func (s *Service) ListRoleVersions(ctx context.Context, workspaceID, subject, roleID string, page, perPage int) (RoleVersionPage, error) {
	offset, ok := checkedRoleVersionPageOffset(page, perPage)
	if !ok {
		return RoleVersionPage{}, invalidRoleArgument("role version page is out of bounds")
	}
	if err := s.validate(workspaceID, subject); err != nil {
		return RoleVersionPage{}, err
	}
	role, err := s.store.GetRole(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(roleID))
	if err != nil {
		return RoleVersionPage{}, mapStoreNotFound(err)
	}
	grant, err := s.roleGrant(ctx, role, subject)
	if err != nil {
		return RoleVersionPage{}, err
	}
	if grant == "" {
		return RoleVersionPage{}, repos.ErrForbidden
	}
	native, err := s.nativeRolePackageRead(ctx, role.WorkspaceID, role.ID)
	if err != nil {
		return RoleVersionPage{}, err
	}
	if !native {
		return RoleVersionPage{}, invalidRoleArgument("native role version history is not active")
	}
	active, err := s.store.GetActiveRoleVersion(ctx, role.WorkspaceID, role.ID)
	if err != nil {
		return RoleVersionPage{}, mapStoreNotFound(err)
	}
	if strings.TrimSpace(active.ContentID) == "" || strings.TrimSpace(active.ContentVersionID) == "" {
		return RoleVersionPage{}, invalidRoleArgument("native role version history is not active")
	}
	rows, err := s.store.ListRoleVersions(ctx, role.WorkspaceID, role.ID, active.ContentID, perPage, offset)
	if err != nil {
		return RoleVersionPage{}, err
	}
	hasMore := len(rows) > perPage
	if hasMore {
		rows = rows[:perPage]
	}
	items := make([]RoleVersion, 0, len(rows))
	for _, row := range rows {
		items = append(items, RoleVersion{
			ID: row.ID, Version: row.Version, ContentID: row.ContentID, ContentVersionID: row.ContentVersionID,
			CreatedBy: row.CreatedBy, CreatedAt: row.CreatedAt, Active: row.ID == active.ID,
		})
	}
	return RoleVersionPage{Items: items, Page: page, PerPage: perPage, HasMore: hasMore}, nil
}

// RollbackRoleVersion republishes one exact historical package as a new
// immutable ContentVersion and RoleVersion. The expected active version makes
// the Content-head and product CAS share the same request baseline.
func (s *Service) RollbackRoleVersion(ctx context.Context, workspaceID, subject, roleID, expectedRoleVersionID, targetRoleVersionID string) (Role, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return Role{}, err
	}
	expectedRoleVersionID = strings.TrimSpace(expectedRoleVersionID)
	targetRoleVersionID = strings.TrimSpace(targetRoleVersionID)
	if expectedRoleVersionID == "" || targetRoleVersionID == "" {
		return Role{}, invalidRoleArgument("expected and target role version ids are required")
	}
	current, err := s.GetRole(ctx, workspaceID, subject, roleID)
	if err != nil {
		return Role{}, err
	}
	if !roleCanManage(current.Role) {
		return Role{}, repos.ErrForbidden
	}
	native, err := s.nativeRolePackageRead(ctx, current.WorkspaceID, current.ID)
	if err != nil {
		return Role{}, err
	}
	if !native {
		return Role{}, invalidRoleArgument("native role rollback is not active")
	}
	if current.RoleVersionID != expectedRoleVersionID {
		return Role{}, ErrRoleVersionConflict
	}
	if targetRoleVersionID == current.RoleVersionID {
		return Role{}, invalidRoleArgument("target role version is already active")
	}
	if current.contentID == "" || current.contentVersionID == "" {
		return Role{}, invalidRoleArgument("native role rollback is not active")
	}
	target, err := s.GetRoleVersion(ctx, workspaceID, subject, roleID, targetRoleVersionID)
	if err != nil {
		return Role{}, err
	}
	if target.contentID == "" || target.contentVersionID == "" || target.contentID != current.contentID {
		return Role{}, invalidRoleArgument("role version content binding does not match active lineage")
	}
	active := current.IsActive
	input := Mutation{
		Name: current.Name, Description: current.Description, Locale: current.Locale, Scenario: current.Scenario,
		Icon: current.Icon, Tags: append([]string(nil), current.Tags...), Definition: target.Definition,
		RuntimeHints: target.RuntimeHints, Attachments: attachmentsFromRoleManifest(target.RolePackageManifest), IsActive: &active,
	}
	operationKey := rolePackageStableOperationKey("rollback", expectedRoleVersionID, target.RoleVersionID, target.contentVersionID)
	return s.updateRoleFromCurrent(ctx, subject, current, input, "", operationKey, true)
}
