package roles

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
)

const (
	rolePackageContentDomain = "role"
	rolePackageRefScheme     = "contentstore"

	// Role operations accept at most a 1 MiB request. These storage bounds keep
	// work tied to that explicit request and reject pathological internal callers.
	rolePackageMaxFiles         = 256
	rolePackageMaxFileBytes     = int64(1 << 20)
	rolePackageMaxTotalBytes    = int64(1 << 20)
	rolePackageMaxManifestBytes = int64(256 << 10)
)

type nativeRolePackageContentService interface {
	FindContentByBinding(context.Context, string, string, string) (contentservice.Content, bool, error)
	FindVersionOperation(context.Context, string, string, string) (contentservice.VersionOperationResult, bool, error)
	CreateContent(context.Context, contentservice.CreateContentRequest) (contentservice.Content, error)
	CommitVersion(context.Context, contentservice.CommitVersionRequest) (contentservice.CommitVersionResult, error)
	OpenFile(context.Context, string, string, string, string, *contentpayload.ByteRange) (io.ReadCloser, contentpayload.BlobMeta, error)
}

// NativeRolePackageContentStore maps each Role to one tree lineage and every
// Role package revision to one immutable ContentVersion. It receives only
// calls already admitted by the Role service and narrows each call again to an
// exact binding/content/action before entering Content Store.
type NativeRolePackageContentStore struct {
	Content nativeRolePackageContentService
}

func (s NativeRolePackageContentStore) StoreRolePackage(ctx context.Context, input StoreRolePackageInput) (StoreRolePackageOutput, error) {
	workspaceID := strings.TrimSpace(input.WorkspaceID)
	roleID := strings.TrimSpace(input.RoleID)
	versionID := strings.TrimSpace(input.VersionID)
	actor := strings.TrimSpace(input.Actor)
	if s.Content == nil || workspaceID == "" || roleID == "" || versionID == "" || actor == "" {
		return StoreRolePackageOutput{}, errRolePackageContentRequired()
	}
	files, storedAssets, err := rolePackageFiles(input.Assets)
	if err != nil {
		return StoreRolePackageOutput{}, err
	}
	content, err := s.findOrCreateContent(ctx, workspaceID, roleID, actor)
	if err != nil {
		return StoreRolePackageOutput{}, rolePackageDependencyError(err)
	}
	writeCtx := rolePackageContentContext(ctx, actor, workspaceID, roleID, content.ID, content.Generation, contentservice.AccessPublish)
	operationKey := strings.TrimSpace(input.OperationKey)
	expectedContentVersionID := strings.TrimSpace(input.ExpectedContentVersionID)
	if operationKey == "" {
		operationKey = "role.package:version:" + versionID
	}
	receipt, found, err := s.Content.FindVersionOperation(writeCtx, workspaceID, content.ID, operationKey)
	if err != nil {
		return StoreRolePackageOutput{}, rolePackageDependencyError(fmt.Errorf("find role package operation: %w", err))
	}
	if found {
		if receipt.OperationKey != operationKey || receipt.OperationNamespace != "role.package" || receipt.OperationVersion != 1 {
			return StoreRolePackageOutput{}, rolePackageDependencyError(fmt.Errorf("role package operation receipt does not match request"))
		}
		if strings.TrimSpace(content.HeadVersionID) != strings.TrimSpace(receipt.VersionID) {
			return StoreRolePackageOutput{}, ErrRoleVersionConflict
		}
		return storedRolePackageOutput(workspaceID, content.ID, receipt.VersionID, storedAssets), nil
	}
	if strings.TrimSpace(content.HeadVersionID) != expectedContentVersionID {
		return StoreRolePackageOutput{}, ErrRoleVersionConflict
	}
	committed, err := s.Content.CommitVersion(writeCtx, contentservice.CommitVersionRequest{
		WorkspaceID: workspaceID, ContentID: content.ID,
		ExpectedHeadVersionID: expectedContentVersionID, ExpectedGeneration: content.Generation,
		Actor: actor, Model: contentservice.TreeModel, Checkpoint: true, Files: files,
		OperationKey: operationKey, OperationNamespace: "role.package", OperationVersion: 1,
		TreeLimits: contentpayload.Limits{
			MaxFiles: rolePackageMaxFiles, MaxFileBytes: rolePackageMaxFileBytes,
			MaxTotalBytes: rolePackageMaxTotalBytes, MaxManifestBytes: rolePackageMaxManifestBytes,
		},
	})
	if err != nil {
		if contentservice.IsConflict(err) {
			return StoreRolePackageOutput{}, ErrRoleVersionConflict
		}
		return StoreRolePackageOutput{}, rolePackageDependencyError(fmt.Errorf("commit role package version: %w", err))
	}
	return storedRolePackageOutput(workspaceID, content.ID, committed.Version.ID, storedAssets), nil
}

