package automation

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

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

type AutomationNativeContentStore interface {
	FindContentByBinding(context.Context, string, string, string) (contentstoreservice.Content, bool, error)
	GetContent(context.Context, string, string) (contentstoreservice.Content, error)
	GetVersion(context.Context, string, string, string) (contentstoreservice.Version, error)
	OpenFile(context.Context, string, string, string, string, *contentpayload.ByteRange) (io.ReadCloser, contentpayload.BlobMeta, error)
	MaterializeTree(context.Context, string, string, string, string, contentpayload.MaterializeLimits) error
	CommitVersion(context.Context, contentstoreservice.CommitVersionRequest) (contentstoreservice.CommitVersionResult, error)
	FindVersionOperation(context.Context, string, string, string) (contentstoreservice.VersionOperationResult, bool, error)
}

type AutomationContentAuthority interface {
	GetAuthority(context.Context, string, string, string) (contentstore.Authority, bool, error)
}

// ContentStoreAutomationSource is the Automation-owned reader/writer over one
// exact native repository binding. Repository-shaped fields remain presentation
// aliases; all durable identity is contentId + contentVersionId + path.
type ContentStoreAutomationSource struct {
	Content   AutomationNativeContentStore
	Authority AutomationContentAuthority
	Store     *automationstore.Store
	WorkRoot  string
	Limits    contentpayload.Limits
	Now       func() time.Time
}

func (source ContentStoreAutomationSource) ResolveAutomationSource(ctx context.Context, request AutomationSourceRequest) (AutomationSourceSnapshot, error) {
	content, err := source.GetAutomationFile(ctx, request)
	if err != nil {
		return AutomationSourceSnapshot{}, err
	}
	return AutomationSourceSnapshot{
		RepositoryOwner: content.RepositoryOwner, RepositoryName: content.RepositoryName,
		DefinitionPath: content.Path, GitRef: content.Ref, CommitSHA: "", Content: content.Content,
		ContentID: content.ContentID, ContentVersionID: content.ContentVersionID,
	}, nil
}

func (source ContentStoreAutomationSource) ListAutomationRepositories(context.Context, string, int, int) ([]AutomationSourceRepository, error) {
	return []AutomationSourceRepository{}, nil
}

func (source ContentStoreAutomationSource) ListAutomationOrgRepositories(ctx context.Context, _ string, workspaceID string, page, perPage int) ([]AutomationSourceRepository, error) {
	if source.Store == nil {
		return nil, fmt.Errorf("automation content publication store is not configured")
	}
	page, perPage = normalizeAutomationPageWithMax(page, perPage, 100)
	rows, err := source.Store.ListLatestContentPublicationRepositories(ctx, workspaceID, (page-1)*perPage, perPage)
	if err != nil {
		return nil, err
	}
	repositories := make([]AutomationSourceRepository, 0, len(rows))
	for _, row := range rows {
		repositories = append(repositories, AutomationSourceRepository{
			Owner: row.RepositoryOwner, Name: row.RepositoryName,
			FullName: row.RepositoryOwner + "/" + row.RepositoryName, Private: true,
		})
	}
	return repositories, nil
}

func (source ContentStoreAutomationSource) ListAutomationFiles(ctx context.Context, request AutomationSourceRequest) ([]AutomationSourceFile, error) {
	ctx = WithAutomationContentAccess(ctx, request.WorkspaceID, automationRepositoryBindingKey(request.RepositoryOwner, request.RepositoryName), "", 0, "",
		contentstoreservice.AccessReadMetadata, contentstoreservice.AccessReadPayload, contentstoreservice.AccessReadHistory)
	publication, err := source.resolvePublication(ctx, request)
	if err != nil {
		return nil, err
	}
	definitions, err := source.Store.ListContentDefinitions(ctx, publication.WorkspaceID, publication.ContentID, publication.ContentVersionID, automationstore.MaxAutomationDefinitionsPerVersion)
	if err != nil {
		return nil, err
	}
	files := make([]AutomationSourceFile, 0, len(definitions))
	for _, definition := range definitions {
		files = append(files, AutomationSourceFile{
			RepositoryOwner: publication.RepositoryOwner, RepositoryName: publication.RepositoryName,
			Path: definition.DefinitionPath, Name: automationSourceFileName(definition.DefinitionPath),
			SHA: definition.Digest, Size: definition.SizeBytes, Ref: publication.ContentVersionID,
			ContentID: publication.ContentID, ContentVersionID: publication.ContentVersionID,
		})
	}
	return files, nil
}

