package memory

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strings"

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

const (
	memoryMigrationMaxTreeResponse = int64(16 << 20)
	memoryMigrationMaxBlobResponse = int64(24 << 20)
)

// ResolveMemoryMigrationHead resolves the exact bound branch selected by an
// explicit Memory migration. It performs no mutation or access repair.
func (s AGSHTTPRepositoryStore) ResolveMemoryMigrationHead(ctx context.Context, repository repos.RepositoryIdentity, branch string) (string, error) {
	owner, repo, err := memoryRepositoryOwnerName(repository)
	if err != nil {
		return "", err
	}
	ref, err := s.readAGSRef(ctx, owner, repo, "heads/"+strings.TrimSpace(branch))
	if err != nil {
		return "", err
	}
	return normalizeMemoryMigrationSHA(ref.SHA)
}

// ListMemoryMigrationBackups returns at most the caller's one-row-lookahead
// bound. Each label is resolved to an exact commit before preflight proceeds.
func (s AGSHTTPRepositoryStore) ListMemoryMigrationBackups(ctx context.Context, repository repos.RepositoryIdentity, limit int) ([]MemoryMigrationReference, error) {
	if limit <= 0 || limit > memoryMigrationMaxBackupReferences+1 {
		return nil, fmt.Errorf("memory migration backup limit is invalid")
	}
	owner, repo, err := memoryRepositoryOwnerName(repository)
	if err != nil {
		return nil, err
	}
	releases, err := s.listAGSReleases(ctx, owner, repo, 1, limit)
	if err != nil {
		return nil, err
	}
	result := make([]MemoryMigrationReference, 0, len(releases))
	for _, release := range releases {
		label := strings.TrimSpace(release.TagName)
		sha, err := s.resolveBackupCommitSHA(ctx, owner, repo, label)
		if err != nil {
			return nil, fmt.Errorf("resolve memory migration backup %q: %w", label, err)
		}
		result = append(result, MemoryMigrationReference{Kind: "backup", Label: label, CommitSHA: sha})
	}
	return result, nil
}

// ReadMemoryMigrationCommit reads one exact Git commit and its first parent.
// Merge side parents are deliberately not retained as native history.
func (s AGSHTTPRepositoryStore) ReadMemoryMigrationCommit(ctx context.Context, repository repos.RepositoryIdentity, commitSHA string) (MemoryMigrationCommit, error) {
	commit, err := s.readMemoryMigrationGitCommit(ctx, repository, commitSHA)
	if err != nil {
		return MemoryMigrationCommit{}, err
	}
	firstParent := ""
	if len(commit.Parents) > 0 {
		firstParent = strings.TrimSpace(commit.Parents[0].SHA)
	}
	return MemoryMigrationCommit{
		CommitSHA: commit.SHA, FirstParentSHA: firstParent,
		AuthorName: commit.Author.Name, AuthorEmail: commit.Author.Email,
		AuthoredAt: commit.Author.Date, Message: commit.Message,
	}, nil
}