func storedRolePackageOutput(workspaceID, contentID, versionID string, storedAssets map[string]RolePackageStoredAsset) StoreRolePackageOutput {
	for assetPath, asset := range storedAssets {
		asset.ContentID = contentID
		asset.VersionID = versionID
		asset.ContentRef = nativeRolePackageContentRef(workspaceID, contentID, versionID, assetPath)
		storedAssets[assetPath] = asset
	}
	return StoreRolePackageOutput{ContentID: contentID, VersionID: versionID, Assets: storedAssets}
}

func (s NativeRolePackageContentStore) findOrCreateContent(ctx context.Context, workspaceID, roleID, actor string) (contentservice.Content, error) {
	readCtx := rolePackageContentContext(ctx, actor, workspaceID, roleID, "", 0, contentservice.AccessReadMetadata)
	content, found, err := s.Content.FindContentByBinding(readCtx, workspaceID, rolePackageContentDomain, roleID)
	if err != nil {
		return contentservice.Content{}, fmt.Errorf("find role package content: %w", err)
	}
	if found {
		if content.Model != contentservice.TreeModel {
			return contentservice.Content{}, fmt.Errorf("role package content uses model %q", content.Model)
		}
		return content, nil
	}
	createCtx := rolePackageContentContext(ctx, actor, workspaceID, roleID, "", 0, contentservice.AccessCreateLineage)
	content, err = s.Content.CreateContent(createCtx, contentservice.CreateContentRequest{
		WorkspaceID: workspaceID, Domain: rolePackageContentDomain,
		BindingKey: roleID, Model: contentservice.TreeModel, Actor: actor,
	})
	if err == nil {
		return content, nil
	}
	if !contentservice.IsBindingConflict(err) {
		return contentservice.Content{}, fmt.Errorf("create role package content: %w", err)
	}
	content, found, findErr := s.Content.FindContentByBinding(readCtx, workspaceID, rolePackageContentDomain, roleID)
	if findErr != nil {
		return contentservice.Content{}, fmt.Errorf("resolve concurrent role package content creation: %w", findErr)
	}
	if !found || content.Model != contentservice.TreeModel {
		return contentservice.Content{}, fmt.Errorf("resolve concurrent role package content creation: binding unavailable")
	}
	return content, nil
}

func (s NativeRolePackageContentStore) ResolveRolePackage(ctx context.Context, input ResolveRolePackageInput) (string, error) {
	if s.Content == nil || strings.TrimSpace(input.Actor) == "" {
		return "", errRolePackageContentRequired()
	}
	workspaceID, contentID, versionID, assetPath, err := parseNativeRolePackageContentRef(input.ContentRef)
	if err != nil || workspaceID != strings.TrimSpace(input.WorkspaceID) {
		return "", invalidRolePackageContract("role package content_ref is invalid")
	}
	readCtx := rolePackageContentContext(ctx, input.Actor, workspaceID, input.RoleID, contentID, 0, contentservice.AccessReadPayload)
	body, meta, err := s.Content.OpenFile(readCtx, workspaceID, contentID, versionID, assetPath, nil)
	if err != nil {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: %w", err))
	}
	defer body.Close()
	if meta.SizeBytes < 0 || meta.SizeBytes > rolePackageMaxFileBytes {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: file exceeds byte limit"))
	}
	data, err := io.ReadAll(io.LimitReader(body, rolePackageMaxFileBytes+1))
	if err != nil {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: read file: %w", err))
	}
	if int64(len(data)) > rolePackageMaxFileBytes {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: file exceeds byte limit"))
	}
	return string(data), nil
}

