package kb

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

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

const maxUADashboardAGSResponseOverheadBytes = 4 << 10

type UADashboardRepository interface {
	PublishUADashboard(context.Context, UADashboardRepositoryPublishRequest) (UADashboardRepositoryPublishResult, error)
	ReadUADashboardFile(context.Context, UADashboardRepositoryFileRequest) (UADashboardRepositoryFile, error)
}

type UADashboardRepositoryPublishRequest struct {
	Repository            repos.ResourceRepository
	WorkspaceID           string
	SourceID              string
	SourceRevisionID      string
	PreviousPublicationID string
	Actor                 string
	Files                 map[string][]byte
	Limits                UADashboardImportLimits
}

type UADashboardRepositoryPublishResult struct {
	CommitSHA   string
	FileDigests map[string]string
}

type UADashboardRepositoryFileRequest struct {
	Repository         repos.ResourceRepository
	WorkspaceID        string
	SourceID           string
	Subject            string
	DashboardCommitSHA string
	Path               string
	ExpectedBlobSHA    string
	MaxBytes           int64
}

type UADashboardRepositoryFile struct {
	Content []byte
	BlobSHA string
}

type uaRepositoryLocation struct {
	owner  string
	repo   string
	branch string
}

func (s AGSHTTPRepositoryStore) PublishUADashboard(ctx context.Context, request UADashboardRepositoryPublishRequest) (UADashboardRepositoryPublishResult, error) {
	location, err := uaDashboardRepositoryLocation(request.Repository)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	sourceID := strings.TrimSpace(request.SourceID)
	if sourceID == "" || strings.ContainsAny(sourceID, "/\\\x00") {
		return UADashboardRepositoryPublishResult{}, invalidArgument("dashboard source id is invalid")
	}
	paths, err := validateUADashboardPublishFiles(request.Files)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	headSHA, treeSHA, err := s.uaDashboardResolveHead(ctx, location)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	newTreeSHA, err := s.uaDashboardCreateTree(ctx, location, treeSHA, paths, request.Files)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	if err := s.uaDashboardVerifyTree(ctx, location, newTreeSHA, paths, request.Files); err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	commitSHA, err := s.uaDashboardCreateCommit(ctx, location, newTreeSHA, headSHA)
	if err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	if err := s.uaDashboardVerifyCommit(ctx, location, commitSHA, newTreeSHA, headSHA); err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	if err := s.uaDashboardRetainCommit(ctx, location, sourceID, commitSHA); err != nil {
		return UADashboardRepositoryPublishResult{}, err
	}
	digests := make(map[string]string, len(paths))
	for _, path := range paths {
		digests[path] = plumbing.ComputeHash(plumbing.BlobObject, request.Files[path]).String()
	}
	return UADashboardRepositoryPublishResult{CommitSHA: commitSHA, FileDigests: digests}, nil
}

func (s AGSHTTPRepositoryStore) ReadUADashboardFile(ctx context.Context, request UADashboardRepositoryFileRequest) (UADashboardRepositoryFile, error) {
	location, err := uaDashboardRepositoryLocation(request.Repository)
	if err != nil {
		return UADashboardRepositoryFile{}, err
	}
	path := strings.TrimSpace(request.Path)
	if _, ok := uaDashboardAllowedFiles[path]; !ok {
		return UADashboardRepositoryFile{}, invalidArgument("dashboard file path is invalid")
	}
	commitSHA := strings.TrimSpace(request.DashboardCommitSHA)
	expectedBlobSHA := strings.TrimSpace(request.ExpectedBlobSHA)
	if commitSHA == "" || expectedBlobSHA == "" {
		return UADashboardRepositoryFile{}, invalidArgument("dashboard commit and blob sha are required")
	}
	maxBytes := request.MaxBytes
	if maxBytes <= 0 {
		maxBytes = DefaultUADashboardImportLimits().FileBytes
	}
	values := url.Values{}
	values.Set("ref", commitSHA)
	responseLimit := ((maxBytes + 2) / 3 * 4) + maxUADashboardAGSResponseOverheadBytes
	status, body, err := s.doAGSAPILimited(ctx, http.MethodGet, agsRepositoryContentsPath(location.owner, location.repo, ".ua/"+path)+"?"+values.Encode(), nil, responseLimit)
	if err != nil {
		return UADashboardRepositoryFile{}, fmt.Errorf("read dashboard file from AGS: %w", err)
	}
	if status < 200 || status >= 300 {
		return UADashboardRepositoryFile{}, fmt.Errorf("read dashboard file from AGS: status %d", status)
	}
	var payload struct {
		Type     string `json:"type"`
		Path     string `json:"path"`
		SHA      string `json:"sha"`
		Size     int64  `json:"size"`
		Encoding string `json:"encoding"`
		Content  string `json:"content"`
	}
	if err := decodeUADashboardAGSResponse(body, &payload); err != nil {
		return UADashboardRepositoryFile{}, fmt.Errorf("read dashboard file from AGS: invalid response")
	}
	if payload.Type != "file" || payload.Path != ".ua/"+path || strings.TrimSpace(payload.SHA) != expectedBlobSHA || payload.Size < 0 || payload.Size > maxBytes || !strings.EqualFold(strings.TrimSpace(payload.Encoding), "base64") {
		return UADashboardRepositoryFile{}, fmt.Errorf("read dashboard file from AGS: response does not match publication manifest")
	}
	content, err := base64.StdEncoding.DecodeString(payload.Content)
	if err != nil || int64(len(content)) != payload.Size || int64(len(content)) > maxBytes || !utf8.Valid(content) {
		return UADashboardRepositoryFile{}, fmt.Errorf("read dashboard file from AGS: invalid file content")
	}
	return UADashboardRepositoryFile{Content: content, BlobSHA: expectedBlobSHA}, nil
}

