package memory

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"

	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/transport"

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

// AGSHTTPRepositoryStore manages the AGS-side repository content operations
// owned by the memory domain: template initialization, memory repository forks,
// and managed-repository deletion.
type AGSHTTPRepositoryStore struct {
	Handler             http.Handler
	BaseURL             string
	AuthHeader          string
	AuthorizationHeader string
	RepositoryGrantor   repos.ResourceRepositoryGrantor
}

const (
	memoryBackupListMaxPerPage      = 10
	memoryBackupAGSReleaseScanPage  = 100
	memoryBackupAGSReleaseMaxPages  = 20
	memoryBackupTimestamp           = "20060102-150405"
	memoryBackupGeneratedTagPrefix  = "memory-"
	memoryBackupBeforeRestorePrefix = "before-restore-"
	memoryHistoryDefaultPerPage     = 30
	memoryHistoryMaxPerPage         = 100
)

func (s AGSHTTPRepositoryStore) InitializeMemoryRepository(ctx context.Context, request MemoryRepositoryInitializeRequest) error {
	baseURL := strings.TrimSpace(s.BaseURL)
	owner, repo, defaultBranch := memoryRepositoryLogFields(request.Repository)
	locale := presets.NormalizeLocale(request.Locale)
	logAttrs := []any{
		"repo_owner", owner,
		"repo_name", repo,
		"repo_full_name", strings.TrimSpace(request.Repository.FullName),
		"default_branch", defaultBranch,
		"locale", locale,
	}
	if baseURL == "" {
		err := invalidArgument("AGS base URL is required to initialize memory repository")
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "validate_base_url", "error", err)...)
		return err
	}
	full := strings.TrimSpace(request.Repository.FullName)
	if full == "" {
		full = strings.TrimSpace(request.Repository.Owner) + "/" + strings.TrimSpace(request.Repository.Name)
	}
	owner, repo, ok := strings.Cut(full, "/")
	if !ok || strings.TrimSpace(owner) == "" || strings.TrimSpace(repo) == "" {
		err := invalidArgument("valid AGS repository is required")
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "validate_repository", "error", err)...)
		return err
	}
	remoteURL, err := agsGitRemoteURL(baseURL, owner, repo)
	if err != nil {
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "build_remote_url", "error", err)...)
		return err
	}
	slog.InfoContext(ctx, "memory repository initialization started", append(logAttrs, "stage", "start", "remote_host", remoteHostForLog(remoteURL))...)
	files, err := presets.MemoryTemplateFiles(locale)
	if err != nil {
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "load_template", "error", err)...)
		return err
	}
	workDir, err := os.MkdirTemp("", "meshx-memory-init-*")
	if err != nil {
		wrapped := fmt.Errorf("create memory init workspace: %w", err)
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "create_workspace", "error", wrapped)...)
		return wrapped
	}
	defer os.RemoveAll(workDir)

	repository, err := git.PlainInitWithOptions(workDir, &git.PlainInitOptions{Bare: false, InitOptions: git.InitOptions{DefaultBranch: plumbing.NewBranchReferenceName(firstNonEmptyString(request.Repository.DefaultBranch, "main"))}})
	if err != nil {
		wrapped := fmt.Errorf("initialize memory template repository: %w", err)
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "init_local_repo", "error", wrapped)...)
		return wrapped
	}
	worktree, err := repository.Worktree()
	if err != nil {
		wrapped := fmt.Errorf("open memory template worktree: %w", err)
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "open_worktree", "error", wrapped)...)
		return wrapped
	}
	for _, file := range files {
		path := filepath.Clean(filepath.FromSlash(file.Path))
		if path == "." || strings.HasPrefix(path, ".."+string(filepath.Separator)) || filepath.IsAbs(path) {
			err := fmt.Errorf("invalid memory template path %q", file.Path)
			slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "write_templates", "template_path", file.Path, "error", err)...)
			return err
		}
		target := filepath.Join(workDir, path)
		if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
			wrapped := fmt.Errorf("create memory template directory %q: %w", file.Path, err)
			slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "write_templates", "template_path", file.Path, "error", wrapped)...)
			return wrapped
		}
		if err := os.WriteFile(target, file.Content, 0o644); err != nil {
			wrapped := fmt.Errorf("write memory template file %q: %w", file.Path, err)
			slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "write_templates", "template_path", file.Path, "error", wrapped)...)
			return wrapped
		}
		if _, err := worktree.Add(filepath.ToSlash(path)); err != nil {
			wrapped := fmt.Errorf("stage memory template file %q: %w", file.Path, err)
			slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "stage_templates", "template_path", file.Path, "error", wrapped)...)
			return wrapped
		}
	}
	message := firstNonEmptyString(request.Message, presets.MemoryInitialCommitMessage(locale))
	_, err = worktree.Commit(message, &git.CommitOptions{
		Author: &object.Signature{
			Name:  firstNonEmptyString(request.AuthorName, "MeshX Memory"),
			Email: firstNonEmptyString(request.AuthorEmail, "meshx-memory@agentteam.local"),
			When:  time.Now().UTC(),
		},
	})
	if err != nil {
		wrapped := fmt.Errorf("commit memory template: %w", err)
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "commit_templates", "error", wrapped)...)
		return wrapped
	}
	_, err = repository.CreateRemote(&config.RemoteConfig{Name: "origin", URLs: []string{remoteURL}})
	if err != nil {
		wrapped := fmt.Errorf("configure memory template remote: %w", err)
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "configure_remote", "remote_host", remoteHostForLog(remoteURL), "error", wrapped)...)
		return wrapped
	}
	pushOptions := &git.PushOptions{RemoteName: "origin"}
	if auth := gitAuthorizationAuth(s.AuthHeader, s.AuthorizationHeader); auth != nil {
		pushOptions.Auth = auth
	}
	if err := repository.PushContext(ctx, pushOptions); err != nil {
		wrapped := fmt.Errorf("push memory template: %w", err)
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "push_templates", "remote_host", remoteHostForLog(remoteURL), "error", wrapped)...)
		return wrapped
	}
	if err := s.ensureMemoryRepositoryLabels(ctx, owner, repo, presets.MemoryLabels(locale)); err != nil {
		wrapped := fmt.Errorf("create memory repository labels: %w", err)
		slog.ErrorContext(ctx, "memory repository initialization failed", append(logAttrs, "stage", "create_labels", "error", wrapped)...)
		return wrapped
	}
	slog.InfoContext(ctx, "memory repository initialization completed", append(logAttrs, "stage", "completed", "template_files", len(files))...)
	return nil
}

