package roles

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sort"
	"strings"

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

func (s *Service) persistRolePackageContent(ctx context.Context, workspaceID, roleID, actor, operationKey, expectedContentVersionID string, requireNative bool, items []rolesstore.RolePackageManifestItem) ([]rolesstore.RolePackageManifestItem, error) {
	if len(items) == 0 {
		return nil, invalidRolePackageContract("active role package manifest is missing")
	}
	versionID := strings.TrimSpace(items[0].RoleVersionID)
	if versionID == "" {
		return nil, invalidRolePackageContract("role package version id is missing")
	}
	stored, err := s.contentStore.StoreRolePackage(ctx, StoreRolePackageInput{WorkspaceID: workspaceID, RoleID: roleID, VersionID: versionID, OperationKey: operationKey, ExpectedContentVersionID: expectedContentVersionID, RequireNative: requireNative, Actor: actor, Assets: inlineRolePackageAssets(items)})
	if err != nil {
		return nil, err
	}
	out := make([]rolesstore.RolePackageManifestItem, 0, len(items))
	for _, item := range items {
		updated := item
		updated.Content = ""
		switch roleManifestItemProjection(item) {
		case roleManifestItemProjectionExact:
			assetPath, pathErr := rolePackageAssetPath(RolePackageAsset{ItemType: item.ItemType, Name: item.Name, Content: item.Content})
			if pathErr != nil {
				return nil, pathErr
			}
			asset, ok := stored.Assets[assetPath]
			if !ok {
				return nil, invalidRolePackageContract(fmt.Sprintf("role package asset %q was not persisted", assetPath))
			}
			updated.ContentRef = asset.ContentRef
			updated.ContentHash = asset.ContentHash
			updated.ContentID = asset.ContentID
			updated.ContentVersionID = asset.VersionID
			if strings.TrimSpace(asset.ContentID) != "" && strings.TrimSpace(asset.VersionID) != "" {
				updated.ContentPath = asset.Path
			} else {
				updated.ContentPath = ""
			}
		case roleManifestItemProjectionExternal:
			updated.ContentRef = ""
			updated.ContentHash = ""
			updated.ContentID = ""
			updated.ContentVersionID = ""
			updated.ContentPath = ""
		default:
			return nil, invalidRolePackageContract(fmt.Sprintf("role package item type %q is not portable", strings.TrimSpace(item.ItemType)))
		}
		out = append(out, updated)
	}
	return out, nil
}

func rolePackageStableOperationKey(kind string, parts ...string) string {
	hash := sha256.New()
	_, _ = hash.Write([]byte(strings.TrimSpace(kind)))
	for _, part := range parts {
		_, _ = hash.Write([]byte{0})
		_, _ = hash.Write([]byte(strings.TrimSpace(part)))
	}
	return "role.package:" + strings.TrimSpace(kind) + ":" + hex.EncodeToString(hash.Sum(nil))
}

func rolePackageManifestDigest(items []rolesstore.RolePackageManifestItem) (string, error) {
	assets := inlineRolePackageAssets(items)
	entries := make([]string, 0, len(assets))
	for _, asset := range assets {
		assetPath, err := rolePackageAssetPath(asset)
		if err != nil {
			return "", err
		}
		entries = append(entries, assetPath+"\x00"+rolePackageContentHash(strings.TrimSpace(asset.Content)))
	}
	sort.Strings(entries)
	hash := sha256.New()
	for _, entry := range entries {
		_, _ = hash.Write([]byte(entry))
		_, _ = hash.Write([]byte{0})
	}
	return hex.EncodeToString(hash.Sum(nil)), nil
}

func (s *Service) resolveManifestItemContentsForRoleRead(ctx context.Context, actor string, role rolesstore.Role, version rolesstore.RoleVersion, items []rolesstore.RolePackageManifestItem) (map[string]string, error) {
	if _, err := validateRoleVersionExactProjection(version, items); err != nil {
		return nil, err
	}
	resolved := map[string]string{}
	for _, item := range items {
		ref := strings.TrimSpace(item.ContentRef)
		if ref == "" {
			continue
		}
		if _, ok := resolved[ref]; ok {
			continue
		}
		content, err := s.resolveManifestItemContent(ctx, actor, item)
		if err != nil {
			// Legacy Role packages retain their existing recoverable-read
			// behavior. Once an exact native projection exists, neither an
			// invalid ref nor native unavailability may fall through to the DB
			// summary or an AGS branch.
			if strings.TrimSpace(item.ContentID) != "" || strings.TrimSpace(item.ContentVersionID) != "" || strings.TrimSpace(item.ContentPath) != "" {
				return nil, err
			}
			continue
		}
		resolved[ref] = content
	}
	s.addRecoverableRoleDefinitionContent(ctx, role, items, resolved)
	return resolved, nil
}