func uaDashboardRepositoryLocation(repo repos.ResourceRepository) (uaRepositoryLocation, error) {
	if repo.ResourceType != repos.ResourceTypeKBSource || repo.RepoRole != repos.RepoRoleInsights {
		return uaRepositoryLocation{}, invalidArgument("dashboard repository mapping is invalid")
	}
	owner := strings.TrimSpace(repo.RepoOwner)
	name := strings.TrimSpace(repo.RepoName)
	if owner == "" || name == "" || strings.Contains(owner, "/") || strings.Contains(name, "/") {
		return uaRepositoryLocation{}, invalidArgument("dashboard repository owner and name are required")
	}
	branch := strings.TrimSpace(repo.DefaultRef)
	if branch == "" {
		branch = "main"
	}
	return uaRepositoryLocation{owner: owner, repo: name, branch: branch}, nil
}

func validateUADashboardPublishFiles(files map[string][]byte) ([]string, error) {
	if !hasExactUADashboardFiles(files) {
		return nil, invalidArgument("dashboard publication requires the exact five-file set")
	}
	paths := make([]string, 0, len(files))
	for path, content := range files {
		if _, ok := uaDashboardAllowedFiles[path]; !ok || !utf8.Valid(content) {
			return nil, invalidArgument("dashboard publication contains an invalid file")
		}
		paths = append(paths, path)
	}
	sort.Strings(paths)
	return paths, nil
}

func (s AGSHTTPRepositoryStore) uaDashboardResolveHead(ctx context.Context, location uaRepositoryLocation) (string, string, error) {
	base := "/api/v3/repos/" + url.PathEscape(location.owner) + "/" + url.PathEscape(location.repo)
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, base+"/git/refs/heads/"+escapeUADashboardPath(location.branch), nil)
	if err != nil || status < 200 || status >= 300 {
		return "", "", fmt.Errorf("resolve dashboard repository head: status %d", status)
	}
	var ref struct {
		Object struct {
			SHA string `json:"sha"`
		} `json:"object"`
	}
	if err := decodeUADashboardAGSResponse(body, &ref); err != nil || !validUADashboardObjectID(ref.Object.SHA) {
		return "", "", fmt.Errorf("resolve dashboard repository head: invalid response")
	}
	headSHA := strings.TrimSpace(ref.Object.SHA)
	status, body, err = s.doAGSAPI(ctx, http.MethodGet, base+"/git/commits/"+url.PathEscape(headSHA), nil)
	if err != nil || status < 200 || status >= 300 {
		return "", "", fmt.Errorf("resolve dashboard repository tree: status %d", status)
	}
	var commit struct {
		SHA  string `json:"sha"`
		Tree struct {
			SHA string `json:"sha"`
		} `json:"tree"`
	}
	if err := decodeUADashboardAGSResponse(body, &commit); err != nil || commit.SHA != headSHA || !validUADashboardObjectID(commit.Tree.SHA) {
		return "", "", fmt.Errorf("resolve dashboard repository tree: invalid response")
	}
	return headSHA, strings.TrimSpace(commit.Tree.SHA), nil
}

