package automation

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/service/automation/runtime"
	contentstoreservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	automationstore "github.com/tidbcloud/AgentTeam/internal/store/automation"
	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
)

type authorityRoutedAutomationRunStore interface {
	GetRun(context.Context, string, string) (automationstore.AutomationRun, error)
}

type AuthorityRoutedAutomationWikiPublisher struct {
	Runs   authorityRoutedAutomationRunStore
	Legacy runtime.WikiPublisher
	Native runtime.WikiPublisher
}

func (publisher AuthorityRoutedAutomationWikiPublisher) PublishWikiPages(ctx context.Context, request runtime.WikiPublishRequest) (runtime.WikiPublishResult, error) {
	run, err := publisher.Runs.GetRun(ctx, request.WorkspaceID, request.RunID)
	if err != nil {
		return runtime.WikiPublishResult{}, err
	}
	if strings.TrimSpace(run.ContentID) != "" || strings.TrimSpace(run.ContentVersionID) != "" {
		if publisher.Native == nil {
			return runtime.WikiPublishResult{}, fmt.Errorf("native automation wiki publisher is not configured")
		}
		return publisher.Native.PublishWikiPages(ctx, request)
	}
	if publisher.Legacy == nil {
		return runtime.WikiPublishResult{}, fmt.Errorf("legacy automation wiki publisher is not configured")
	}
	return publisher.Legacy.PublishWikiPages(ctx, request)
}

type AuthorityRoutedAutomationTreePublisher struct {
	Runs   authorityRoutedAutomationRunStore
	Legacy runtime.TreePublisher
	Native runtime.TreePublisher
}

func (publisher AuthorityRoutedAutomationTreePublisher) PublishRepositoryTree(ctx context.Context, request runtime.TreePublishRequest) (runtime.TreePublishResult, error) {
	run, err := publisher.Runs.GetRun(ctx, request.WorkspaceID, request.RunID)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	if strings.TrimSpace(run.ContentID) != "" || strings.TrimSpace(run.ContentVersionID) != "" {
		if publisher.Native == nil {
			return runtime.TreePublishResult{}, fmt.Errorf("native automation tree publisher is not configured")
		}
		return publisher.Native.PublishRepositoryTree(ctx, request)
	}
	if publisher.Legacy == nil {
		return runtime.TreePublishResult{}, fmt.Errorf("legacy automation tree publisher is not configured")
	}
	return publisher.Legacy.PublishRepositoryTree(ctx, request)
}

type ContentStoreAutomationPublisher struct {
	Content     AutomationNativeContentStore
	Authority   AutomationContentAuthority
	Automations *AutomationsService
	Limits      contentpayload.Limits
	Now         func() time.Time
}

func (publisher ContentStoreAutomationPublisher) PublishRepositoryTree(ctx context.Context, request runtime.TreePublishRequest) (runtime.TreePublishResult, error) {
	normalizedLimits, err := contentpayload.NormalizeLimits(publisher.Limits)
	if err != nil {
		return runtime.TreePublishResult{}, fmt.Errorf("normalize native automation tree limits: %w", err)
	}
	limits := automationTreePublishLimits{
		MaxFiles: request.MaxFiles, MaxEntries: defaultAutomationTreePublishMaxEntries,
		MaxFileBytes: normalizedLimits.MaxFileBytes, MaxBytes: normalizedLimits.MaxTotalBytes,
	}
	if limits.MaxFiles <= 0 || limits.MaxFiles > normalizedLimits.MaxFiles {
		limits.MaxFiles = normalizedLimits.MaxFiles
	}
	files, bytesRead, err := readAutomationTreePublishFiles(request.SourcePath, limits)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	contentFiles := make([]contentpayload.File, 0, len(files))
	paths := make([]string, 0, len(files))
	for _, file := range files {
		contentFiles = append(contentFiles, contentpayload.File{Path: file.Path, Size: file.Size, Content: bytes.NewBufferString(file.Content)})
		paths = append(paths, file.Path)
	}
	versionID, err := publisher.publish(ctx, request.WorkspaceID, request.RunID, request.JobRunID, request.Repository, request.DefaultBranch, contentFiles, paths)
	if err != nil {
		return runtime.TreePublishResult{}, err
	}
	return runtime.TreePublishResult{Repository: request.Repository, Files: len(files), Bytes: bytesRead, CommitSHA: versionID}, nil
}

