package kb

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

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

type AGSHTTPRepositoryStore struct {
	Handler             http.Handler
	BaseURL             string
	AuthHeader          string
	AuthorizationHeader string
	RepositoryGrantor   repos.ResourceRepositoryGrantor
}

type agsWikiPageLabel struct {
	Name string `json:"name"`
}

func (s AGSHTTPRepositoryStore) RepositoryExists(ctx context.Context, repository repos.RepositoryIdentity) (bool, error) {
	owner := strings.TrimSpace(repository.Owner)
	name := strings.TrimSpace(repository.Name)
	if owner == "" || name == "" {
		return false, invalidArgument("repository owner/name is required")
	}
	_, exists, err := s.getRepo(ctx, owner, name)
	return exists, err
}

type agsWikiPagePruneRow struct {
	Slug   string             `json:"slug"`
	Labels []agsWikiPageLabel `json:"labels"`
}

func (s AGSHTTPRepositoryStore) EnsureRepository(ctx context.Context, request repos.AGSRepositoryEnsureRequest) (repos.RepositoryIdentity, error) {
	if s.Handler == nil {
		return repos.RepositoryIdentity{}, invalidArgument("AGS repository provisioner is not configured")
	}
	if strings.TrimSpace(s.AuthorizationHeader) == "" {
		return repos.RepositoryIdentity{}, unauthorized("Authorization is required to ensure AGS repositories")
	}
	owner := strings.TrimSpace(request.Owner)
	name := strings.TrimSpace(request.Name)
	if owner == "" || name == "" {
		return repos.RepositoryIdentity{}, invalidArgument("repository owner/name is required")
	}
	identity := repos.RepositoryIdentity{Owner: owner, Name: name, FullName: owner + "/" + name, DefaultBranch: strings.TrimSpace(request.DefaultBranch)}
	if repo, ok, err := s.getRepo(ctx, owner, name); err != nil || ok {
		if err != nil {
			return repos.RepositoryIdentity{}, err
		}
		resolved := repositoryIdentityFromAGS(identity, repo)
		if request.RejectExisting {
			return repos.RepositoryIdentity{}, repos.RepositoryAlreadyExistsConflict(resolved.FullName, request.ResourceType, request.ResourceID, request.RepoRole, firstNonEmptyString(request.NameConflictField, repos.RepositoryNameFieldRepoNameOverride), request.SuggestedRepoName)
		}
		if err := s.grantSourceRepositoryAccess(ctx, resolved, request.Actor); err != nil {
			return repos.RepositoryIdentity{}, err
		}
		return resolved, nil
	}
	if err := s.ensureOwner(ctx, owner); err != nil {
		return repos.RepositoryIdentity{}, err
	}
	repo, err := s.createRepo(ctx, owner, name, request)
	if err != nil {
		// createRepo may return a fallback response (with known owner/name)
		// when the repo was created (2xx) but the response could not be
		// decoded. In that case, pass the identity to the caller for cleanup.
		if repo.Name != "" || repo.FullName != "" {
			return repositoryIdentityFromAGS(identity, repo), err
		}
		return repos.RepositoryIdentity{}, err
	}
	resolved := repositoryIdentityFromAGS(identity, repo)
	if err := s.grantSourceRepositoryAccess(ctx, resolved, request.Actor); err != nil {
		// The repo was just created by this call but the post-create grant
		// failed. Return the resolved identity alongside the error so that
		// the caller (ensureSourceRepositories) can include it in the repos
		// slice for best-effort cleanup. We do NOT delete here because the
		// grant failure may be transient (e.g. permissions not yet
		// propagated) and a retry should find the repo via getRepo.
		return resolved, err
	}
	return resolved, nil
}