func (source ContentStoreAutomationSource) GetAutomationFile(ctx context.Context, request AutomationSourceRequest) (AutomationSourceContent, error) {
	ctx = WithAutomationContentAccess(ctx, request.WorkspaceID, automationRepositoryBindingKey(request.RepositoryOwner, request.RepositoryName), "", 0, "",
		contentstoreservice.AccessReadMetadata, contentstoreservice.AccessReadPayload)
	publication, err := source.resolvePublication(ctx, request)
	if err != nil {
		return AutomationSourceContent{}, err
	}
	path := normalizeAutomationSourcePath(request.DefinitionPath)
	definitions, err := source.Store.ListContentDefinitions(ctx, publication.WorkspaceID, publication.ContentID, publication.ContentVersionID, automationstore.MaxAutomationDefinitionsPerVersion)
	if err != nil {
		return AutomationSourceContent{}, err
	}
	var projected *automationstore.AutomationContentDefinition
	for index := range definitions {
		if definitions[index].DefinitionPath == path {
			projected = &definitions[index]
			break
		}
	}
	if projected == nil {
		return AutomationSourceContent{}, ErrNotFound
	}
	reader, meta, err := source.Content.OpenFile(ctx, publication.WorkspaceID, publication.ContentID, publication.ContentVersionID, path, nil)
	if err != nil {
		return AutomationSourceContent{}, err
	}
	defer reader.Close()
	bytes, err := io.ReadAll(io.LimitReader(reader, projected.SizeBytes+1))
	digest := sha256.Sum256(bytes)
	if err != nil || int64(len(bytes)) != projected.SizeBytes || meta.SizeBytes != projected.SizeBytes ||
		string(meta.Digest) != projected.Digest || hex.EncodeToString(digest[:]) != projected.Digest {
		return AutomationSourceContent{}, fmt.Errorf("automation content definition projection mismatch")
	}
	return AutomationSourceContent{AutomationSourceFile: AutomationSourceFile{
		RepositoryOwner: publication.RepositoryOwner, RepositoryName: publication.RepositoryName,
		Path: path, Name: automationSourceFileName(path), SHA: projected.Digest, Size: projected.SizeBytes,
		Ref: publication.ContentVersionID, ContentID: publication.ContentID, ContentVersionID: publication.ContentVersionID,
	}, Content: string(bytes), Encoding: "utf-8"}, nil
}

func (source ContentStoreAutomationSource) PutAutomationFile(ctx context.Context, request AutomationSourceWriteRequest) (AutomationSourceWriteResult, error) {
	ctx = WithAutomationContentAccess(ctx, request.WorkspaceID, automationRepositoryBindingKey(request.RepositoryOwner, request.RepositoryName), "", 0, request.Actor,
		contentstoreservice.AccessReadMetadata, contentstoreservice.AccessReadPayload, contentstoreservice.AccessPublish)
	return source.publishMutation(ctx, request.WorkspaceID, request.Actor, request.RepositoryOwner, request.RepositoryName, request.DefinitionPath, request.Branch, request.Content, request.SHA, false, request.Message)
}

func (source ContentStoreAutomationSource) DeleteAutomationFile(ctx context.Context, request AutomationSourceDeleteRequest) error {
	ctx = WithAutomationContentAccess(ctx, request.WorkspaceID, automationRepositoryBindingKey(request.RepositoryOwner, request.RepositoryName), "", 0, request.Actor,
		contentstoreservice.AccessReadMetadata, contentstoreservice.AccessReadPayload, contentstoreservice.AccessPublish)
	_, err := source.publishMutation(ctx, request.WorkspaceID, request.Actor, request.RepositoryOwner, request.RepositoryName, request.DefinitionPath, request.Branch, "", request.SHA, true, request.Message)
	return err
}