func (publisher ContentStoreAutomationPublisher) PublishWikiPages(ctx context.Context, request runtime.WikiPublishRequest) (runtime.WikiPublishResult, error) {
	normalizedLimits, err := contentpayload.NormalizeLimits(publisher.Limits)
	if err != nil {
		return runtime.WikiPublishResult{}, fmt.Errorf("normalize native automation wiki limits: %w", err)
	}
	maxPages := min(defaultAutomationWikiPublishMaxPages, normalizedLimits.MaxFiles)
	if request.MaxPages > 0 && request.MaxPages < maxPages {
		maxPages = request.MaxPages
	}
	pages, bytesRead, err := readAutomationWikiPublishPages(request.SourcePath, automationWikiPublishLimits{
		MaxPages: maxPages, MaxEntries: defaultAutomationWikiPublishMaxEntries,
		MaxPageBytes: min(defaultAutomationWikiPublishMaxPageBytes, normalizedLimits.MaxFileBytes),
		MaxBytes:     normalizedLimits.MaxTotalBytes,
	})
	if err != nil {
		return runtime.WikiPublishResult{}, err
	}
	files := make([]contentpayload.File, 0, len(pages))
	paths := make([]string, 0, len(pages))
	for _, page := range pages {
		path := page.Slug + ".md"
		files = append(files, contentpayload.File{Path: path, Size: int64(len(page.Body)), Content: strings.NewReader(page.Body)})
		paths = append(paths, path)
	}
	versionID, err := publisher.publish(ctx, request.WorkspaceID, request.RunID, request.JobRunID, request.Repository, request.DefaultBranch, files, paths)
	if err != nil {
		return runtime.WikiPublishResult{}, err
	}
	return runtime.WikiPublishResult{Repository: request.Repository, Pages: len(pages), Bytes: bytesRead, IndexedCommitSHA: versionID, Reconciled: true}, nil
}

func (publisher ContentStoreAutomationPublisher) publish(ctx context.Context, workspaceID, runID, jobRunID, repository, ref string, files []contentpayload.File, paths []string) (string, error) {
	if publisher.Content == nil || publisher.Authority == nil || publisher.Automations == nil || publisher.Automations.store == nil {
		return "", fmt.Errorf("native automation publisher is not configured")
	}
	run, err := publisher.Automations.store.GetRun(ctx, workspaceID, runID)
	if err != nil {
		return "", err
	}
	owner, name, err := automationPublishRepository(repository)
	if err != nil {
		return "", err
	}
	bindingKey := automationRepositoryBindingKey(owner, name)
	ctx = WithAutomationContentAccess(ctx, workspaceID, bindingKey, "", 0, run.OwnerPrincipalID,
		contentstoreservice.AccessReadMetadata, contentstoreservice.AccessPublish)
	authority, found, err := publisher.Authority.GetAuthority(ctx, workspaceID, "automation", bindingKey)
	if err != nil {
		return "", err
	}
	if !found || authority.State != contentstore.AuthorityStateNativeActive {
		return "", fmt.Errorf("automation publish target is not native-active")
	}
	content, found, err := publisher.Content.FindContentByBinding(ctx, workspaceID, "automation", bindingKey)
	if err != nil {
		return "", err
	}
	if !found || content.Generation != authority.Generation {
		return "", fmt.Errorf("automation publish target projection is unavailable")
	}
	operationKey, definitions, err := automationNativePublishProjection(publisher.Automations, runID, jobRunID, bindingKey, paths, files)
	if err != nil {
		return "", err
	}
	receipt, receiptFound, err := publisher.Content.FindVersionOperation(ctx, workspaceID, content.ID, operationKey)
	if err != nil {
		return "", err
	}
	versionID := receipt.VersionID
	expectedCurrentVersionID := content.HeadVersionID
	if !receiptFound {
		limits, err := contentpayload.NormalizeLimits(publisher.Limits)
		if err != nil {
			return "", err
		}
		committed, err := publisher.Content.CommitVersion(ctx, contentstoreservice.CommitVersionRequest{
			WorkspaceID: workspaceID, ContentID: content.ID, ExpectedHeadVersionID: content.HeadVersionID,
			ExpectedGeneration: content.Generation, Actor: run.OwnerPrincipalID, Model: contentstore.ModelTree,
			Files: files, TreeLimits: limits, OperationKey: operationKey,
			OperationNamespace: "automation-publish", OperationVersion: 1,
		})
		if err != nil {
			return "", err
		}
		versionID = committed.Version.ID
	} else {
		expectedCurrentVersionID = receipt.ExpectedHeadVersionID
	}
	sort.Strings(paths)
	changedJSON, _ := json.Marshal(paths)
	now := time.Now().UTC()
	if publisher.Now != nil {
		now = publisher.Now().UTC()
	}
	publication := automationstore.AutomationContentPublication{
		WorkspaceID: workspaceID, ContentID: content.ID, ContentVersionID: versionID,
		RepositoryOwner: owner, RepositoryName: name, Ref: firstNonEmptyString(ref, "main"),
		ChangedPathsJSON: string(changedJSON), ActorID: run.OwnerPrincipalID, CreatedAt: now,
	}
	recordMode := automationstore.ContentPublicationRecordStrict
	if receiptFound {
		recordMode = automationstore.ContentPublicationRecordRecovery
	}
	if err := publisher.Automations.store.RecordContentPublication(ctx, publication, definitions, expectedCurrentVersionID, recordMode); err != nil {
		return "", err
	}
	return versionID, nil
}