func (s AGSHTTPRepositoryStore) ResolveRepositorySnapshot(ctx context.Context, repository repos.RepositoryIdentity) (string, error) {
	if s.Handler == nil || strings.TrimSpace(s.AuthorizationHeader) == "" {
		return "", invalidArgument("AGS repository snapshot resolver is not configured")
	}
	identity := sourceRepositoryIdentity(repository)
	branch := firstNonEmptyString(identity.DefaultBranch, "main")
	response := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodGet,
		"/api/v3/repos/"+url.PathEscape(identity.Owner)+"/"+url.PathEscape(identity.Name)+"/git/refs/heads/"+pathEscapeSegments(branch), nil)
	if response.Code < 200 || response.Code >= 300 {
		return "", fmt.Errorf("resolve AGS repository snapshot: status %d", response.Code)
	}
	var ref struct {
		Object struct {
			SHA string `json:"sha"`
		} `json:"object"`
	}
	if err := json.Unmarshal(response.Body.Bytes(), &ref); err != nil || !isCommitSHA(strings.TrimSpace(ref.Object.SHA)) {
		return "", fmt.Errorf("resolve AGS repository snapshot: invalid response")
	}
	return strings.TrimSpace(ref.Object.SHA), nil
}

func (s AGSHTTPRepositoryStore) DeleteRepository(ctx context.Context, repository repos.RepositoryIdentity) error {
	if s.Handler == nil {
		return invalidArgument("AGS repository deleter is not configured")
	}
	if strings.TrimSpace(s.AuthorizationHeader) == "" {
		return unauthorized("Authorization is required to delete AGS repositories")
	}
	identity := sourceRepositoryIdentity(repository)
	if identity.Owner == "" || identity.Name == "" {
		return invalidArgument("repository owner/name is required")
	}
	response := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodDelete, "/api/v3/repos/"+url.PathEscape(identity.Owner)+"/"+url.PathEscape(identity.Name), nil)
	if response.Code == http.StatusNotFound {
		return nil
	}
	if response.Code < 200 || response.Code >= 300 {
		return fmt.Errorf("delete AGS repository: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	return nil
}

func (s AGSHTTPRepositoryStore) UpdateRepositoryVisibility(ctx context.Context, repository repos.RepositoryIdentity, visibility string) (repos.RepositoryIdentity, error) {
	if s.Handler == nil {
		return repos.RepositoryIdentity{}, invalidArgument("AGS repository updater is not configured")
	}
	if strings.TrimSpace(s.AuthorizationHeader) == "" {
		return repos.RepositoryIdentity{}, unauthorized("Authorization is required to update AGS repositories")
	}
	identity := sourceRepositoryIdentity(repository)
	if identity.Owner == "" || identity.Name == "" {
		return repos.RepositoryIdentity{}, invalidArgument("repository owner/name is required")
	}
	visibility = normalizeSourceRepositoryVisibility(visibility)
	response := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodPatch, "/api/v3/repos/"+url.PathEscape(identity.Owner)+"/"+url.PathEscape(identity.Name), map[string]any{
		"private": visibility != "public",
	})
	if response.Code < 200 || response.Code >= 300 {
		return repos.RepositoryIdentity{}, fmt.Errorf("update AGS repository visibility: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	var repoResponse agsRepositoryResponse
	if err := json.Unmarshal(response.Body.Bytes(), &repoResponse); err != nil {
		return repos.RepositoryIdentity{}, fmt.Errorf("update AGS repository visibility: decode repository: %w", err)
	}
	return repositoryIdentityFromAGS(identity, repoResponse), nil
}

func (s AGSHTTPRepositoryStore) PutPage(ctx context.Context, request wiki.KBWikiPageWrite) error {
	if s.Handler == nil {
		return invalidArgument("AGS wiki store is not configured")
	}
	full := request.Repository.FullName
	if full == "" {
		full = request.Repository.Owner + "/" + request.Repository.Name
	}
	owner, repo, ok := strings.Cut(full, "/")
	if !ok || strings.TrimSpace(owner) == "" || strings.TrimSpace(repo) == "" {
		return invalidArgument("valid AGS wiki repository is required")
	}
	payload := map[string]string{
		"body":    request.Page.Body,
		"message": firstNonEmptyString(request.Message, "Generate KB wiki page"),
	}
	response := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodPut, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/wiki/pages/"+escapedWikiSlug(request.Page.Slug), payload)
	if response.Code < 200 || response.Code >= 300 {
		return fmt.Errorf("put AGS wiki page: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	labels := normalizeLabels(request.Page.Labels)
	if len(labels) > 0 {
		if err := s.ensureRepoLabels(ctx, owner, repo, labels); err != nil {
			return err
		}
		labelResponse := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodPut, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/wiki/pages/"+escapedWikiSlug(request.Page.Slug)+"/labels", map[string][]string{"labels": labels})
		if labelResponse.Code < 200 || labelResponse.Code >= 300 {
			return fmt.Errorf("set AGS wiki page labels: status %d: %s", labelResponse.Code, strings.TrimSpace(labelResponse.Body.String()))
		}
	}
	return nil
}

func (s AGSHTTPRepositoryStore) ensureRepositoryLabels(ctx context.Context, owner string, repo string, labels []repositoryLabel) 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 []agsWikiPageLabel
	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
}

func (s AGSHTTPRepositoryStore) ensureRepoLabels(ctx context.Context, owner string, repo string, labels []string) error {
	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 []agsWikiPageLabel
	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 {
		if _, ok := seen[label]; ok {
			continue
		}
		payload := map[string]string{
			"name":        label,
			"color":       "0969da",
			"description": "Generated from repository code",
		}
		createResponse := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodPost, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/labels", payload)
		if createResponse.Code == http.StatusUnprocessableEntity && strings.Contains(strings.ToLower(createResponse.Body.String()), "exists") {
			continue
		}
		if createResponse.Code < 200 || createResponse.Code >= 300 {
			return fmt.Errorf("create AGS repository label %q: status %d: %s", label, createResponse.Code, strings.TrimSpace(createResponse.Body.String()))
		}
	}
	return nil
}

func (s AGSHTTPRepositoryStore) PrunePages(ctx context.Context, request wiki.KBWikiPrune) error {
	if s.Handler == nil {
		return invalidArgument("AGS wiki store is not configured")
	}
	full := request.Repository.FullName
	if full == "" {
		full = request.Repository.Owner + "/" + request.Repository.Name
	}
	owner, repo, ok := strings.Cut(full, "/")
	if !ok || strings.TrimSpace(owner) == "" || strings.TrimSpace(repo) == "" {
		return invalidArgument("valid AGS wiki repository is required")
	}
	labels := normalizeLabels(request.Labels)
	if len(labels) == 0 {
		return invalidArgument("wiki labels are required to prune generated pages")
	}
	var rows []agsWikiPagePruneRow
	const perPage = 100
	for page := 1; ; page++ {
		query := url.Values{}
		query.Set("labels", strings.Join(labels, ","))
		query.Set("per_page", strconv.Itoa(perPage))
		query.Set("page", strconv.Itoa(page))
		listPath := "/api/v3/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(repo) + "/wiki/pages?" + query.Encode()
		response := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodGet, listPath, nil)
		if response.Code < 200 || response.Code >= 300 {
			return fmt.Errorf("list AGS wiki pages for prune: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
		}
		var pageRows []agsWikiPagePruneRow
		if err := json.Unmarshal(response.Body.Bytes(), &pageRows); err != nil {
			return fmt.Errorf("decode AGS wiki pages for prune: %w", err)
		}
		rows = append(rows, pageRows...)
		if len(pageRows) < perPage {
			break
		}
	}
	keep := map[string]struct{}{}
	for _, slug := range request.KeepSlugs {
		slug = normalizeWikiSlug(slug)
		if slug != "" {
			keep[slug] = struct{}{}
		}
	}
	message := firstNonEmptyString(request.Message, "Prune stale generated KB wiki page")
	for _, row := range rows {
		slug := normalizeWikiSlug(row.Slug)
		if slug == "" {
			continue
		}
		if len(row.Labels) > 0 && !agsWikiPageHasLabels(row.Labels, labels) {
			continue
		}
		if _, ok := keep[slug]; ok {
			continue
		}
		payload := map[string]string{"message": message}
		deleteResponse := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodDelete, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/wiki/pages/"+escapedWikiSlug(slug), payload)
		if deleteResponse.Code == http.StatusNotFound {
			continue
		}
		if deleteResponse.Code < 200 || deleteResponse.Code >= 300 {
			return fmt.Errorf("delete stale AGS wiki page %q: status %d: %s", slug, deleteResponse.Code, strings.TrimSpace(deleteResponse.Body.String()))
		}
	}
	return nil
}

func agsWikiPageHasLabels(pageLabels []agsWikiPageLabel, required []string) bool {
	if len(required) == 0 {
		return true
	}
	seen := map[string]struct{}{}
	for _, label := range pageLabels {
		name := strings.ToLower(strings.TrimSpace(label.Name))
		if name != "" {
			seen[name] = struct{}{}
		}
	}
	for _, label := range required {
		if _, ok := seen[label]; !ok {
			return false
		}
	}
	return true
}

func (s AGSHTTPRepositoryStore) Reconcile(ctx context.Context, repository repos.RepositoryIdentity) (wiki.KBWikiReconcileResult, error) {
	if s.Handler == nil {
		return wiki.KBWikiReconcileResult{}, invalidArgument("AGS wiki store is not configured")
	}
	full := repository.FullName
	if full == "" {
		full = repository.Owner + "/" + repository.Name
	}
	owner, repo, ok := strings.Cut(full, "/")
	if !ok || strings.TrimSpace(owner) == "" || strings.TrimSpace(repo) == "" {
		return wiki.KBWikiReconcileResult{}, invalidArgument("valid AGS wiki repository is required")
	}
	response := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodPost, "/api/ext/v1/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo)+"/wiki/reconcile", nil)
	if response.Code < 200 || response.Code >= 300 {
		return wiki.KBWikiReconcileResult{}, fmt.Errorf("reconcile AGS wiki: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	var body struct {
		RepositoryID     any    `json:"repository_id"`
		IndexedCommitSHA string `json:"indexed_commit_sha"`
		PageCount        int    `json:"page_count"`
		Reconciled       bool   `json:"reconciled"`
	}
	if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil {
		return wiki.KBWikiReconcileResult{}, fmt.Errorf("decode AGS wiki reconcile: %w", err)
	}
	return wiki.KBWikiReconcileResult{
		RepositoryID:     fmt.Sprint(body.RepositoryID),
		IndexedCommitSHA: body.IndexedCommitSHA,
		PageCount:        body.PageCount,
		Reconciled:       body.Reconciled,
	}, nil
}

func (s AGSHTTPRepositoryStore) getRepo(ctx context.Context, owner string, repo string) (agsRepositoryResponse, bool, error) {
	response := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo), nil)
	if response.Code == http.StatusNotFound {
		return agsRepositoryResponse{}, false, nil
	}
	if response.Code < 200 || response.Code >= 300 {
		return agsRepositoryResponse{}, false, fmt.Errorf("read AGS repository: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	var repoResponse agsRepositoryResponse
	if err := json.Unmarshal(response.Body.Bytes(), &repoResponse); err != nil {
		return agsRepositoryResponse{}, false, fmt.Errorf("decode AGS repository: %w", err)
	}
	return repoResponse, true, nil
}

func (s AGSHTTPRepositoryStore) ensureOwner(ctx context.Context, owner string) error {
	getResponse := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodGet, "/api/v3/orgs/"+url.PathEscape(owner), nil)
	if getResponse.Code >= 200 && getResponse.Code < 300 {
		return nil
	}
	if getResponse.Code != http.StatusNotFound {
		return fmt.Errorf("ensure AGS repository owner: get org: status %d: %s", getResponse.Code, strings.TrimSpace(getResponse.Body.String()))
	}
	payload := map[string]string{"login": owner, "name": owner, "default_repository_permission": "none"}
	createResponse := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodPost, "/api/ext/v1/user/orgs", payload)
	if createResponse.Code == http.StatusConflict {
		return nil
	}
	if createResponse.Code < 200 || createResponse.Code >= 300 {
		return fmt.Errorf("ensure AGS repository owner: create org: status %d: %s", createResponse.Code, strings.TrimSpace(createResponse.Body.String()))
	}
	return nil
}

func (s AGSHTTPRepositoryStore) createRepo(ctx context.Context, owner string, repo string, request repos.AGSRepositoryEnsureRequest) (agsRepositoryResponse, error) {
	falseValue := false
	visibility := normalizeSourceRepositoryVisibility(request.Visibility)
	description := firstNonEmptyString(request.Description, "AgentTeam managed repository for "+repo)
	kind := firstNonEmptyString(request.Kind, "meshx_resource")
	payload := map[string]any{
		"name":            repo,
		"description":     description,
		"kind":            kind,
		"source_type":     strings.TrimSpace(request.SourceType),
		"user_visible":    request.UserVisible,
		"visibility":      visibility,
		"private":         visibility != "public",
		"has_issues":      request.Features.Issues,
		"has_projects":    request.Features.Projects,
		"has_wiki":        request.Features.Wiki,
		"has_downloads":   request.Features.Downloads,
		"has_discussions": request.Features.Discussions,
		"auto_init":       request.AutoInit,
		"default_branch":  firstNonEmptyString(request.DefaultBranch, "main"),
	}
	if !request.Features.Issues {
		payload["has_issues"] = falseValue
	}
	if !request.Features.Wiki {
		payload["has_wiki"] = falseValue
	}
	response := doAGSInternalJSONRequest(ctx, s.Handler, s.AuthHeader, s.AuthorizationHeader, http.MethodPost, "/api/v3/orgs/"+url.PathEscape(owner)+"/repos", payload)
	if response.Code == http.StatusConflict {
		repoResponse, ok, err := s.getRepo(ctx, owner, repo)
		if err != nil {
			return agsRepositoryResponse{}, err
		}
		if ok {
			if request.RejectExisting {
				resolved := repositoryIdentityFromAGS(repos.RepositoryIdentity{Owner: owner, Name: repo, FullName: owner + "/" + repo}, repoResponse)
				return agsRepositoryResponse{}, repos.RepositoryAlreadyExistsConflict(resolved.FullName, request.ResourceType, request.ResourceID, request.RepoRole, firstNonEmptyString(request.NameConflictField, repos.RepositoryNameFieldRepoNameOverride), request.SuggestedRepoName)
			}
			return repoResponse, nil
		}
	}
	if response.Code < 200 || response.Code >= 300 {
		return agsRepositoryResponse{}, fmt.Errorf("create AGS repository: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	var repoResponse agsRepositoryResponse
	if err := json.Unmarshal(response.Body.Bytes(), &repoResponse); err != nil {
		// The repo was created (2xx) but the response body could not be
		// decoded. Return a fallback response with the known owner/name so
		// that EnsureRepository can pass the identity to the caller for
		// cleanup. Deleting an AGS repo only needs owner/name; ID is not
		// required.
		return agsRepositoryResponse{
			Name:          repo,
			FullName:      owner + "/" + repo,
			DefaultBranch: firstNonEmptyString(request.DefaultBranch, "main"),
		}, fmt.Errorf("create AGS repository: decode created repo: %w", err)
	}
	return repoResponse, nil
}

func (s AGSHTTPRepositoryStore) grantSourceRepositoryAccess(ctx context.Context, repository repos.RepositoryIdentity, actor repos.ResourceRepositoryActor) error {
	_, err := s.GrantRepositoryCollaborator(ctx, repos.ResourceRepositoryCollaboratorGrant{
		Repository: repository,
		Actor:      actor,
		Permission: "write",
	})
	return err
}

func (s AGSHTTPRepositoryStore) GrantRepositoryCollaborator(ctx context.Context, grant repos.ResourceRepositoryCollaboratorGrant) (repos.ResourceRepositoryCollaboratorGrantResult, error) {
	if s.RepositoryGrantor == nil {
		return repos.ResourceRepositoryCollaboratorGrantResult{}, nil
	}
	if strings.TrimSpace(grant.Actor.Provider) == "" || strings.TrimSpace(grant.Actor.Subject) == "" || strings.TrimSpace(grant.Actor.Login) == "" {
		return repos.ResourceRepositoryCollaboratorGrantResult{}, nil
	}
	grant.Repository = sourceRepositoryIdentity(grant.Repository)
	if strings.TrimSpace(grant.Repository.Owner) == "" || strings.TrimSpace(grant.Repository.Name) == "" {
		return repos.ResourceRepositoryCollaboratorGrantResult{}, invalidArgument("repository owner/name is required")
	}
	if strings.TrimSpace(grant.Permission) == "" {
		grant.Permission = "write"
	}
	return s.RepositoryGrantor.GrantRepositoryCollaborator(ctx, grant)
}

func (s AGSHTTPRepositoryStore) RevokeRepositoryCollaborator(ctx context.Context, revoke repos.ResourceRepositoryCollaboratorRevoke) error {
	revoker, ok := s.RepositoryGrantor.(repos.ResourceRepositoryRevoker)
	if !ok || revoker == nil {
		return nil
	}
	if strings.TrimSpace(revoke.Actor.Provider) == "" || strings.TrimSpace(revoke.Actor.Subject) == "" || strings.TrimSpace(revoke.Actor.Login) == "" {
		return nil
	}
	revoke.Repository = sourceRepositoryIdentity(revoke.Repository)
	if strings.TrimSpace(revoke.Repository.Owner) == "" || strings.TrimSpace(revoke.Repository.Name) == "" {
		return invalidArgument("repository owner/name is required")
	}
	return revoker.RevokeRepositoryCollaborator(ctx, revoke)
}

func sourceRepositoryIdentity(repository repos.RepositoryIdentity) repos.RepositoryIdentity {
	identity := repos.RepositoryIdentity{
		Owner:         strings.TrimSpace(repository.Owner),
		Name:          strings.TrimSpace(repository.Name),
		FullName:      strings.TrimSpace(repository.FullName),
		ID:            strings.TrimSpace(repository.ID),
		DefaultBranch: strings.TrimSpace(repository.DefaultBranch),
		Archived:      repository.Archived,
	}
	if identity.FullName != "" {
		if owner, name, ok := strings.Cut(identity.FullName, "/"); ok {
			if identity.Owner == "" {
				identity.Owner = strings.TrimSpace(owner)
			}
			if identity.Name == "" {
				identity.Name = strings.TrimSpace(name)
			}
		}
	}
	if identity.Owner != "" && identity.Name != "" {
		identity.FullName = identity.Owner + "/" + identity.Name
	}
	return identity
}

func normalizeSourceRepositoryVisibility(visibility string) string {
	switch strings.ToLower(strings.TrimSpace(visibility)) {
	case "public":
		return "public"
	default:
		return "private"
	}
}

func repositoryIdentityFromAGS(base repos.RepositoryIdentity, repo agsRepositoryResponse) repos.RepositoryIdentity {
	if repo.Name != "" {
		base.Name = repo.Name
	}
	if repo.FullName != "" {
		base.FullName = repo.FullName
		if owner, name, ok := strings.Cut(repo.FullName, "/"); ok {
			base.Owner = strings.TrimSpace(owner)
			base.Name = strings.TrimSpace(name)
		}
	}
	if repo.ID != 0 {
		base.ID = fmt.Sprintf("%d", repo.ID)
	}
	if repo.DefaultBranch != "" {
		base.DefaultBranch = repo.DefaultBranch
	}
	base.Archived = repo.Archived
	return base
}