func (s *Service) resolveManifestItemContents(ctx context.Context, actor string, items []rolesstore.RolePackageManifestItem) (map[string]string, error) {
	resolved := map[string]string{}
	for _, item := range items {
		ref := strings.TrimSpace(item.ContentRef)
		if ref == "" {
			continue
		}
		if _, ok := resolved[ref]; ok {
			continue
		}
		content, err := s.resolveManifestItemContent(ctx, actor, item)
		if err != nil {
			return nil, err
		}
		resolved[ref] = content
	}
	return resolved, nil
}

func (s *Service) resolveAvailableManifestItemContents(ctx context.Context, actor string, items []rolesstore.RolePackageManifestItem) map[string]string {
	resolved := map[string]string{}
	for _, item := range items {
		ref := strings.TrimSpace(item.ContentRef)
		if ref == "" {
			continue
		}
		if _, ok := resolved[ref]; ok {
			continue
		}
		content, err := s.resolveManifestItemContent(ctx, actor, item)
		if err != nil {
			continue
		}
		resolved[ref] = content
	}
	return resolved
}

func (s *Service) resolveRequiredManifestItemContents(ctx context.Context, actor string, role rolesstore.Role, version rolesstore.RoleVersion, items []rolesstore.RolePackageManifestItem) (map[string]string, error) {
	native, err := validateRoleVersionExactProjection(version, items)
	if err != nil {
		return nil, err
	}
	if native {
		resolved, err := s.resolveManifestItemContents(ctx, actor, items)
		if err != nil {
			return nil, err
		}
		if err := ensureRequiredManifestItemContentsResolved(items, resolved); err != nil {
			return nil, err
		}
		return resolved, nil
	}
	resolved := s.resolveAvailableManifestItemContents(ctx, actor, items)
	s.addRecoverableRoleDefinitionContent(ctx, role, items, resolved)
	if err := ensureRequiredManifestItemContentsResolved(items, resolved); err != nil {
		return nil, err
	}
	return resolved, nil
}

func validateRoleVersionExactProjection(version rolesstore.RoleVersion, items []rolesstore.RolePackageManifestItem) (bool, error) {
	contentID := strings.TrimSpace(version.ContentID)
	contentVersionID := strings.TrimSpace(version.ContentVersionID)
	if (contentID == "") != (contentVersionID == "") {
		return false, rolePackageDependencyError(fmt.Errorf("role version exact content projection is incomplete"))
	}
	native := contentID != ""
	hasExactItem := false
	for _, item := range items {
		itemExact := strings.TrimSpace(item.ContentID) != "" || strings.TrimSpace(item.ContentVersionID) != "" || strings.TrimSpace(item.ContentPath) != ""
		projection := roleManifestItemProjection(item)
		if projection == roleManifestItemProjectionInvalid {
			return false, rolePackageDependencyError(fmt.Errorf("role package item type %q is invalid", strings.TrimSpace(item.ItemType)))
		}
		if native {
			if projection == roleManifestItemProjectionExact && (!itemExact || strings.TrimSpace(item.ContentRef) == "") {
				return false, rolePackageDependencyError(fmt.Errorf("role package inline item exact content projection is missing"))
			}
			if projection == roleManifestItemProjectionExternal && (itemExact || strings.TrimSpace(item.ContentRef) != "" || strings.TrimSpace(item.ContentHash) != "" || strings.TrimSpace(item.Content) != "") {
				return false, rolePackageDependencyError(fmt.Errorf("role package external resource item has local content projection"))
			}
		}
		if !itemExact {
			continue
		}
		hasExactItem = true
		if err := validateRoleManifestExactContentRef(item); err != nil {
			return false, err
		}
		if !native || strings.TrimSpace(item.ContentID) != contentID || strings.TrimSpace(item.ContentVersionID) != contentVersionID {
			return false, rolePackageDependencyError(fmt.Errorf("role version exact content projection does not match manifest"))
		}
	}
	if native != hasExactItem {
		return false, rolePackageDependencyError(fmt.Errorf("role version exact content projection does not match manifest"))
	}
	return native, nil
}

type roleManifestItemProjectionKind uint8

const (
	roleManifestItemProjectionInvalid roleManifestItemProjectionKind = iota
	roleManifestItemProjectionExact
	roleManifestItemProjectionExternal
)