// ReadMemoryMigrationSnapshot exports one exact full regular-file tree. Tree
// and blob responses are independently bounded and every blob object ID is
// verified before its bytes can enter the migration plan.
func (s AGSHTTPRepositoryStore) ReadMemoryMigrationSnapshot(ctx context.Context, repository repos.RepositoryIdentity, commitSHA string, limits contentpayload.Limits) ([]MemoryMigrationFile, error) {
	limits, err := contentpayload.NormalizeLimits(limits)
	if err != nil || limits.MaxFiles > memoryContentMaxFiles || limits.MaxFileBytes > memoryContentMaxFileBytes || limits.MaxTotalBytes > memoryContentMaxTotalBytes || limits.MaxManifestBytes > memoryContentMaxManifestBytes {
		return nil, fmt.Errorf("memory migration snapshot limits are invalid")
	}
	owner, repo, err := memoryRepositoryOwnerName(repository)
	if err != nil {
		return nil, err
	}
	commit, err := s.readMemoryMigrationGitCommit(ctx, repository, commitSHA)
	if err != nil {
		return nil, err
	}
	base := "/api/v3/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(repo)
	status, body, err := s.doAGSAPIWithLimit(ctx, http.MethodGet, base+"/git/trees/"+url.PathEscape(commit.Tree.SHA)+"?recursive=1", nil, memoryMigrationMaxTreeResponse)
	if err != nil {
		return nil, fmt.Errorf("read memory migration tree: %w", err)
	}
	if status < 200 || status >= 300 {
		return nil, fmt.Errorf("read memory migration tree: status %d", status)
	}
	var tree struct {
		SHA       string `json:"sha"`
		Truncated bool   `json:"truncated"`
		Tree      []struct {
			Path string `json:"path"`
			Mode string `json:"mode"`
			Type string `json:"type"`
			SHA  string `json:"sha"`
			Size int64  `json:"size"`
		} `json:"tree"`
	}
	if err := json.Unmarshal(body, &tree); err != nil || strings.TrimSpace(tree.SHA) != commit.Tree.SHA || tree.Truncated {
		return nil, fmt.Errorf("read memory migration tree: invalid or truncated response")
	}
	entries := make([]struct {
		path       string
		sha        string
		size       int64
		executable bool
	}, 0, len(tree.Tree))
	var total int64
	seenPaths := make(map[string]struct{}, len(tree.Tree))
	for _, entry := range tree.Tree {
		if entry.Type == "tree" && entry.Mode == "040000" {
			continue
		}
		if entry.Type != "blob" || (entry.Mode != "100644" && entry.Mode != "100755") {
			return nil, fmt.Errorf("memory migration tree contains a symlink or special entry")
		}
		if !validMemoryMigrationPath(entry.Path) {
			return nil, fmt.Errorf("memory migration tree contains an invalid path")
		}
		if _, duplicate := seenPaths[entry.Path]; duplicate {
			return nil, fmt.Errorf("memory migration tree contains a normalization collision")
		}
		seenPaths[entry.Path] = struct{}{}
		if len(entries) >= limits.MaxFiles || entry.Size < 0 || entry.Size > limits.MaxFileBytes || total > limits.MaxTotalBytes-entry.Size {
			return nil, fmt.Errorf("memory migration tree exceeds file or byte bounds")
		}
		sha, err := normalizeMemoryMigrationSHA(entry.SHA)
		if err != nil {
			return nil, fmt.Errorf("memory migration blob identity is invalid")
		}
		entries = append(entries, struct {
			path       string
			sha        string
			size       int64
			executable bool
		}{path: entry.Path, sha: sha, size: entry.Size, executable: entry.Mode == "100755"})
		total += entry.Size
	}
	sort.Slice(entries, func(i, j int) bool { return entries[i].path < entries[j].path })
	files := make([]MemoryMigrationFile, 0, len(entries))
	for _, entry := range entries {
		status, body, err := s.doAGSAPIWithLimit(ctx, http.MethodGet, base+"/git/blobs/"+url.PathEscape(entry.sha), nil, memoryMigrationMaxBlobResponse)
		if err != nil {
			return nil, fmt.Errorf("read memory migration blob %q: %w", entry.path, err)
		}
		if status < 200 || status >= 300 {
			return nil, fmt.Errorf("read memory migration blob %q: status %d", entry.path, status)
		}
		var blob struct {
			SHA      string `json:"sha"`
			Encoding string `json:"encoding"`
			Content  string `json:"content"`
		}
		if err := json.Unmarshal(body, &blob); err != nil || !strings.EqualFold(strings.TrimSpace(blob.Encoding), "base64") || strings.TrimSpace(blob.SHA) != entry.sha {
			return nil, fmt.Errorf("read memory migration blob %q: invalid response", entry.path)
		}
		data, err := base64.StdEncoding.Strict().DecodeString(strings.Map(func(r rune) rune {
			if r == '\n' || r == '\r' {
				return -1
			}
			return r
		}, blob.Content))
		if err != nil || int64(len(data)) != entry.size || plumbing.ComputeHash(plumbing.BlobObject, data).String() != entry.sha {
			return nil, fmt.Errorf("read memory migration blob %q: content mismatch", entry.path)
		}
		files = append(files, MemoryMigrationFile{Path: entry.path, Data: data, Executable: entry.executable})
	}
	return files, nil
}

type memoryMigrationGitCommit struct {
	SHA     string `json:"sha"`
	Message string `json:"message"`
	Tree    struct {
		SHA string `json:"sha"`
	} `json:"tree"`
	Parents []struct {
		SHA string `json:"sha"`
	} `json:"parents"`
	Author struct {
		Name  string `json:"name"`
		Email string `json:"email"`
		Date  string `json:"date"`
	} `json:"author"`
}

func (s AGSHTTPRepositoryStore) readMemoryMigrationGitCommit(ctx context.Context, repository repos.RepositoryIdentity, commitSHA string) (memoryMigrationGitCommit, error) {
	owner, repo, err := memoryRepositoryOwnerName(repository)
	if err != nil {
		return memoryMigrationGitCommit{}, err
	}
	commitSHA, err = normalizeMemoryMigrationSHA(commitSHA)
	if err != nil {
		return memoryMigrationGitCommit{}, err
	}
	path := "/api/v3/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(repo) + "/git/commits/" + url.PathEscape(commitSHA)
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, path, nil)
	if err != nil {
		return memoryMigrationGitCommit{}, fmt.Errorf("read memory migration commit: %w", err)
	}
	if status < 200 || status >= 300 {
		return memoryMigrationGitCommit{}, fmt.Errorf("read memory migration commit: status %d", status)
	}
	var commit memoryMigrationGitCommit
	if err := json.Unmarshal(body, &commit); err != nil {
		return memoryMigrationGitCommit{}, fmt.Errorf("read memory migration commit: decode response: %w", err)
	}
	if strings.TrimSpace(commit.SHA) != commitSHA {
		return memoryMigrationGitCommit{}, fmt.Errorf("read memory migration commit: identity mismatch")
	}
	commit.Tree.SHA, err = normalizeMemoryMigrationSHA(commit.Tree.SHA)
	if err != nil {
		return memoryMigrationGitCommit{}, fmt.Errorf("read memory migration commit: invalid tree identity")
	}
	for _, parent := range commit.Parents {
		if _, err := normalizeMemoryMigrationSHA(parent.SHA); err != nil {
			return memoryMigrationGitCommit{}, fmt.Errorf("read memory migration commit: invalid parent identity")
		}
	}
	return commit, nil
}