func (source ContentStoreAutomationSource) publishMutation(ctx context.Context, workspaceID, actor, owner, repository, definitionPath, ref, value, expectedDigest string, deleting bool, message string) (AutomationSourceWriteResult, error) {
	publication, content, err := source.resolveMutationBase(ctx, workspaceID, owner, repository)
	if err != nil {
		return AutomationSourceWriteResult{}, err
	}
	definitionPath = normalizeAutomationSourcePath(definitionPath)
	operationKey := automationContentMutationOperationKey(publication.ContentVersionID, definitionPath, deleting, value)
	receipt, receiptFound, err := source.Content.FindVersionOperation(ctx, workspaceID, content.ID, operationKey)
	if err != nil {
		return AutomationSourceWriteResult{}, err
	}
	if content.ID != publication.ContentID || (content.HeadVersionID != publication.ContentVersionID && (!receiptFound || content.HeadVersionID != receipt.VersionID)) {
		return AutomationSourceWriteResult{}, conflict("Automation content head changed")
	}
	actor = strings.TrimSpace(actor)
	if actor == "" {
		actor = publication.ActorID
	}
	workParent, err := os.MkdirTemp(source.WorkRoot, "automation-content-")
	if err != nil {
		return AutomationSourceWriteResult{}, fmt.Errorf("create automation content workspace: %w", err)
	}
	defer os.RemoveAll(workParent)
	workRoot := filepath.Join(workParent, "tree")
	limits, err := contentpayload.NormalizeLimits(source.Limits)
	if err != nil {
		return AutomationSourceWriteResult{}, err
	}
	if err := source.Content.MaterializeTree(ctx, workspaceID, content.ID, publication.ContentVersionID, workRoot, contentpayload.MaterializeLimits{MaxFiles: limits.MaxFiles, MaxFileBytes: limits.MaxFileBytes, MaxTotalBytes: limits.MaxTotalBytes}); err != nil {
		return AutomationSourceWriteResult{}, fmt.Errorf("materialize current automation content: %w", err)
	}
	target := filepath.Join(workRoot, filepath.FromSlash(definitionPath))
	currentDigest, existed, err := automationWorkspaceFileDigest(target)
	if err != nil {
		return AutomationSourceWriteResult{}, err
	}
	if expectedDigest = strings.TrimSpace(expectedDigest); expectedDigest != "" && expectedDigest != currentDigest {
		return AutomationSourceWriteResult{}, conflict("Automation source changed")
	}
	if deleting {
		if !existed {
			return AutomationSourceWriteResult{}, ErrNotFound
		}
		if err := os.Remove(target); err != nil {
			return AutomationSourceWriteResult{}, err
		}
	} else {
		if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
			return AutomationSourceWriteResult{}, err
		}
		if err := os.WriteFile(target, []byte(value), 0o640); err != nil {
			return AutomationSourceWriteResult{}, err
		}
	}
	files, definitions, err := collectAutomationContentWorkspace(workRoot, limits, workspaceID, content.ID)
	if err != nil {
		return AutomationSourceWriteResult{}, err
	}
	committedVersionID := receipt.VersionID
	if !receiptFound {
		committed, err := source.Content.CommitVersion(ctx, contentstoreservice.CommitVersionRequest{
			WorkspaceID: workspaceID, ContentID: content.ID, ExpectedHeadVersionID: publication.ContentVersionID,
			ExpectedGeneration: content.Generation, Actor: actor, Model: contentstore.ModelTree,
			Files: files, TreeLimits: limits, OperationKey: operationKey,
			OperationNamespace: "automation-source", OperationVersion: 1,
		})
		if err != nil {
			return AutomationSourceWriteResult{}, err
		}
		committedVersionID = committed.Version.ID
	}
	changedPathsJSON, _ := json.Marshal([]string{definitionPath})
	now := time.Now().UTC()
	if source.Now != nil {
		now = source.Now().UTC()
	}
	for index := range definitions {
		definitions[index].WorkspaceID = workspaceID
		definitions[index].ContentID = content.ID
		definitions[index].ContentVersionID = committedVersionID
		definitions[index].CreatedAt = now
	}
	if err := source.Store.RecordContentPublication(ctx, automationstore.AutomationContentPublication{
		WorkspaceID: workspaceID, ContentID: content.ID, ContentVersionID: committedVersionID,
		RepositoryOwner: owner, RepositoryName: repository, Ref: strings.TrimSpace(ref),
		ChangedPathsJSON: string(changedPathsJSON), ActorID: actor, CreatedAt: now,
	}, definitions, publication.ContentVersionID, automationstore.ContentPublicationRecordStrict); err != nil {
		return AutomationSourceWriteResult{}, err
	}
	if deleting {
		return AutomationSourceWriteResult{}, nil
	}
	digest := sha256.Sum256([]byte(value))
	digestText := hex.EncodeToString(digest[:])
	return AutomationSourceWriteResult{
		Content: AutomationSourceSavedFile{AutomationSourceFile: AutomationSourceFile{
			RepositoryOwner: owner, RepositoryName: repository, Path: definitionPath,
			Name: automationSourceFileName(definitionPath), SHA: digestText, Size: int64(len(value)),
			Ref: committedVersionID, ContentID: content.ID, ContentVersionID: committedVersionID,
		}, Encoding: "utf-8"},
		Commit:  AutomationSourceCommit{ContentVersionID: committedVersionID, Message: strings.TrimSpace(message)},
		Created: !existed,
	}, nil
}

