package kb

import (
	"bytes"
	"context"
	"crypto/sha1" // #nosec G505 -- verifies immutable legacy Git blob identities during one-way cutover.
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"sort"
	"strings"
	"unicode/utf8"

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
)

const (
	kbUADashboardContentDomain             = "kb_ua_dashboard"
	kbUADashboardOperationNamespace        = "kb.ua-dashboard"
	kbUADashboardOperationVersion   uint64 = 1
)

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

// NativeUADashboardRepository owns the exact-five Dashboard tree inside one
// source-scoped Content Store lineage. Product CAS remains in the KB store;
// Content Store operation identity closes only the publication/retry window.
type NativeUADashboardRepository struct {
	Content nativeUADashboardContentStore
}

func (repository NativeUADashboardRepository) PublishUADashboard(ctx context.Context, request UADashboardRepositoryPublishRequest) (UADashboardRepositoryPublishResult, error) {
	if repository.Content == nil {
		return UADashboardRepositoryPublishResult{}, fmt.Errorf("native Dashboard content store is unavailable")
	}
	paths, err := validateUADashboardPublishFiles(request.Files)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	request.WorkspaceID = strings.TrimSpace(request.WorkspaceID)
	request.SourceID = strings.TrimSpace(request.SourceID)
	request.SourceRevisionID = strings.TrimSpace(request.SourceRevisionID)
	request.PreviousPublicationID = strings.TrimSpace(request.PreviousPublicationID)
	request.Actor = strings.TrimSpace(request.Actor)
	if request.WorkspaceID == "" || request.SourceID == "" || request.SourceRevisionID == "" || request.Actor == "" {
		return UADashboardRepositoryPublishResult{}, invalidArgument("native Dashboard publication scope is incomplete")
	}
	limits := normalizedUADashboardLimits(request.Limits)
	files, digests, totalBytes, err := nativeUADashboardFiles(paths, request.Files, limits)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}

	readCtx := uaDashboardContentContext(ctx, request.Actor, request.WorkspaceID, request.SourceID, "",
		contentservice.AccessReadMetadata, contentservice.AccessCreateLineage)
	content, found, err := repository.Content.FindContentByBinding(readCtx, request.WorkspaceID, kbUADashboardContentDomain, request.SourceID)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	if !found {
		content, err = repository.Content.CreateContent(readCtx, contentservice.CreateContentRequest{
			WorkspaceID: request.WorkspaceID, Domain: kbUADashboardContentDomain,
			BindingKey: request.SourceID, Model: contentservice.TreeModel, Actor: request.Actor,
		})
		if err != nil {
			if !contentservice.IsBindingConflict(err) {
				return UADashboardRepositoryPublishResult{}, err
			}
			content, found, err = repository.Content.FindContentByBinding(readCtx, request.WorkspaceID, kbUADashboardContentDomain, request.SourceID)
			if err != nil || !found {
				return UADashboardRepositoryPublishResult{}, errors.Join(err, fmt.Errorf("native Dashboard binding conflict"))
			}
		}
	}
	if content.Model != contentservice.TreeModel {
		return UADashboardRepositoryPublishResult{}, fmt.Errorf("native Dashboard binding model changed")
	}

	operationKey := nativeUADashboardOperationKey(request, paths, digests)
	publishCtx := uaDashboardContentContext(ctx, request.Actor, request.WorkspaceID, request.SourceID, content.ID,
		contentservice.AccessReadMetadata, contentservice.AccessPublish)
	receipt, replayed, err := repository.Content.FindVersionOperation(publishCtx, request.WorkspaceID, content.ID, operationKey)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	if replayed {
		if receipt.OperationNamespace != kbUADashboardOperationNamespace || receipt.OperationVersion != kbUADashboardOperationVersion ||
			receipt.FileCount != len(files) || receipt.SizeBytes != totalBytes {
			return UADashboardRepositoryPublishResult{}, fmt.Errorf("native Dashboard operation receipt changed")
		}
		version, err := repository.Content.GetVersion(publishCtx, request.WorkspaceID, content.ID, receipt.VersionID)
		if err != nil {
			return UADashboardRepositoryPublishResult{}, err
		}
		if version.RootDigest != receipt.RootDigest || version.FileCount != receipt.FileCount || version.SizeBytes != receipt.SizeBytes {
			return UADashboardRepositoryPublishResult{}, fmt.Errorf("native Dashboard operation result changed")
		}
		return UADashboardRepositoryPublishResult{CommitSHA: receipt.VersionID, FileDigests: digests}, nil
	}
	committed, err := repository.Content.CommitVersion(publishCtx, contentservice.CommitVersionRequest{
		WorkspaceID: request.WorkspaceID, ContentID: content.ID,
		ExpectedHeadVersionID: content.HeadVersionID, ExpectedGeneration: content.Generation,
		Actor: request.Actor, Model: contentservice.TreeModel, Checkpoint: true, Files: files,
		TreeLimits: contentpayload.Limits{
			MaxFiles: len(files), MaxFileBytes: limits.FileBytes,
			MaxTotalBytes: limits.ExtractedBytes, MaxManifestBytes: contentpayload.DefaultLimits.MaxManifestBytes,
		},
		OperationKey: operationKey, OperationNamespace: kbUADashboardOperationNamespace,
		OperationVersion: kbUADashboardOperationVersion,
	})
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	if committed.Version.FileCount != len(files) || committed.Version.SizeBytes != totalBytes || committed.Version.ParentVersionID != content.HeadVersionID {
		return UADashboardRepositoryPublishResult{}, fmt.Errorf("native Dashboard publication changed")
	}
	return UADashboardRepositoryPublishResult{CommitSHA: committed.Version.ID, FileDigests: digests}, nil
}