func (s AGSHTTPRepositoryStore) uaDashboardVerifyTree(ctx context.Context, location uaRepositoryLocation, treeSHA string, paths []string, files map[string][]byte) error {
	base := "/api/v3/repos/" + url.PathEscape(location.owner) + "/" + url.PathEscape(location.repo)
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, base+"/git/trees/"+url.PathEscape(treeSHA), nil)
	if err != nil || status < 200 || status >= 300 {
		return fmt.Errorf("verify dashboard repository tree: status %d", status)
	}
	var payload struct {
		SHA       string                                   `json:"sha"`
		Truncated bool                                     `json:"truncated"`
		Tree      []struct{ Path, Mode, Type, SHA string } `json:"tree"`
	}
	if decodeUADashboardAGSResponse(body, &payload) != nil || payload.SHA != treeSHA || payload.Truncated {
		return fmt.Errorf("verify dashboard repository tree: invalid response")
	}
	uaTreeSHA := ""
	for _, entry := range payload.Tree {
		if entry.Path == ".ua" {
			if uaTreeSHA != "" || entry.Mode != "040000" || entry.Type != "tree" || !validUADashboardObjectID(entry.SHA) {
				return fmt.Errorf("verify dashboard repository tree: invalid .ua subtree")
			}
			uaTreeSHA = entry.SHA
		}
	}
	if uaTreeSHA == "" {
		return fmt.Errorf("verify dashboard repository tree: missing .ua subtree")
	}
	status, body, err = s.doAGSAPI(ctx, http.MethodGet, base+"/git/trees/"+url.PathEscape(uaTreeSHA), nil)
	if err != nil || status < 200 || status >= 300 {
		return fmt.Errorf("verify dashboard repository subtree: status %d", status)
	}
	payload = struct {
		SHA       string                                   `json:"sha"`
		Truncated bool                                     `json:"truncated"`
		Tree      []struct{ Path, Mode, Type, SHA string } `json:"tree"`
	}{}
	if decodeUADashboardAGSResponse(body, &payload) != nil || payload.SHA != uaTreeSHA || payload.Truncated {
		return fmt.Errorf("verify dashboard repository subtree: invalid response")
	}
	want := make(map[string]string, len(paths))
	for _, path := range paths {
		want[path] = plumbing.ComputeHash(plumbing.BlobObject, files[path]).String()
	}
	seen := make(map[string]struct{}, len(paths))
	for _, entry := range payload.Tree {
		expected, ok := want[entry.Path]
		_, duplicate := seen[entry.Path]
		if !ok || duplicate || entry.Mode != "100644" || entry.Type != "blob" || entry.SHA != expected {
			return fmt.Errorf("verify dashboard repository tree: publication contents mismatch")
		}
		seen[entry.Path] = struct{}{}
	}
	for path := range want {
		if _, ok := seen[path]; !ok {
			return fmt.Errorf("verify dashboard repository tree: publication contents mismatch")
		}
	}
	return nil
}

func (s AGSHTTPRepositoryStore) uaDashboardCreateTree(ctx context.Context, location uaRepositoryLocation, baseTreeSHA string, paths []string, files map[string][]byte) (string, error) {
	tree := make([]map[string]any, 0, len(paths)+1)
	tree = append(tree, map[string]any{"path": ".ua", "mode": "040000", "type": "tree", "sha": nil})
	for _, path := range paths {
		blobSHA, err := s.uaDashboardCreateBlob(ctx, location, files[path])
		if err != nil {
			return "", err
		}
		tree = append(tree, map[string]any{"path": ".ua/" + path, "mode": "100644", "type": "blob", "sha": blobSHA})
	}
	apiPath := "/api/v3/repos/" + url.PathEscape(location.owner) + "/" + url.PathEscape(location.repo) + "/git/trees"
	status, body, err := s.doAGSAPI(ctx, http.MethodPost, apiPath, map[string]any{"base_tree": baseTreeSHA, "tree": tree})
	if err != nil || status < 200 || status >= 300 {
		return "", fmt.Errorf("create dashboard repository tree: status %d", status)
	}
	var payload struct {
		SHA string `json:"sha"`
	}
	if err := decodeUADashboardAGSResponse(body, &payload); err != nil || !validUADashboardObjectID(payload.SHA) {
		return "", fmt.Errorf("create dashboard repository tree: invalid response")
	}
	return strings.TrimSpace(payload.SHA), nil
}

func (s AGSHTTPRepositoryStore) uaDashboardCreateBlob(ctx context.Context, location uaRepositoryLocation, content []byte) (string, error) {
	apiPath := "/api/v3/repos/" + url.PathEscape(location.owner) + "/" + url.PathEscape(location.repo) + "/git/blobs"
	status, body, err := s.doAGSAPI(ctx, http.MethodPost, apiPath, map[string]string{
		"content":  base64.StdEncoding.EncodeToString(content),
		"encoding": "base64",
	})
	if err != nil || status < 200 || status >= 300 {
		return "", fmt.Errorf("create dashboard repository blob: status %d", status)
	}
	var payload struct {
		SHA string `json:"sha"`
	}
	expectedSHA := plumbing.ComputeHash(plumbing.BlobObject, content).String()
	if err := decodeUADashboardAGSResponse(body, &payload); err != nil || strings.TrimSpace(payload.SHA) != expectedSHA {
		return "", fmt.Errorf("create dashboard repository blob: invalid response")
	}
	return expectedSHA, nil
}