func (source ContentStoreAutomationSource) resolvePublication(ctx context.Context, request AutomationSourceRequest) (automationstore.AutomationContentPublication, error) {
	if source.Store == nil || source.Content == nil || source.Authority == nil {
		return automationstore.AutomationContentPublication{}, fmt.Errorf("automation native content source is not configured")
	}
	content, err := source.requireNativeContent(ctx, request.WorkspaceID, request.RepositoryOwner, request.RepositoryName)
	if err != nil {
		return automationstore.AutomationContentPublication{}, err
	}
	requestedRef := strings.TrimSpace(request.GitRef)
	if requestedRef != "" {
		publication, exactErr := source.Store.GetContentPublication(ctx, request.WorkspaceID, content.ID, requestedRef)
		if exactErr == nil {
			if publication.RepositoryOwner != strings.TrimSpace(request.RepositoryOwner) || publication.RepositoryName != strings.TrimSpace(request.RepositoryName) {
				return automationstore.AutomationContentPublication{}, ErrNotFound
			}
			return publication, nil
		}
		if !errors.Is(exactErr, gorm.ErrRecordNotFound) {
			return automationstore.AutomationContentPublication{}, exactErr
		}
	}
	publication, err := source.Store.GetContentPublication(ctx, request.WorkspaceID, content.ID, content.HeadVersionID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return automationstore.AutomationContentPublication{}, ErrNotFound
	}
	if err == nil && (publication.RepositoryOwner != strings.TrimSpace(request.RepositoryOwner) || publication.RepositoryName != strings.TrimSpace(request.RepositoryName) ||
		(requestedRef != "" && publication.Ref != requestedRef)) {
		return automationstore.AutomationContentPublication{}, ErrNotFound
	}
	return publication, err
}

func (source ContentStoreAutomationSource) resolveMutationBase(ctx context.Context, workspaceID, owner, repository string) (automationstore.AutomationContentPublication, contentstoreservice.Content, error) {
	content, err := source.requireNativeContent(ctx, workspaceID, owner, repository)
	if err != nil {
		return automationstore.AutomationContentPublication{}, contentstoreservice.Content{}, err
	}
	publication, err := source.Store.GetContentPublication(ctx, workspaceID, content.ID, content.HeadVersionID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		head, versionErr := source.Content.GetVersion(ctx, workspaceID, content.ID, content.HeadVersionID)
		if versionErr != nil {
			return automationstore.AutomationContentPublication{}, contentstoreservice.Content{}, versionErr
		}
		publication, err = source.Store.GetContentPublication(ctx, workspaceID, content.ID, head.ParentVersionID)
	}
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return automationstore.AutomationContentPublication{}, contentstoreservice.Content{}, ErrNotFound
	}
	if err != nil {
		return automationstore.AutomationContentPublication{}, contentstoreservice.Content{}, err
	}
	if publication.RepositoryOwner != strings.TrimSpace(owner) || publication.RepositoryName != strings.TrimSpace(repository) {
		return automationstore.AutomationContentPublication{}, contentstoreservice.Content{}, ErrNotFound
	}
	return publication, content, nil
}