func (repository NativeUADashboardRepository) ReadUADashboardFile(ctx context.Context, request UADashboardRepositoryFileRequest) (UADashboardRepositoryFile, error) {
	if repository.Content == nil {
		return UADashboardRepositoryFile{}, fmt.Errorf("native Dashboard content store is unavailable")
	}
	request.WorkspaceID = strings.TrimSpace(request.WorkspaceID)
	request.SourceID = strings.TrimSpace(request.SourceID)
	request.Subject = strings.TrimSpace(request.Subject)
	path := strings.TrimSpace(request.Path)
	versionID := strings.TrimSpace(request.DashboardCommitSHA)
	expectedDigest := strings.ToLower(strings.TrimSpace(request.ExpectedBlobSHA))
	if request.WorkspaceID == "" || request.SourceID == "" || request.Subject == "" || versionID == "" ||
		!validNativeUADashboardDigest(expectedDigest) {
		return UADashboardRepositoryFile{}, invalidArgument("native Dashboard file scope is incomplete")
	}
	if _, ok := uaDashboardAllowedFiles[path]; !ok {
		return UADashboardRepositoryFile{}, invalidArgument("dashboard file path is invalid")
	}
	maxBytes := request.MaxBytes
	if maxBytes <= 0 {
		maxBytes = DefaultUADashboardImportLimits().FileBytes
	}
	readCtx := uaDashboardContentContext(ctx, request.Subject, request.WorkspaceID, request.SourceID, "", contentservice.AccessReadMetadata)
	content, found, err := repository.Content.FindContentByBinding(readCtx, request.WorkspaceID, kbUADashboardContentDomain, request.SourceID)
	if err != nil || !found {
		return UADashboardRepositoryFile{}, errors.Join(err, fmt.Errorf("native Dashboard content is unavailable"))
	}
	readCtx = uaDashboardContentContext(ctx, request.Subject, request.WorkspaceID, request.SourceID, content.ID,
		contentservice.AccessReadMetadata, contentservice.AccessReadPayload)
	body, meta, err := repository.Content.OpenFile(readCtx, request.WorkspaceID, content.ID, versionID, ".ua/"+path, nil)
	if err != nil {
		return UADashboardRepositoryFile{}, err
	}
	defer body.Close()
	if meta.SizeBytes < 0 || meta.SizeBytes > maxBytes ||
		len(expectedDigest) == sha256.Size*2 && strings.ToLower(strings.TrimSpace(string(meta.Digest))) != expectedDigest {
		return UADashboardRepositoryFile{}, fmt.Errorf("native Dashboard file does not match publication manifest")
	}
	data, err := io.ReadAll(io.LimitReader(body, maxBytes+1))
	if err != nil {
		return UADashboardRepositoryFile{}, err
	}
	if int64(len(data)) != meta.SizeBytes || int64(len(data)) > maxBytes || !utf8.Valid(data) {
		return UADashboardRepositoryFile{}, fmt.Errorf("native Dashboard file content is invalid")
	}
	if !matchesNativeUADashboardDigest(data, expectedDigest) {
		return UADashboardRepositoryFile{}, fmt.Errorf("native Dashboard file does not match publication manifest")
	}
	return UADashboardRepositoryFile{Content: data, BlobSHA: expectedDigest}, nil
}