func (s AGSHTTPRepositoryStore) uaDashboardCreateCommit(ctx context.Context, location uaRepositoryLocation, treeSHA, parentSHA string) (string, error) {
	apiPath := "/api/v3/repos/" + url.PathEscape(location.owner) + "/" + url.PathEscape(location.repo) + "/git/commits"
	status, body, err := s.doAGSAPI(ctx, http.MethodPost, apiPath, map[string]any{"message": "Import Understand Anything dashboard", "tree": treeSHA, "parents": []string{parentSHA}})
	if err != nil || status < 200 || status >= 300 {
		return "", fmt.Errorf("create dashboard repository commit: status %d", status)
	}
	var payload struct {
		SHA string `json:"sha"`
	}
	if err := decodeUADashboardAGSResponse(body, &payload); err != nil || !validUADashboardObjectID(payload.SHA) {
		return "", fmt.Errorf("create dashboard repository commit: invalid response")
	}
	return strings.TrimSpace(payload.SHA), nil
}

func (s AGSHTTPRepositoryStore) uaDashboardRetainCommit(ctx context.Context, location uaRepositoryLocation, sourceID, commitSHA string) error {
	ref := "refs/meshx/kb-dashboard/" + sourceID + "/" + commitSHA
	base := "/api/v3/repos/" + url.PathEscape(location.owner) + "/" + url.PathEscape(location.repo)
	status, _, err := s.doAGSAPI(ctx, http.MethodPost, base+"/git/refs", map[string]string{"ref": ref, "sha": commitSHA})
	if err != nil || status < 200 || status >= 300 {
		// A retry may observe an already-created immutable ref; exact read-back
		// below distinguishes safe recovery from conflict.
	}
	status, body, getErr := s.doAGSAPI(ctx, http.MethodGet, base+"/git/refs/"+escapeUADashboardPath(strings.TrimPrefix(ref, "refs/")), nil)
	if getErr != nil || status < 200 || status >= 300 {
		return fmt.Errorf("retain dashboard repository commit: status %d", status)
	}
	var payload struct {
		Ref    string `json:"ref"`
		Object struct {
			SHA string `json:"sha"`
		} `json:"object"`
	}
	if decodeErr := decodeUADashboardAGSResponse(body, &payload); decodeErr != nil || payload.Ref != ref || strings.TrimSpace(payload.Object.SHA) != commitSHA {
		return fmt.Errorf("retain dashboard repository commit: immutable ref conflict")
	}
	return nil
}

func (s AGSHTTPRepositoryStore) uaDashboardVerifyCommit(ctx context.Context, location uaRepositoryLocation, commitSHA, treeSHA, parentSHA string) error {
	base := "/api/v3/repos/" + url.PathEscape(location.owner) + "/" + url.PathEscape(location.repo)
	status, body, err := s.doAGSAPI(ctx, http.MethodGet, base+"/git/commits/"+url.PathEscape(commitSHA), nil)
	if err != nil || status < 200 || status >= 300 {
		return fmt.Errorf("verify dashboard repository commit: status %d", status)
	}
	var commit struct {
		SHA  string `json:"sha"`
		Tree struct {
			SHA string `json:"sha"`
		} `json:"tree"`
		Parents []struct {
			SHA string `json:"sha"`
		} `json:"parents"`
	}
	if decodeUADashboardAGSResponse(body, &commit) != nil || commit.SHA != commitSHA || commit.Tree.SHA != treeSHA || len(commit.Parents) != 1 || commit.Parents[0].SHA != parentSHA {
		return fmt.Errorf("verify dashboard repository commit: response does not match requested tree and parent")
	}
	return nil
}

func validUADashboardObjectID(value string) bool {
	value = strings.TrimSpace(value)
	if len(value) != 40 && len(value) != 64 {
		return false
	}
	for _, character := range value {
		if !strings.ContainsRune("0123456789abcdef", character) {
			return false
		}
	}
	return true
}

func decodeUADashboardAGSResponse(body []byte, value any) error {
	if len(body) == 0 {
		return fmt.Errorf("invalid AGS response size")
	}
	return json.Unmarshal(body, value)
}

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