func (s AGSHTTPRepositoryStore) ForkMemoryRepository(ctx context.Context, request MemoryRepositoryForkRequest) error {
	baseURL := strings.TrimSpace(s.BaseURL)
	if baseURL == "" {
		return invalidArgument("AGS base URL is required to fork memory repository")
	}
	if strings.TrimSpace(s.AuthorizationHeader) == "" {
		return unauthorized("Authorization is required to fork memory repositories")
	}
	sourceOwner, sourceRepo, err := memoryRepositoryOwnerName(request.Source)
	if err != nil {
		return fmt.Errorf("source memory repository: %w", err)
	}
	targetOwner, targetRepo, err := memoryRepositoryOwnerName(request.Target)
	if err != nil {
		return fmt.Errorf("target memory repository: %w", err)
	}
	sourceURL, err := agsGitRemoteURL(baseURL, sourceOwner, sourceRepo)
	if err != nil {
		return err
	}
	targetURL, err := agsGitRemoteURL(baseURL, targetOwner, targetRepo)
	if err != nil {
		return err
	}
	auth := gitAuthorizationAuth(s.AuthHeader, s.AuthorizationHeader)
	if auth == nil {
		return unauthorized("Authorization is required to fork memory repositories")
	}
	if err := runGoGitMirror(ctx, sourceURL, targetURL, auth); err != nil {
		return fmt.Errorf("fork memory repository: %w", err)
	}
	return nil
}