func nativeUADashboardFiles(paths []string, source map[string][]byte, limits UADashboardImportLimits) ([]contentpayload.File, map[string]string, int64, error) {
	files := make([]contentpayload.File, 0, len(paths))
	digests := make(map[string]string, len(paths))
	var total int64
	for _, path := range paths {
		data := source[path]
		if int64(len(data)) > limits.FileBytes || total > limits.ExtractedBytes-int64(len(data)) {
			return nil, nil, 0, fmt.Errorf("%w: Dashboard files exceed publication limits", ErrUADashboardArchiveLimit)
		}
		total += int64(len(data))
		sum := sha256.Sum256(data)
		digests[path] = hex.EncodeToString(sum[:])
		files = append(files, contentpayload.File{Path: ".ua/" + path, Size: int64(len(data)), Content: bytes.NewReader(data)})
	}
	return files, digests, total, nil
}

func nativeUADashboardOperationKey(request UADashboardRepositoryPublishRequest, paths []string, digests map[string]string) string {
	hash := sha256.New()
	for _, value := range []string{
		request.WorkspaceID, request.SourceID, request.SourceRevisionID,
		request.PreviousPublicationID, request.Actor,
	} {
		_, _ = io.WriteString(hash, value)
		_, _ = hash.Write([]byte{0})
	}
	ordered := append([]string(nil), paths...)
	sort.Strings(ordered)
	for _, path := range ordered {
		_, _ = io.WriteString(hash, path)
		_, _ = hash.Write([]byte{0})
		_, _ = io.WriteString(hash, digests[path])
		_, _ = hash.Write([]byte{0})
	}
	return kbUADashboardOperationNamespace + ":" + hex.EncodeToString(hash.Sum(nil))
}

func uaDashboardContentContext(ctx context.Context, subject, workspaceID, sourceID, contentID string, actions ...contentservice.AccessAction) context.Context {
	return contentservice.WithAccessPrincipal(ctx, contentservice.AccessPrincipal{
		Subject: strings.TrimSpace(subject), SubjectType: kbContentSubjectUser,
		WorkspaceID: strings.TrimSpace(workspaceID), Domain: kbUADashboardContentDomain,
		BindingKey: strings.TrimSpace(sourceID), ContentID: strings.TrimSpace(contentID), Actions: actions,
	})
}

func validNativeUADashboardDigest(value string) bool {
	decoded, err := hex.DecodeString(value)
	return err == nil && (len(decoded) == sha256.Size || len(decoded) == sha1.Size)
}

func matchesNativeUADashboardDigest(data []byte, expected string) bool {
	if len(expected) == sha1.Size*2 {
		// A migrated legacy manifest carries the immutable Git blob digest until
		// the first native Dashboard publication replaces that product pointer.
		return plumbing.ComputeHash(plumbing.BlobObject, data).String() == expected
	}
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:]) == expected
}

var _ UADashboardRepository = NativeUADashboardRepository{}
