// Package asset contains Desktop access-batch primitives shared by
// repository-backed MeshX asset providers. Domain providers still own Memory
// vault/write-back and Skill materialization semantics.
package asset

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

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
)

// ConnectionInfo describes the AgentTeam channel connection needed to call a MeshX
// access-batch endpoint.
type ConnectionInfo struct {
	ServerURL string
	UserToken string
	// RefreshUserToken rotates a stale session token after a downstream 401.
	// It is optional so test callers and non-session transports can keep using
	// a fixed token.
	RefreshUserToken func(context.Context, string) (string, error)
	WorkspaceID      string
	TemplateLocale   string
	HTTPClient       *http.Client
}

// batchRequest is the common JSON body for agent asset access-batch endpoints.
type batchRequest struct {
	AgentIDs       []string `json:"agentIds"`
	TemplateLocale string   `json:"templateLocale,omitempty"`
}

// PostAgentsBatchAccess posts one bounded access request for up to 200 agents.
func PostAgentsBatchAccess(ctx context.Context, fallbackClient *http.Client, conn ConnectionInfo, endpointPath string, agentIDs []string, out any) (string, error) {
	ids := make([]string, 0, len(agentIDs))
	seen := make(map[string]struct{}, len(agentIDs))
	for _, agentID := range agentIDs {
		agentID = strings.TrimSpace(agentID)
		if agentID == "" {
			continue
		}
		if _, ok := seen[agentID]; ok {
			continue
		}
		seen[agentID] = struct{}{}
		ids = append(ids, agentID)
	}
	if len(ids) == 0 || len(ids) > 200 {
		return "", fmt.Errorf("assetprovider: agent ids must contain 1 to 200 unique values")
	}
	serverURL := strings.TrimRight(strings.TrimSpace(conn.ServerURL), "/")
	userToken := strings.TrimSpace(conn.UserToken)
	workspaceID := strings.TrimSpace(conn.WorkspaceID)
	if serverURL == "" || userToken == "" || !identifiers.ValidWorkspaceID(workspaceID) {
		return "", fmt.Errorf("assetprovider: missing or invalid server URL, token, or workspace")
	}
	client := conn.HTTPClient
	if client == nil {
		client = fallbackClient
	}
	if client == nil {
		client = http.DefaultClient
	}
	endpointPath = "/" + strings.TrimLeft(strings.TrimSpace(endpointPath), "/")
	if endpointPath != "/api/v1" && !strings.HasPrefix(endpointPath, "/api/v1/") {
		return "", fmt.Errorf("assetprovider: invalid workspace asset endpoint")
	}
	endpointPath = "/api/v1/workspaces/" + url.PathEscape(workspaceID) +
		strings.TrimPrefix(endpointPath, "/api/v1")
	endpoint := serverURL + endpointPath
	body, err := json.Marshal(batchRequest{
		AgentIDs:       ids,
		TemplateLocale: strings.TrimSpace(conn.TemplateLocale),
	})
	if err != nil {
		return "", fmt.Errorf("assetprovider: marshal batch request: %w", err)
	}
	request := func(token string) (*http.Response, error) {
		req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
		if err != nil {
			return nil, fmt.Errorf("assetprovider: build batch request: %w", err)
		}
		req.Header.Set("Authorization", "Bearer "+token)
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("X-Workspace-Id", workspaceID)
		req.Header.Set("X-AgentTeam-Route-Workspace-Id", workspaceID)
		return client.Do(req)
	}
	resp, err := request(userToken)
	if err != nil {
		return "", fmt.Errorf("assetprovider: batch access request: %w", err)
	}
	if resp.StatusCode == http.StatusUnauthorized && conn.RefreshUserToken != nil {
		_ = resp.Body.Close()
		refreshed, refreshErr := conn.RefreshUserToken(ctx, userToken)
		if refreshErr != nil {
			return "", fmt.Errorf("assetprovider: refresh user token after 401: %w", refreshErr)
		}
		refreshed = strings.TrimSpace(refreshed)
		if refreshed == "" {
			return "", fmt.Errorf("assetprovider: refresh user token after 401: empty token")
		}
		userToken = refreshed
		resp, err = request(userToken)
		if err != nil {
			return "", fmt.Errorf("assetprovider: batch access request after token refresh: %w", err)
		}
	}
	defer func() { _ = resp.Body.Close() }()
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("assetprovider: batch access: status %d", resp.StatusCode)
	}
	if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
		return "", fmt.Errorf("assetprovider: decode batch response: %w", err)
	}
	return endpoint, nil
}

// ComposeCloneURL builds the workspace-qualified Git URL from the AgentTeam server
// URL and an AGS repository full name. The path workspace supplies placement;
// the repository credential remains the authorization authority at AGS.
func ComposeCloneURL(serverURL, workspaceID, repoFullName string) string {
	serverURL = strings.TrimRight(strings.TrimSpace(serverURL), "/")
	workspaceID = strings.TrimSpace(workspaceID)
	repoFullName = strings.Trim(strings.TrimSpace(repoFullName), "/")
	if serverURL == "" || !identifiers.ValidWorkspaceID(workspaceID) || repoFullName == "" {
		return ""
	}
	return serverURL + "/api/v1/workspaces/" + url.PathEscape(workspaceID) + "/ags/" + repoFullName + ".git"
}