func roleManifestItemProjection(item rolesstore.RolePackageManifestItem) roleManifestItemProjectionKind {
	switch strings.TrimSpace(item.ItemType) {
	case ManifestItemRoleDefinitionAsset, AttachmentPortableKnowledge:
		return roleManifestItemProjectionExact
	case AttachmentSkill:
		if strings.TrimSpace(item.SourceResourceID) == "" {
			return roleManifestItemProjectionExact
		}
		return roleManifestItemProjectionExternal
	case AttachmentKnowledgeBase, AttachmentMemory:
		return roleManifestItemProjectionExternal
	default:
		return roleManifestItemProjectionInvalid
	}
}

func ensureRequiredManifestItemContentsResolved(items []rolesstore.RolePackageManifestItem, contentByRef map[string]string) error {
	for _, item := range items {
		if !item.Required && strings.TrimSpace(item.ItemType) != ManifestItemRoleDefinitionAsset {
			continue
		}
		ref := strings.TrimSpace(item.ContentRef)
		if ref == "" {
			continue
		}
		if strings.TrimSpace(contentByRef[ref]) != "" {
			continue
		}
		return rolePackageDependencyError(fmt.Errorf("required role package content %q is unavailable", firstNonEmpty(item.Name, item.ItemType)))
	}
	return nil
}

func (s *Service) addRecoverableRoleDefinitionContent(ctx context.Context, role rolesstore.Role, items []rolesstore.RolePackageManifestItem, contentByRef map[string]string) {
	if contentByRef == nil {
		return
	}
	item, ok := roleDefinitionManifestItem(items)
	if !ok {
		return
	}
	ref := strings.TrimSpace(item.ContentRef)
	if ref == "" || strings.TrimSpace(contentByRef[ref]) != "" {
		return
	}
	for _, content := range s.roleDefinitionRecoveryCandidates(ctx, role) {
		if !rolePackageContentMatchesManifest(item, content) {
			continue
		}
		contentByRef[ref] = strings.TrimSpace(content)
		return
	}
}

func (s *Service) roleDefinitionRecoveryCandidates(ctx context.Context, role rolesstore.Role) []string {
	candidates := make([]string, 0, 3)
	if version, err := s.store.GetActiveRoleVersion(ctx, role.WorkspaceID, role.ID); err == nil {
		candidates = append(candidates, version.Definition)
	}
	if preset, ok, err := selectPresetTemplate(role.SourcePresetID, role.Locale); err == nil && ok && preset.Version == role.SourcePresetVersion {
		candidates = append(candidates, preset.Definition)
	}
	candidates = append(candidates, role.Definition)
	return candidates
}

func (s *Service) resolveManifestItemContent(ctx context.Context, actor string, item rolesstore.RolePackageManifestItem) (string, error) {
	ref := strings.TrimSpace(item.ContentRef)
	if ref == "" {
		return "", nil
	}
	if err := validateRoleManifestExactContentRef(item); err != nil {
		return "", err
	}
	request := ResolveRolePackageInput{WorkspaceID: item.WorkspaceID, RoleID: item.RoleID, Actor: actor, ContentRef: ref}
	content, err := s.contentStore.ResolveRolePackage(ctx, request)
	if err != nil {
		if strings.TrimSpace(item.ContentID) != "" {
			return "", err
		}
		fallbackRef := rolePackageBranchContentRef(ref, item.RoleVersionID)
		if fallbackRef == "" {
			return "", err
		}
		request.ContentRef = fallbackRef
		content, err = s.contentStore.ResolveRolePackage(ctx, request)
		if err != nil {
			return "", err
		}
	}
	if hash := strings.TrimSpace(item.ContentHash); hash != "" && !strings.EqualFold(rolePackageContentHash(content), hash) {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: content hash mismatch"))
	}
	return content, nil
}

func validateRoleManifestExactContentRef(item rolesstore.RolePackageManifestItem) error {
	contentID := strings.TrimSpace(item.ContentID)
	versionID := strings.TrimSpace(item.ContentVersionID)
	contentPath := strings.TrimSpace(item.ContentPath)
	if contentID == "" && versionID == "" && contentPath == "" {
		return nil
	}
	if contentID == "" || versionID == "" || contentPath == "" {
		return rolePackageDependencyError(fmt.Errorf("role package exact content projection is incomplete"))
	}
	workspaceID, parsedContentID, parsedVersionID, parsedPath, err := parseNativeRolePackageContentRef(item.ContentRef)
	if err != nil || workspaceID != strings.TrimSpace(item.WorkspaceID) || parsedContentID != contentID || parsedVersionID != versionID || parsedPath != contentPath {
		return rolePackageDependencyError(fmt.Errorf("role package exact content projection does not match content ref"))
	}
	return nil
}