func rolePackageContentContext(ctx context.Context, actor, workspaceID, roleID, contentID string, generation uint64, action contentservice.AccessAction) context.Context {
	return contentservice.WithAccessPrincipal(ctx, contentservice.AccessPrincipal{
		Subject: strings.TrimSpace(actor), WorkspaceID: strings.TrimSpace(workspaceID),
		Domain: rolePackageContentDomain, BindingKey: strings.TrimSpace(roleID), ContentID: strings.TrimSpace(contentID),
		Generation: generation, Actions: []contentservice.AccessAction{action},
	})
}

func rolePackageFiles(assets []RolePackageAsset) ([]contentpayload.File, map[string]RolePackageStoredAsset, error) {
	if len(assets) == 0 || len(assets) > rolePackageMaxFiles {
		return nil, nil, invalidRolePackageContract("role package asset count is invalid")
	}
	files := make([]contentpayload.File, 0, len(assets))
	stored := make(map[string]RolePackageStoredAsset, len(assets))
	var totalBytes int64
	for _, asset := range assets {
		assetPath, err := rolePackageAssetPath(asset)
		if err != nil {
			return nil, nil, err
		}
		if _, duplicate := stored[assetPath]; duplicate {
			return nil, nil, invalidRolePackageContract(fmt.Sprintf("duplicate role package asset path %q", assetPath))
		}
		content := strings.TrimSpace(asset.Content)
		size := int64(len(content))
		if size == 0 || size > rolePackageMaxFileBytes || size > rolePackageMaxTotalBytes-totalBytes {
			return nil, nil, invalidRolePackageContract(fmt.Sprintf("role package asset %q exceeds content bounds", assetPath))
		}
		totalBytes += size
		files = append(files, contentpayload.File{Path: assetPath, Size: size, Content: strings.NewReader(content)})
		stored[assetPath] = RolePackageStoredAsset{Path: assetPath, ContentHash: rolePackageContentHash(content)}
	}
	return files, stored, nil
}

func nativeRolePackageContentRef(workspaceID, contentID, versionID, assetPath string) string {
	return (&url.URL{Scheme: rolePackageRefScheme, Host: workspaceID, Path: "/" + contentID + "/" + versionID + "/" + assetPath}).String()
}

func parseNativeRolePackageContentRef(ref string) (workspaceID, contentID, versionID, assetPath string, err error) {
	parsed, err := url.Parse(strings.TrimSpace(ref))
	if err != nil || parsed.Scheme != rolePackageRefScheme || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
		return "", "", "", "", errors.New("invalid native role package ref")
	}
	parts := strings.Split(strings.TrimPrefix(parsed.Path, "/"), "/")
	if len(parts) < 3 || parts[0] == "" || parts[1] == "" {
		return "", "", "", "", errors.New("invalid native role package ref path")
	}
	assetPath = strings.Join(parts[2:], "/")
	if assetPath == "" || strings.Contains(assetPath, "\\") {
		return "", "", "", "", errors.New("invalid native role package asset path")
	}
	for _, part := range parts {
		if part == "" || part == "." || part == ".." {
			return "", "", "", "", errors.New("invalid native role package ref segment")
		}
	}
	return parsed.Host, parts[0], parts[1], assetPath, nil
}