func automationNativePublishProjection(service *AutomationsService, runID, jobRunID, bindingKey string, paths []string, files []contentpayload.File) (string, []automationstore.AutomationContentDefinition, error) {
	if service == nil {
		return "", nil, fmt.Errorf("native automation publication validator is not configured")
	}
	if len(paths) != len(files) {
		return "", nil, fmt.Errorf("native automation publish payload is inconsistent")
	}
	hash := sha256.New()
	for _, value := range []string{runID, jobRunID, bindingKey} {
		hash.Write([]byte(strings.TrimSpace(value)))
		hash.Write([]byte{0})
	}
	definitions := make([]automationstore.AutomationContentDefinition, 0)
	for index, path := range paths {
		if path != files[index].Path {
			return "", nil, fmt.Errorf("native automation publish path projection is inconsistent")
		}
		hash.Write([]byte(path))
		hash.Write([]byte{0})
		data, err := io.ReadAll(io.LimitReader(files[index].Content, files[index].Size+1))
		if err != nil {
			return "", nil, fmt.Errorf("hash native automation publish payload: %w", err)
		}
		if int64(len(data)) != files[index].Size {
			return "", nil, fmt.Errorf("native automation publish payload size mismatch")
		}
		files[index].Content = bytes.NewReader(data)
		hash.Write(data)
		hash.Write([]byte{0})
		if !isAutomationSourcePath(path) {
			continue
		}
		if len(definitions) >= automationstore.MaxAutomationDefinitionsPerVersion {
			return "", nil, invalidArgument("native automation publication contains too many definitions")
		}
		if _, report := service.decodeAndValidateAutomationContent(string(data), path); !report.Valid {
			return "", nil, invalidArgument("native automation publication definition is invalid")
		}
		digest := sha256.Sum256(data)
		definitions = append(definitions, automationstore.AutomationContentDefinition{
			DefinitionPath: path, Digest: hex.EncodeToString(digest[:]), SizeBytes: int64(len(data)),
		})
	}
	return "automation-publish:" + hex.EncodeToString(hash.Sum(nil)), definitions, nil
}

var _ runtime.WikiPublisher = AuthorityRoutedAutomationWikiPublisher{}
var _ runtime.TreePublisher = AuthorityRoutedAutomationTreePublisher{}
var _ runtime.WikiPublisher = ContentStoreAutomationPublisher{}
var _ runtime.TreePublisher = ContentStoreAutomationPublisher{}