func (s AGSHTTPRepositoryStore) DeleteRepository(ctx context.Context, repository repos.RepositoryIdentity) error {
	owner, repo, err := memoryRepositoryOwnerName(repository)
	if err != nil {
		return err
	}
	status, body, err := s.doAGSAPI(ctx, http.MethodDelete, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo), nil)
	if err != nil {
		return fmt.Errorf("delete AGS repository: %w", err)
	}
	if status == http.StatusNotFound {
		return nil
	}
	if status < 200 || status >= 300 {
		return fmt.Errorf("delete AGS repository: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	return nil
}

func (s AGSHTTPRepositoryStore) ListMemoryRepositoryCommits(ctx context.Context, request MemoryRepositoryCommitListRequest) (json.RawMessage, error) {
	owner, repo, err := memoryRepositoryOwnerName(request.Repository)
	if err != nil {
		return nil, err
	}
	page, perPage := normalizeMemoryHistoryPagination(request.Page, request.PerPage)
	query := url.Values{}
	query.Set("page", fmt.Sprintf("%d", page))
	query.Set("per_page", fmt.Sprintf("%d", perPage))
	if ref := strings.TrimSpace(request.Ref); ref != "" {
		query.Set("sha", ref)
	}
	if path := strings.TrimSpace(request.Path); path != "" {
		query.Set("path", path)
	}
	if author := strings.TrimSpace(request.Author); author != "" {
		query.Set("author", author)
	}
	if since := strings.TrimSpace(request.Since); since != "" {
		query.Set("since", since)
	}
	if until := strings.TrimSpace(request.Until); until != "" {
		query.Set("until", until)
	}
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/commits?"+query.Encode(), nil)
	if err != nil {
		return nil, fmt.Errorf("list memory commits: %w", err)
	}
	if status == http.StatusNotFound {
		return nil, notFound("memory commits not found")
	}
	if status < 200 || status >= 300 {
		return nil, fmt.Errorf("list memory commits: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	if !json.Valid(body) {
		return nil, fmt.Errorf("decode memory commits: invalid JSON")
	}
	return append(json.RawMessage(nil), body...), nil
}

func (s AGSHTTPRepositoryStore) GetMemoryRepositoryCompare(ctx context.Context, request MemoryRepositoryCompareRequest) (json.RawMessage, error) {
	owner, repo, err := memoryRepositoryOwnerName(request.Repository)
	if err != nil {
		return nil, err
	}
	base := strings.TrimSpace(request.Base)
	head := strings.TrimSpace(request.Head)
	if base == "" || head == "" {
		return nil, invalidArgument("base and head are required")
	}
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/compare/"+url.PathEscape(base)+"..."+url.PathEscape(head), nil)
	if err != nil {
		return nil, fmt.Errorf("compare memory commits: %w", err)
	}
	if status == http.StatusNotFound {
		return nil, notFound("memory compare not found")
	}
	if status < 200 || status >= 300 {
		return nil, fmt.Errorf("compare memory commits: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	if !json.Valid(body) {
		return nil, fmt.Errorf("decode memory compare: invalid JSON")
	}
	return append(json.RawMessage(nil), body...), nil
}

func (s AGSHTTPRepositoryStore) GrantMemoryRepositoryAccess(ctx context.Context, grant MemoryRepositoryAccessGrantRequest) (MemoryRepositoryAccessGrantResult, error) {
	if s.RepositoryGrantor == nil {
		return MemoryRepositoryAccessGrantResult{}, invalidArgument("memory repository access grantor is not configured")
	}
	if strings.TrimSpace(grant.Actor.Provider) == "" || strings.TrimSpace(grant.Actor.Subject) == "" || strings.TrimSpace(grant.Actor.Login) == "" {
		return MemoryRepositoryAccessGrantResult{}, unauthorized("Missing authenticated identity")
	}
	owner, repo, err := memoryRepositoryOwnerName(grant.Repository)
	if err != nil {
		return MemoryRepositoryAccessGrantResult{}, err
	}
	permission := strings.TrimSpace(grant.Permission)
	if permission == "" {
		permission = "read"
	}
	result, err := s.RepositoryGrantor.GrantRepositoryCollaborator(ctx, repos.ResourceRepositoryCollaboratorGrant{
		Repository: repos.RepositoryIdentity{
			Owner:         owner,
			Name:          repo,
			FullName:      owner + "/" + repo,
			ID:            grant.Repository.ID,
			DefaultBranch: grant.Repository.DefaultBranch,
			Archived:      grant.Repository.Archived,
		},
		Actor: repos.ResourceRepositoryActor{
			Provider: grant.Actor.Provider,
			Subject:  grant.Actor.Subject,
			Login:    grant.Actor.Login,
			Name:     grant.Actor.Name,
			Email:    grant.Actor.Email,
			Groups:   grant.Actor.Groups,
		},
		Permission:                permission,
		AllowExistingDirectUpdate: grant.AllowExistingDirectUpdate,
	})
	return MemoryRepositoryAccessGrantResult{Materialized: result.Materialized}, err
}

func (s AGSHTTPRepositoryStore) ListMemoryRepositoryBackups(ctx context.Context, request MemoryRepositoryBackupListRequest) (AgentCloudMemoryBackupPage, error) {
	owner, repo, err := memoryRepositoryOwnerName(request.Repository)
	if err != nil {
		return AgentCloudMemoryBackupPage{}, err
	}
	page, perPage := normalizeMemoryBackupPagination(request.Page, request.PerPage)
	search := strings.ToLower(strings.TrimSpace(request.Query))

	if search == "" {
		releases, err := s.listAGSReleases(ctx, owner, repo, page, perPage)
		if err != nil {
			return AgentCloudMemoryBackupPage{}, err
		}
		next, err := s.listAGSReleases(ctx, owner, repo, page+1, perPage)
		if err != nil {
			return AgentCloudMemoryBackupPage{}, err
		}
		return AgentCloudMemoryBackupPage{
			Items:   memoryBackupsFromAGSReleases(releases),
			Page:    page,
			PerPage: perPage,
			HasMore: len(next) > 0,
		}, nil
	}

	targetCount := page*perPage + 1
	matches := make([]AgentCloudMemoryBackup, 0, targetCount)
	scanCapped := false
	for agsPage := 1; agsPage <= memoryBackupAGSReleaseMaxPages; agsPage++ {
		releases, err := s.listAGSReleases(ctx, owner, repo, agsPage, memoryBackupAGSReleaseScanPage)
		if err != nil {
			return AgentCloudMemoryBackupPage{}, err
		}
		if len(releases) == 0 {
			break
		}
		for _, release := range releases {
			backup := memoryBackupFromAGSRelease(release)
			if backup.TagName == "" {
				continue
			}
			if !strings.Contains(strings.ToLower(backup.Name+" "+backup.TagName), search) {
				continue
			}
			matches = append(matches, backup)
			if len(matches) >= targetCount {
				break
			}
		}
		if len(matches) >= targetCount || len(releases) < memoryBackupAGSReleaseScanPage {
			break
		}
		if agsPage == memoryBackupAGSReleaseMaxPages {
			scanCapped = true
		}
	}
	start := (page - 1) * perPage
	if start >= len(matches) {
		return AgentCloudMemoryBackupPage{Items: []AgentCloudMemoryBackup{}, Page: page, PerPage: perPage, HasMore: false}, nil
	}
	end := start + perPage
	if end > len(matches) {
		end = len(matches)
	}
	hasMore := end < len(matches)
	if !hasMore && scanCapped && end > start {
		hasMore = true
	}
	return AgentCloudMemoryBackupPage{Items: matches[start:end], Page: page, PerPage: perPage, HasMore: hasMore}, nil
}

func (s AGSHTTPRepositoryStore) CreateMemoryRepositoryBackup(ctx context.Context, request MemoryRepositoryBackupCreateRequest) (AgentCloudMemoryBackup, error) {
	owner, repo, err := memoryRepositoryOwnerName(request.Repository)
	if err != nil {
		return AgentCloudMemoryBackup{}, err
	}
	now := request.Now.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	branch := firstNonEmptyString(strings.TrimSpace(request.Branch), request.Repository.DefaultBranch, "main")
	tagName := generatedMemoryBackupTagName(now, memoryBackupGeneratedTagPrefix)
	releaseName := memoryBackupReleaseName(request.Name, tagName)
	release, err := s.createAGSRelease(ctx, owner, repo, agsReleaseCreateRequest{
		TagName:         tagName,
		TargetCommitish: branch,
		Name:            releaseName,
		Body:            "Cloud memory backup.",
	})
	if err != nil {
		if !errors.Is(err, ErrConflict) {
			return AgentCloudMemoryBackup{}, err
		}
		tagName = tagName + "-" + fmt.Sprintf("%d", now.UnixNano())
		release, err = s.createAGSRelease(ctx, owner, repo, agsReleaseCreateRequest{
			TagName:         tagName,
			TargetCommitish: branch,
			Name:            memoryBackupReleaseName(request.Name, tagName),
			Body:            "Cloud memory backup.",
		})
		if err != nil {
			return AgentCloudMemoryBackup{}, err
		}
	}
	backup := memoryBackupFromAGSRelease(release)
	if backup.CreatedAt.IsZero() {
		backup.CreatedAt = now
	}
	return backup, nil
}

func (s AGSHTTPRepositoryStore) RestoreMemoryRepositoryBackup(ctx context.Context, request MemoryRepositoryBackupRestoreRequest) (MemoryRepositoryBackupRestoreResult, error) {
	owner, repo, err := memoryRepositoryOwnerName(request.Repository)
	if err != nil {
		return MemoryRepositoryBackupRestoreResult{}, err
	}
	now := request.Now.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	branch := firstNonEmptyString(strings.TrimSpace(request.Branch), request.Repository.DefaultBranch, "main")
	tagName := strings.TrimSpace(request.TagName)
	if tagName == "" {
		return MemoryRepositoryBackupRestoreResult{}, invalidArgument("backup tag name is required")
	}
	if err := validateMemoryBackupTagName(tagName); err != nil {
		return MemoryRepositoryBackupRestoreResult{}, err
	}
	release, err := s.readAGSReleaseByTag(ctx, owner, repo, tagName)
	if err != nil {
		return MemoryRepositoryBackupRestoreResult{}, err
	}
	targetSHA, err := s.resolveBackupCommitSHA(ctx, owner, repo, tagName)
	if err != nil {
		return MemoryRepositoryBackupRestoreResult{}, err
	}
	head, err := s.readAGSRef(ctx, owner, repo, "heads/"+branch)
	if err != nil {
		return MemoryRepositoryBackupRestoreResult{}, err
	}
	beforeTag := generatedMemoryBackupTagName(now, memoryBackupBeforeRestorePrefix)
	beforeRelease, err := s.createAGSRelease(ctx, owner, repo, agsReleaseCreateRequest{
		TagName:         beforeTag,
		TargetCommitish: branch,
		Name:            beforeTag,
		Body:            "Cloud memory backup before restore.",
	})
	if err != nil {
		if errors.Is(err, ErrConflict) {
			beforeTag = beforeTag + "-" + fmt.Sprintf("%d", now.UnixNano())
			beforeRelease, err = s.createAGSRelease(ctx, owner, repo, agsReleaseCreateRequest{
				TagName:         beforeTag,
				TargetCommitish: branch,
				Name:            beforeTag,
				Body:            "Cloud memory backup before restore.",
			})
			if err != nil {
				return MemoryRepositoryBackupRestoreResult{}, err
			}
		} else {
			return MemoryRepositoryBackupRestoreResult{}, err
		}
	}
	if err := s.updateAGSHead(ctx, owner, repo, branch, targetSHA, true); err != nil {
		return MemoryRepositoryBackupRestoreResult{}, err
	}
	restored := memoryBackupFromAGSRelease(release)
	restored.CommitSHA = targetSHA
	beforeBackup := memoryBackupFromAGSRelease(beforeRelease)
	beforeBackup.CommitSHA = head.SHA
	if beforeBackup.CreatedAt.IsZero() {
		beforeBackup.CreatedAt = now
	}
	return MemoryRepositoryBackupRestoreResult{
		Restored:     restored,
		BeforeBackup: beforeBackup,
	}, nil
}

func (s AGSHTTPRepositoryStore) ensureMemoryRepositoryLabels(ctx context.Context, owner string, repo string, labels []presets.MemoryLabel) error {
	owner = strings.TrimSpace(owner)
	repo = strings.TrimSpace(repo)
	if owner == "" || repo == "" {
		return invalidArgument("valid AGS repository is required")
	}
	if len(labels) == 0 {
		return nil
	}
	query := url.Values{}
	query.Set("per_page", "100")
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/labels?"+query.Encode(), nil)
	if err != nil {
		return fmt.Errorf("list AGS repository labels: %w", err)
	}
	if status < 200 || status >= 300 {
		return fmt.Errorf("list AGS repository labels: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	var existing []struct {
		Name string `json:"name"`
	}
	if err := json.Unmarshal(body, &existing); err != nil {
		return fmt.Errorf("decode AGS repository labels: %w", err)
	}
	seen := map[string]struct{}{}
	for _, label := range existing {
		name := strings.ToLower(strings.TrimSpace(label.Name))
		if name != "" {
			seen[name] = struct{}{}
		}
	}
	for _, label := range labels {
		name := strings.TrimSpace(label.Name)
		if name == "" {
			continue
		}
		if _, ok := seen[strings.ToLower(name)]; ok {
			continue
		}
		payload := map[string]string{
			"name":        name,
			"color":       strings.TrimPrefix(strings.TrimSpace(label.Color), "#"),
			"description": strings.TrimSpace(label.Description),
		}
		status, body, err := s.doAGSAPI(ctx, http.MethodPost, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/labels", payload)
		if err != nil {
			return fmt.Errorf("create AGS repository label %q: %w", name, err)
		}
		if status == http.StatusUnprocessableEntity && strings.Contains(strings.ToLower(string(body)), "exists") {
			continue
		}
		if status < 200 || status >= 300 {
			return fmt.Errorf("create AGS repository label %q: status %d: %s", name, status, strings.TrimSpace(string(body)))
		}
		seen[strings.ToLower(name)] = struct{}{}
	}
	return nil
}

type agsAnnotatedTag struct {
	Tag     string `json:"tag"`
	SHA     string `json:"sha"`
	Message string `json:"message"`
	Tagger  struct {
		Name  string `json:"name"`
		Email string `json:"email"`
		Date  string `json:"date"`
	} `json:"tagger"`
	Object struct {
		SHA  string `json:"sha"`
		Type string `json:"type"`
	} `json:"object"`
}

type memoryBackupAnnotatedMetadata struct {
	CreatedAt  time.Time
	ObjectSHA  string
	ObjectType string
}

type agsRelease struct {
	TagName      string `json:"tag_name"`
	Name         string `json:"name"`
	CreatedAt    string `json:"created_at"`
	PublishedAt  string `json:"published_at"`
	TargetCommit string `json:"target_commitish"`
}

type agsReleaseCreateRequest struct {
	TagName         string
	TargetCommitish string
	Name            string
	Body            string
}

type agsGitRef struct {
	Ref    string `json:"ref"`
	Object struct {
		SHA  string `json:"sha"`
		Type string `json:"type"`
	} `json:"object"`
}

type resolvedAGSRef struct {
	SHA  string
	Type string
}

func (s AGSHTTPRepositoryStore) readAGSRef(ctx context.Context, owner, repo, ref string) (resolvedAGSRef, error) {
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/refs/"+pathEscapeSegments(ref), nil)
	if err != nil {
		return resolvedAGSRef{}, fmt.Errorf("read memory backup ref: %w", err)
	}
	if status == http.StatusNotFound {
		return resolvedAGSRef{}, notFound("memory backup ref not found")
	}
	if status < 200 || status >= 300 {
		return resolvedAGSRef{}, fmt.Errorf("read memory backup ref: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	var payload agsGitRef
	if err := json.Unmarshal(body, &payload); err != nil {
		return resolvedAGSRef{}, fmt.Errorf("decode memory backup ref: %w", err)
	}
	sha := strings.TrimSpace(payload.Object.SHA)
	if sha == "" {
		return resolvedAGSRef{}, fmt.Errorf("read memory backup ref: missing commit sha")
	}
	return resolvedAGSRef{SHA: sha, Type: strings.TrimSpace(payload.Object.Type)}, nil
}

func (s AGSHTTPRepositoryStore) readAGSAnnotatedTagMetadata(ctx context.Context, owner, repo, sha string) (memoryBackupAnnotatedMetadata, bool) {
	sha = strings.TrimSpace(sha)
	if sha == "" {
		return memoryBackupAnnotatedMetadata{}, false
	}
	tag, err := s.readAGSAnnotatedTag(ctx, owner, repo, sha)
	if err != nil {
		return memoryBackupAnnotatedMetadata{}, false
	}
	return memoryBackupAnnotatedMetadata{
		CreatedAt:  parseAGSTime(tag.Tagger.Date),
		ObjectSHA:  strings.TrimSpace(tag.Object.SHA),
		ObjectType: strings.TrimSpace(tag.Object.Type),
	}, true
}

func (s AGSHTTPRepositoryStore) readAGSAnnotatedTag(ctx context.Context, owner, repo, sha string) (agsAnnotatedTag, error) {
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/tags/"+url.PathEscape(strings.TrimSpace(sha)), nil)
	if err != nil {
		return agsAnnotatedTag{}, fmt.Errorf("read memory backup tag: %w", err)
	}
	if status == http.StatusNotFound {
		return agsAnnotatedTag{}, notFound("memory backup tag not found")
	}
	if status < 200 || status >= 300 {
		return agsAnnotatedTag{}, fmt.Errorf("read memory backup tag: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	var payload agsAnnotatedTag
	if err := json.Unmarshal(body, &payload); err != nil {
		return agsAnnotatedTag{}, fmt.Errorf("decode memory backup tag: %w", err)
	}
	return payload, nil
}

func (s AGSHTTPRepositoryStore) listAGSReleases(ctx context.Context, owner, repo string, page, perPage int) ([]agsRelease, error) {
	query := url.Values{}
	query.Set("page", fmt.Sprintf("%d", page))
	query.Set("per_page", fmt.Sprintf("%d", perPage))
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/releases?"+query.Encode(), nil)
	if err != nil {
		return nil, fmt.Errorf("list memory backups: %w", err)
	}
	if status < 200 || status >= 300 {
		return nil, fmt.Errorf("list memory backups: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	var payload []agsRelease
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("decode memory backups: %w", err)
	}
	return payload, nil
}

func (s AGSHTTPRepositoryStore) createAGSRelease(ctx context.Context, owner, repo string, request agsReleaseCreateRequest) (agsRelease, error) {
	payload := map[string]any{
		"tag_name":         strings.TrimSpace(request.TagName),
		"target_commitish": strings.TrimSpace(request.TargetCommitish),
		"name":             strings.TrimSpace(request.Name),
		"body":             strings.TrimSpace(request.Body),
		"draft":            true,
	}
	status, body, err := s.doAGSAPI(ctx, http.MethodPost, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/releases", payload)
	if err != nil {
		return agsRelease{}, fmt.Errorf("create memory backup: %w", err)
	}
	if status == http.StatusUnprocessableEntity {
		lowerBody := strings.ToLower(string(body))
		if strings.Contains(lowerBody, "exist") || strings.Contains(lowerBody, "already") || strings.Contains(lowerBody, "conflict") {
			return agsRelease{}, conflict("memory backup already exists")
		}
	}
	if status < 200 || status >= 300 {
		return agsRelease{}, fmt.Errorf("create memory backup: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	var release agsRelease
	if err := json.Unmarshal(body, &release); err != nil {
		return agsRelease{}, fmt.Errorf("decode memory backup: %w", err)
	}
	return release, nil
}

func (s AGSHTTPRepositoryStore) readAGSReleaseByTag(ctx context.Context, owner, repo, tagName string) (agsRelease, error) {
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/releases/tags/"+url.PathEscape(strings.TrimSpace(tagName)), nil)
	if err != nil {
		return agsRelease{}, fmt.Errorf("read memory backup: %w", err)
	}
	if status == http.StatusNotFound {
		return agsRelease{}, notFound("memory backup not found")
	}
	if status < 200 || status >= 300 {
		return agsRelease{}, fmt.Errorf("read memory backup: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	var release agsRelease
	if err := json.Unmarshal(body, &release); err != nil {
		return agsRelease{}, fmt.Errorf("decode memory backup: %w", err)
	}
	return release, nil
}

func (s AGSHTTPRepositoryStore) updateAGSHead(ctx context.Context, owner, repo, branch, sha string, force bool) error {
	status, body, err := s.doAGSAPI(ctx, http.MethodPatch, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/git/refs/heads/"+pathEscapeSegments(branch), map[string]any{"sha": sha, "force": force})
	if err != nil {
		return fmt.Errorf("restore memory backup: %w", err)
	}
	if status == http.StatusNotFound {
		return notFound("memory branch not found")
	}
	if status < 200 || status >= 300 {
		return fmt.Errorf("restore memory backup: status %d: %s", status, strings.TrimSpace(string(body)))
	}
	return nil
}

func (s AGSHTTPRepositoryStore) resolveBackupCommitSHA(ctx context.Context, owner, repo, tagName string) (string, error) {
	tagName = strings.TrimSpace(tagName)
	ref, err := s.readAGSRef(ctx, owner, repo, "tags/"+tagName)
	if err != nil {
		return "", err
	}
	if annotated, ok := s.readAGSAnnotatedTagMetadata(ctx, owner, repo, ref.SHA); ok {
		if strings.EqualFold(annotated.ObjectType, "commit") && strings.TrimSpace(annotated.ObjectSHA) != "" {
			return strings.TrimSpace(annotated.ObjectSHA), nil
		}
	}
	return ref.SHA, nil
}

func normalizeBackupTagName(value string) string {
	value = strings.TrimSpace(value)
	value = strings.TrimPrefix(value, "refs/tags/")
	return strings.TrimSpace(value)
}

func memoryBackupsFromAGSReleases(releases []agsRelease) []AgentCloudMemoryBackup {
	items := make([]AgentCloudMemoryBackup, 0, len(releases))
	for _, release := range releases {
		backup := memoryBackupFromAGSRelease(release)
		if backup.TagName == "" {
			continue
		}
		items = append(items, backup)
	}
	return items
}

func memoryBackupFromAGSRelease(release agsRelease) AgentCloudMemoryBackup {
	tagName := normalizeBackupTagName(release.TagName)
	name := strings.TrimSpace(release.Name)
	if name == "" {
		name = tagName
	}
	return AgentCloudMemoryBackup{
		TagName:   tagName,
		Name:      name,
		CreatedAt: parseAGSTime(release.CreatedAt, release.PublishedAt),
	}
}

func generatedMemoryBackupTagName(now time.Time, prefix string) string {
	now = now.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	return prefix + now.Format(memoryBackupTimestamp)
}

func memoryBackupReleaseName(name, tagName string) string {
	name = strings.TrimSpace(name)
	if name != "" {
		return name
	}
	return strings.TrimSpace(tagName)
}

func validateMemoryBackupTagName(tag string) error {
	tag = strings.TrimSpace(tag)
	if tag == "" ||
		strings.HasPrefix(tag, ".") ||
		strings.Contains(tag, "/") ||
		strings.Contains(tag, "..") ||
		strings.HasSuffix(tag, ".lock") ||
		strings.HasSuffix(tag, ".") {
		return invalidArgument("backup name must be a valid git tag name")
	}
	for _, r := range tag {
		switch r {
		case '~', '^', ':', '?', '*', '[', '\\':
			return invalidArgument("backup name must be a valid git tag name")
		}
		if r <= 32 || r == 127 {
			return invalidArgument("backup name must be a valid git tag name")
		}
	}
	return nil
}

func normalizeMemoryBackupPagination(page, perPage int) (int, int) {
	if page <= 0 {
		page = 1
	}
	if perPage <= 0 {
		perPage = memoryBackupListMaxPerPage
	}
	if perPage > memoryBackupListMaxPerPage {
		perPage = memoryBackupListMaxPerPage
	}
	return page, perPage
}

func normalizeMemoryHistoryPagination(page, perPage int) (int, int) {
	if page <= 0 {
		page = 1
	}
	if perPage <= 0 {
		perPage = memoryHistoryDefaultPerPage
	}
	if perPage > memoryHistoryMaxPerPage {
		perPage = memoryHistoryMaxPerPage
	}
	return page, perPage
}

func parseAGSTime(values ...string) time.Time {
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		for _, layout := range []string{
			time.RFC3339Nano,
			time.RFC3339,
			"2006-01-02 15:04:05 -0700",
			"2006-01-02 15:04:05",
		} {
			parsed, err := time.Parse(layout, value)
			if err == nil {
				return parsed.UTC()
			}
		}
	}
	return time.Time{}
}

func pathEscapeSegments(value string) string {
	parts := strings.Split(value, "/")
	for i := range parts {
		parts[i] = url.PathEscape(parts[i])
	}
	return strings.Join(parts, "/")
}

func (s AGSHTTPRepositoryStore) doAGSAPI(ctx context.Context, method string, path string, payload any) (int, []byte, error) {
	if strings.TrimSpace(method) == "" || strings.TrimSpace(path) == "" {
		return 0, nil, invalidArgument("AGS method and path are required")
	}
	return s.doAGSAPIWithLimit(ctx, method, path, payload, 1<<20)
}

func (s AGSHTTPRepositoryStore) doAGSAPIWithLimit(ctx context.Context, method string, path string, requestPayload any, maxResponseBytes int64) (int, []byte, error) {
	if maxResponseBytes <= 0 {
		return 0, nil, fmt.Errorf("AGS response limit is required")
	}
	var body io.Reader
	if requestPayload != nil {
		encoded, err := json.Marshal(requestPayload)
		if err != nil {
			return 0, nil, err
		}
		body = bytes.NewReader(encoded)
	}
	if s.Handler != nil {
		request, err := http.NewRequestWithContext(agsbridge.ContextWithSiteAdmin(ctx), method, path, body)
		if err != nil {
			return 0, nil, err
		}
		if requestPayload != nil {
			request.Header.Set("Content-Type", "application/json")
		}
		agsbridge.SetAuthHeader(request.Header, s.AuthHeader, s.AuthorizationHeader)
		response := newBoundedMemoryAGSResponse(maxResponseBytes)
		s.Handler.ServeHTTP(response, request)
		if response.overflow {
			return 0, nil, fmt.Errorf("AGS response exceeds %d bytes", maxResponseBytes)
		}
		return response.statusCode(), append([]byte(nil), response.body.Bytes()...), nil
	}
	baseURL := strings.TrimRight(strings.TrimSpace(s.BaseURL), "/")
	if baseURL == "" {
		return 0, nil, invalidArgument("AGS base URL is required")
	}
	parsed, err := url.Parse(baseURL)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return 0, nil, invalidArgument("valid AGS base URL is required")
	}
	apiPath, err := url.Parse(path)
	if err != nil {
		return 0, nil, err
	}
	parsed.Path = strings.TrimRight(parsed.Path, "/") + apiPath.Path
	parsed.RawQuery = apiPath.RawQuery
	parsed.Fragment = ""
	request, err := http.NewRequestWithContext(ctx, method, parsed.String(), body)
	if err != nil {
		return 0, nil, err
	}
	if requestPayload != nil {
		request.Header.Set("Content-Type", "application/json")
	}
	agsbridge.SetAuthHeader(request.Header, s.AuthHeader, s.AuthorizationHeader)
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		return 0, nil, err
	}
	defer response.Body.Close()
	responseBody, err := io.ReadAll(io.LimitReader(response.Body, maxResponseBytes+1))
	if err != nil {
		return 0, nil, err
	}
	if int64(len(responseBody)) > maxResponseBytes {
		return 0, nil, fmt.Errorf("AGS response exceeds %d bytes", maxResponseBytes)
	}
	return response.StatusCode, responseBody, nil
}

type boundedMemoryAGSResponse struct {
	header   http.Header
	body     bytes.Buffer
	status   int
	limit    int64
	overflow bool
}

func newBoundedMemoryAGSResponse(limit int64) *boundedMemoryAGSResponse {
	return &boundedMemoryAGSResponse{header: make(http.Header), limit: limit}
}

func (response *boundedMemoryAGSResponse) Header() http.Header { return response.header }

func (response *boundedMemoryAGSResponse) WriteHeader(status int) {
	if response.status == 0 {
		response.status = status
	}
}

func (response *boundedMemoryAGSResponse) Write(value []byte) (int, error) {
	if response.status == 0 {
		response.status = http.StatusOK
	}
	remaining := response.limit + 1 - int64(response.body.Len())
	if remaining <= 0 {
		response.overflow = true
		return len(value), nil
	}
	write := int64(len(value))
	if write > remaining {
		write = remaining
		response.overflow = true
	}
	_, _ = response.body.Write(value[:write])
	return len(value), nil
}

func (response *boundedMemoryAGSResponse) statusCode() int {
	if response.status == 0 {
		return http.StatusOK
	}
	return response.status
}

func memoryRepositoryLogFields(repository repos.RepositoryIdentity) (owner string, repo string, defaultBranch string) {
	full := strings.TrimSpace(repository.FullName)
	if full != "" {
		if parsedOwner, parsedRepo, ok := strings.Cut(full, "/"); ok {
			owner = strings.TrimSpace(parsedOwner)
			repo = strings.TrimSpace(parsedRepo)
		}
	}
	if owner == "" {
		owner = strings.TrimSpace(repository.Owner)
	}
	if repo == "" {
		repo = strings.TrimSpace(repository.Name)
	}
	defaultBranch = firstNonEmptyString(repository.DefaultBranch, "main")
	return owner, repo, defaultBranch
}

func memoryRepositoryOwnerName(repository repos.RepositoryIdentity) (string, string, error) {
	fullName := strings.TrimSpace(repository.FullName)
	if fullName != "" {
		owner, repo, ok := strings.Cut(fullName, "/")
		if ok && strings.TrimSpace(owner) != "" && strings.TrimSpace(repo) != "" {
			return strings.TrimSpace(owner), strings.TrimSpace(repo), nil
		}
	}
	owner := strings.TrimSpace(repository.Owner)
	repo := strings.TrimSpace(repository.Name)
	if owner == "" || repo == "" {
		return "", "", invalidArgument("valid AGS repository is required")
	}
	return owner, repo, nil
}

func remoteHostForLog(raw string) string {
	parsed, err := url.Parse(raw)
	if err != nil {
		return ""
	}
	return parsed.Host
}

func agsGitRemoteURL(baseURL, owner, repo string) (string, error) {
	parsed, err := url.Parse(strings.TrimRight(baseURL, "/"))
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return "", invalidArgument("valid AGS base URL is required to initialize memory repository")
	}
	parsed.Path = strings.TrimRight(parsed.Path, "/") + "/" + url.PathEscape(owner) + "/" + url.PathEscape(repo) + ".git"
	parsed.RawQuery = ""
	parsed.Fragment = ""
	return parsed.String(), nil
}

func gitAuthorizationAuth(configuredHeader string, value string) *memoryBearerAuth {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil
	}
	return &memoryBearerAuth{headerName: agsbridge.AuthHeader(configuredHeader), value: value}
}

type memoryBearerAuth struct {
	headerName string
	value      string
}

func (a *memoryBearerAuth) SetAuth(r *http.Request) {
	if a != nil && strings.TrimSpace(a.value) != "" {
		r.Header.Set(agsbridge.AuthHeader(a.headerName), a.value)
	}
}

func (a *memoryBearerAuth) Name() string { return "http-agentteam-auth-header" }

func (a *memoryBearerAuth) String() string { return a.Name() + " - *******" }

func runGoGitMirror(ctx context.Context, sourceURL string, targetURL string, auth transport.AuthMethod) error {
	workDir, err := os.MkdirTemp("", "meshx-memory-git-mirror-*")
	if err != nil {
		return fmt.Errorf("prepare git mirror workspace: %w", err)
	}
	defer os.RemoveAll(workDir)
	repoDir := filepath.Join(workDir, "repo.git")
	repo, err := git.PlainCloneContext(ctx, repoDir, true, &git.CloneOptions{URL: sourceURL, Auth: auth, Mirror: true, Progress: io.Discard})
	if err != nil {
		return fmt.Errorf("clone git mirror source: %w", err)
	}
	remote := git.NewRemote(repo.Storer, &config.RemoteConfig{Name: "target", URLs: []string{targetURL}})
	if err := remote.PushContext(ctx, &git.PushOptions{RemoteName: "target", Auth: auth, RefSpecs: []config.RefSpec{"+refs/*:refs/*"}, Progress: io.Discard}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
		return fmt.Errorf("push git mirror target: %w", err)
	}
	if err := pruneGoGitMirrorTargetRefs(ctx, repo, remote, auth); err != nil {
		return fmt.Errorf("prune git mirror target: %w", err)
	}
	return nil
}

func pruneGoGitMirrorTargetRefs(ctx context.Context, repo *git.Repository, remote *git.Remote, auth transport.AuthMethod) error {
	sourceRefs, err := goGitLocalRefs(repo)
	if err != nil {
		return err
	}
	targetRefs, err := remote.ListContext(ctx, &git.ListOptions{Auth: auth})
	if err != nil {
		return err
	}
	deleteSpecs := make([]config.RefSpec, 0)
	for _, ref := range targetRefs {
		if ref == nil || !strings.HasPrefix(ref.Name().String(), "refs/") {
			continue
		}
		if _, ok := sourceRefs[ref.Name()]; !ok {
			deleteSpecs = append(deleteSpecs, config.RefSpec(":"+ref.Name().String()))
		}
	}
	if len(deleteSpecs) == 0 {
		return nil
	}
	if err := remote.PushContext(ctx, &git.PushOptions{RemoteName: "target", Auth: auth, RefSpecs: deleteSpecs, Progress: io.Discard}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
		return err
	}
	return nil
}

func goGitLocalRefs(repo *git.Repository) (map[plumbing.ReferenceName]struct{}, error) {
	refs := map[plumbing.ReferenceName]struct{}{}
	if repo == nil {
		return refs, nil
	}
	iter, err := repo.References()
	if err != nil {
		return nil, err
	}
	defer iter.Close()
	err = iter.ForEach(func(ref *plumbing.Reference) error {
		if ref != nil && strings.HasPrefix(ref.Name().String(), "refs/") {
			refs[ref.Name()] = struct{}{}
		}
		return nil
	})
	return refs, err
}