func (source ContentStoreAutomationSource) requireNativeContent(ctx context.Context, workspaceID, owner, repository string) (contentstoreservice.Content, error) {
	bindingKey := strings.TrimSpace(owner) + "/" + strings.TrimSpace(repository)
	authority, found, err := source.Authority.GetAuthority(ctx, workspaceID, "automation", bindingKey)
	if err != nil {
		return contentstoreservice.Content{}, err
	}
	if !found || authority.State != contentstore.AuthorityStateNativeActive {
		return contentstoreservice.Content{}, fmt.Errorf("automation content binding is not native")
	}
	content, found, err := source.Content.FindContentByBinding(ctx, workspaceID, "automation", bindingKey)
	if err != nil {
		return contentstoreservice.Content{}, err
	}
	if !found || content.Generation != authority.Generation {
		return contentstoreservice.Content{}, fmt.Errorf("automation content binding projection is unavailable")
	}
	return content, nil
}

func collectAutomationContentWorkspace(root string, limits contentpayload.Limits, workspaceID, contentID string) ([]contentpayload.File, []automationstore.AutomationContentDefinition, error) {
	type staged struct {
		path string
		data []byte
	}
	var stagedFiles []staged
	var totalBytes int64
	err := filepath.WalkDir(root, func(current string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			return nil
		}
		if entry.Type()&os.ModeSymlink != 0 || !entry.Type().IsRegular() {
			return fmt.Errorf("automation content contains unsupported entry")
		}
		relative, err := filepath.Rel(root, current)
		if err != nil {
			return err
		}
		path := filepath.ToSlash(relative)
		data, err := os.ReadFile(current)
		if err != nil {
			return err
		}
		if len(stagedFiles)+1 > limits.MaxFiles || int64(len(data)) > limits.MaxFileBytes || totalBytes > limits.MaxTotalBytes-int64(len(data)) {
			return fmt.Errorf("automation content workspace exceeds publication bounds")
		}
		totalBytes += int64(len(data))
		stagedFiles = append(stagedFiles, staged{path: path, data: data})
		return nil
	})
	if err != nil {
		return nil, nil, err
	}
	sort.Slice(stagedFiles, func(i, j int) bool { return stagedFiles[i].path < stagedFiles[j].path })
	files := make([]contentpayload.File, 0, len(stagedFiles))
	definitions := make([]automationstore.AutomationContentDefinition, 0)
	for _, file := range stagedFiles {
		files = append(files, contentpayload.File{Path: file.path, Size: int64(len(file.data)), Content: bytes.NewReader(file.data)})
		if isAutomationSourcePath(file.path) {
			digest := sha256.Sum256(file.data)
			definitions = append(definitions, automationstore.AutomationContentDefinition{
				WorkspaceID: workspaceID, ContentID: contentID, DefinitionPath: file.path,
				Digest: hex.EncodeToString(digest[:]), SizeBytes: int64(len(file.data)),
			})
		}
	}
	if len(definitions) > automationstore.MaxAutomationDefinitionsPerVersion {
		return nil, nil, fmt.Errorf("automation definitions exceed %d", automationstore.MaxAutomationDefinitionsPerVersion)
	}
	return files, definitions, nil
}

func automationWorkspaceFileDigest(path string) (string, bool, error) {
	data, err := os.ReadFile(path)
	if os.IsNotExist(err) {
		return "", false, nil
	}
	if err != nil {
		return "", false, err
	}
	digest := sha256.Sum256(data)
	return hex.EncodeToString(digest[:]), true, nil
}

func automationContentMutationOperationKey(versionID, path string, deleting bool, value string) string {
	hash := sha256.New()
	for _, part := range []string{strings.TrimSpace(versionID), strings.TrimSpace(path), fmt.Sprint(deleting), value} {
		hash.Write([]byte(part))
		hash.Write([]byte{0})
	}
	return "automation-source:" + hex.EncodeToString(hash.Sum(nil))
}

var _ AutomationSourceResolver = ContentStoreAutomationSource{}
var _ AutomationSourceControlPlane = ContentStoreAutomationSource{}
var _ AutomationSourceWriteControlPlane = ContentStoreAutomationSource{}
