package roles

import (
	"context"
	"errors"
	"fmt"
	"path"
	"regexp"
	"strings"

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

const (
	roleContentRefDefinition   = "IDENTITY.md"
	roleContentRefKnowledgeDir = ".agents/knowledge"
	roleContentRefSkillDir     = ".agents/skills"
)

var roleContentRefSanitizer = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)

var errRolePackageContentRequiredSentinel = errors.New("role package content storage is required")
var errRolePackageDependencyUnavailable = errors.New("role package dependency unavailable")
var errRoleDependencyUnavailable = errors.New("role dependency unavailable")

// RolePackageAsset is one canonical package file payload that must be written
// outside the DB before manifest metadata is committed.
type RolePackageAsset struct {
	ItemType string
	Name     string
	Content  string
}

// RolePackageStoredAsset is the durable metadata returned by the selected
// package content store for one persisted asset.
type RolePackageStoredAsset struct {
	Path        string
	ContentRef  string
	ContentHash string
	ContentID   string
	VersionID   string
}

// RolePackageContentStore persists and resolves Role package file payloads.
// The owning Role service selects exactly one authority-gated implementation;
// it defaults to FailClosedRolePackageContentStore when none is configured.
type RolePackageContentStore interface {
	StoreRolePackage(ctx context.Context, input StoreRolePackageInput) (StoreRolePackageOutput, error)
	ResolveRolePackage(ctx context.Context, input ResolveRolePackageInput) (string, error)
}

// StoreRolePackageInput describes the immutable Role package revision to write
// before DB manifest metadata is persisted.
type StoreRolePackageInput struct {
	WorkspaceID              string
	RoleID                   string
	VersionID                string
	OperationKey             string
	ExpectedContentVersionID string
	// RequireNative prevents a native-only operation from being redirected to
	// AGS if authority changes before writer admission.
	RequireNative bool
	Actor         string
	Assets        []RolePackageAsset
}

// ResolveRolePackageInput carries the already-authorized Role identity and one
// exact stored asset reference. The content owner never infers authority from
// a URL or caller-controlled repository name.
type ResolveRolePackageInput struct {
	WorkspaceID string
	RoleID      string
	Actor       string
	ContentRef  string
}

// StoreRolePackageOutput maps logical package paths to their durable refs and
// integrity hashes.
type StoreRolePackageOutput struct {
	ContentID string
	VersionID string
	Assets    map[string]RolePackageStoredAsset
}

// FailClosedRolePackageContentStore rejects Role package writes and reads when
// no role package content store is configured.
type FailClosedRolePackageContentStore struct{}

func (FailClosedRolePackageContentStore) StoreRolePackage(context.Context, StoreRolePackageInput) (StoreRolePackageOutput, error) {
	return StoreRolePackageOutput{}, errRolePackageContentRequired()
}

func (FailClosedRolePackageContentStore) ResolveRolePackage(context.Context, ResolveRolePackageInput) (string, error) {
	return "", errRolePackageContentRequired()
}

func errRolePackageContentRequired() error {
	return fmt.Errorf("%w: %w", errRolePackageContentRequiredSentinel, invalidRolePackageContract("role package content storage is required"))
}

func isRolePackageContentRequired(err error) bool {
	return errors.Is(err, errRolePackageContentRequiredSentinel)
}

// IsRolePackageDependencyUnavailable reports whether role package storage
// could not reach or use its selected authority owner.
func IsRolePackageDependencyUnavailable(err error) bool {
	return errors.Is(err, errRolePackageDependencyUnavailable)
}

// IsRoleDependencyUnavailable reports whether applying a role could not finish
// because a resource service or repository dependency failed.
func IsRoleDependencyUnavailable(err error) bool {
	return errors.Is(err, errRoleDependencyUnavailable) ||
		IsRolePackageDependencyUnavailable(err)
}

// RoleDependencyUnavailableMessage returns a user-facing dependency message for
// API clients. It intentionally avoids exposing AGS/GitHub protocol details.
func RoleDependencyUnavailableMessage(err error) string {
	if IsRolePackageDependencyUnavailable(err) {
		return "role content is temporarily unavailable. Try again later or regenerate the role draft."
	}
	message := strings.TrimSpace(err.Error())
	for {
		next := strings.TrimSpace(strings.TrimPrefix(message, errRoleDependencyUnavailable.Error()+":"))
		next = strings.TrimSpace(strings.TrimPrefix(next, errRolePackageDependencyUnavailable.Error()+":"))
		if next == message {
			break
		}
		message = next
	}
	if message == "" {
		return "role dependencies are temporarily unavailable"
	}
	return message
}

func invalidRolePackageContract(message string) error {
	return fmt.Errorf("%s: %w", strings.TrimSpace(message), repos.ErrInvalidArgument)
}

func rolePackageAssetPath(asset RolePackageAsset) (string, error) {
	switch strings.TrimSpace(asset.ItemType) {
	case ManifestItemRoleDefinitionAsset:
		return roleContentRefDefinition, nil
	case AttachmentPortableKnowledge:
		name := sanitizedRolePackageAssetName(asset.Name, "knowledge")
		return path.Join(roleContentRefKnowledgeDir, name+".md"), nil
	case AttachmentSkill:
		name := sanitizedRolePackageAssetName(asset.Name, "skill")
		return path.Join(roleContentRefSkillDir, name, "SKILL.md"), nil
	default:
		return "", invalidRolePackageContract(fmt.Sprintf("role package item type %q does not support stored file payloads", strings.TrimSpace(asset.ItemType)))
	}
}

func sanitizedRolePackageAssetName(name, fallback string) string {
	trimmed := strings.TrimSpace(strings.ToLower(name))
	trimmed = strings.ReplaceAll(trimmed, " ", "-")
	trimmed = roleContentRefSanitizer.ReplaceAllString(trimmed, "-")
	trimmed = strings.Trim(trimmed, "-./")
	if trimmed == "" {
		trimmed = fallback
	}
	return trimmed
}
