package roles

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/agsbridge"
	"github.com/tidbcloud/AgentTeam/internal/service/kb"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

const (
	rolePackageRepositoryResourceType = "role_package"
	rolePackageRepositoryRepoRole     = reposstore.RepoRolePrimary
	rolePackageRepositoryBranchPrefix = "role-package/"
)

// AGSHTTPRolePackageContentStore persists immutable role package assets into a
// hidden AGS repository per role and resolves them back through the AGS
// contents API.
type AGSHTTPRolePackageContentStore struct {
	Handler             http.Handler
	BaseURL             string
	AuthHeader          string
	AuthorizationHeader string
}

func (s AGSHTTPRolePackageContentStore) StoreRolePackage(ctx context.Context, input StoreRolePackageInput) (StoreRolePackageOutput, error) {
	if strings.TrimSpace(input.WorkspaceID) == "" || strings.TrimSpace(input.RoleID) == "" || strings.TrimSpace(input.VersionID) == "" {
		return StoreRolePackageOutput{}, errRolePackageContentRequired()
	}
	if s.Handler == nil && strings.TrimSpace(s.BaseURL) == "" {
		return StoreRolePackageOutput{}, errRolePackageContentRequired()
	}
	repo, err := s.ensureRepository(ctx, input)
	if err != nil {
		return StoreRolePackageOutput{}, rolePackageDependencyError(err)
	}
	branch := rolePackageRepositoryBranch(input.VersionID)
	if err := s.ensureBranch(ctx, repo, branch); err != nil {
		return StoreRolePackageOutput{}, rolePackageDependencyError(err)
	}
	assets := make(map[string]RolePackageStoredAsset, len(input.Assets))
	for _, asset := range input.Assets {
		assetPath, err := rolePackageAssetPath(asset)
		if err != nil {
			return StoreRolePackageOutput{}, err
		}
		content := strings.TrimSpace(asset.Content)
		if content == "" {
			return StoreRolePackageOutput{}, errRolePackageContentRequired()
		}
		commitSHA, err := s.putFile(ctx, repo, branch, assetPath, content, rolePackageCommitMessage(input.VersionID, assetPath))
		if err != nil {
			return StoreRolePackageOutput{}, rolePackageDependencyError(err)
		}
		assets[assetPath] = RolePackageStoredAsset{
			Path:        assetPath,
			ContentRef:  s.contentRef(repo, assetPath, commitSHA),
			ContentHash: rolePackageContentHash(content),
		}
	}
	return StoreRolePackageOutput{Assets: assets}, nil
}

func (s AGSHTTPRolePackageContentStore) ResolveRolePackage(ctx context.Context, input ResolveRolePackageInput) (string, error) {
	requestPath, err := s.resolveRequestPath(input.ContentRef)
	if err != nil {
		return "", errRolePackageContentRequired()
	}
	status, body, err := s.doJSON(ctx, http.MethodGet, requestPath, nil)
	if err != nil {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: %w", err))
	}
	if status < 200 || status >= 300 {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: status %d", status))
	}
	var payload struct {
		Type     string `json:"type"`
		Encoding string `json:"encoding"`
		Content  string `json:"content"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: decode AGS contents: %w", err))
	}
	if strings.TrimSpace(payload.Type) != "file" {
		return "", invalidRolePackageContract("role package content_ref must resolve to a file")
	}
	decoded, err := base64.StdEncoding.DecodeString(removeASCIIWhitespace(payload.Content))
	if err != nil {
		return "", rolePackageDependencyError(fmt.Errorf("resolve role package content: decode content: %w", err))
	}
	return string(decoded), nil
}

func (s AGSHTTPRolePackageContentStore) ensureRepository(ctx context.Context, input StoreRolePackageInput) (repos.RepositoryIdentity, error) {
	store := kb.AGSHTTPRepositoryStore{
		Handler:             s.Handler,
		BaseURL:             strings.TrimSpace(s.BaseURL),
		AuthHeader:          s.AuthHeader,
		AuthorizationHeader: s.AuthorizationHeader,
	}
	repoName := rolePackageRepositoryName(input.RoleID)
	repo, err := store.EnsureRepository(ctx, repos.AGSRepositoryEnsureRequest{
		WorkspaceID:    strings.TrimSpace(input.WorkspaceID),
		ResourceType:   rolePackageRepositoryResourceType,
		ResourceID:     strings.TrimSpace(input.RoleID),
		RepoRole:       rolePackageRepositoryRepoRole,
		Owner:          strings.TrimSpace(input.WorkspaceID),
		Name:           repoName,
		Description:    "AgentTeam managed role package repository for " + strings.TrimSpace(input.RoleID),
		Kind:           "role_package_canonical",
		Visibility:     "private",
		UserVisible:    false,
		AutoInit:       true,
		RejectExisting: false,
		DefaultBranch:  "main",
		Features:       repos.AGSRepositoryFeatures{},
	})
	if err != nil {
		return repos.RepositoryIdentity{}, fmt.Errorf("ensure role package repository: %w", err)
	}
	return repos.RepositoryIdentity{
		Owner:         repo.Owner,
		Name:          repo.Name,
		FullName:      repo.FullName,
		ID:            repo.ID,
		DefaultBranch: repo.DefaultBranch,
		Archived:      repo.Archived,
	}, nil
}

func (s AGSHTTPRolePackageContentStore) ensureBranch(ctx context.Context, repo repos.RepositoryIdentity, branch string) error {
	baseBranch := firstNonEmpty(strings.TrimSpace(repo.DefaultBranch), "main")
	baseSHA, err := s.branchHeadSHA(ctx, repo, baseBranch)
	if err != nil {
		return err
	}
	status, body, err := s.doJSON(ctx, http.MethodPost, "/api/v3/repos/"+url.PathEscape(repo.Owner)+"/"+url.PathEscape(repo.Name)+"/git/refs", map[string]any{
		"ref": "refs/heads/" + branch,
		"sha": baseSHA,
	})
	if err != nil {
		return fmt.Errorf("create role package branch %q: %w", branch, err)
	}
	if status == http.StatusCreated {
		return nil
	}
	if status == http.StatusUnprocessableEntity && strings.Contains(strings.ToLower(string(body)), "reference already exists") {
		return nil
	}
	if status < 200 || status >= 300 {
		return fmt.Errorf("create role package branch %q: status %d: %s", branch, status, strings.TrimSpace(string(body)))
	}
	return nil
}

func (s AGSHTTPRolePackageContentStore) branchHeadSHA(ctx context.Context, repo repos.RepositoryIdentity, branch string) (string, error) {
	status, body, err := s.doJSON(ctx, http.MethodGet, "/api/v3/repos/"+url.PathEscape(repo.Owner)+"/"+url.PathEscape(repo.Name)+"/branches/"+escapeAGSPath(branch), nil)
	if err != nil {
		return "", fmt.Errorf("read role package branch %q: %w", branch, err)
	}
	if status < 200 || status >= 300 {
		return "", fmt.Errorf("read role package branch %q: status %d: %s", branch, status, strings.TrimSpace(string(body)))
	}
	var payload struct {
		Commit struct {
			SHA string `json:"sha"`
		} `json:"commit"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return "", fmt.Errorf("read role package branch %q: decode branch: %w", branch, err)
	}
	if strings.TrimSpace(payload.Commit.SHA) == "" {
		return "", fmt.Errorf("read role package branch %q: empty commit sha", branch)
	}
	return strings.TrimSpace(payload.Commit.SHA), nil
}

func (s AGSHTTPRolePackageContentStore) putFile(ctx context.Context, repo repos.RepositoryIdentity, branch string, assetPath string, content string, message string) (string, error) {
	body := map[string]any{
		"message": message,
		"content": base64.StdEncoding.EncodeToString([]byte(content)),
		"branch":  branch,
	}
	if sha, ok, err := s.currentContentSHA(ctx, repo, branch, assetPath); err != nil {
		return "", err
	} else if ok {
		body["sha"] = sha
	}
	status, respBody, err := s.doJSON(ctx, http.MethodPut, "/api/v3/repos/"+url.PathEscape(repo.Owner)+"/"+url.PathEscape(repo.Name)+"/contents/"+escapeAGSPath(assetPath), body)
	if err != nil {
		return "", fmt.Errorf("write role package asset %q: %w", assetPath, err)
	}
	if status < 200 || status >= 300 {
		return "", fmt.Errorf("write role package asset %q: status %d: %s", assetPath, status, strings.TrimSpace(string(respBody)))
	}
	var payload struct {
		Commit struct {
			SHA string `json:"sha"`
		} `json:"commit"`
	}
	if err := json.Unmarshal(respBody, &payload); err != nil {
		return "", fmt.Errorf("write role package asset %q: decode content response: %w", assetPath, err)
	}
	if strings.TrimSpace(payload.Commit.SHA) == "" {
		return "", fmt.Errorf("write role package asset %q: empty commit sha", assetPath)
	}
	return strings.TrimSpace(payload.Commit.SHA), nil
}

func (s AGSHTTPRolePackageContentStore) currentContentSHA(ctx context.Context, repo repos.RepositoryIdentity, branch string, assetPath string) (string, bool, error) {
	path := "/api/v3/repos/" + url.PathEscape(repo.Owner) + "/" + url.PathEscape(repo.Name) + "/contents/" + escapeAGSPath(assetPath) + "?ref=" + url.QueryEscape(branch)
	status, body, err := s.doJSON(ctx, http.MethodGet, path, nil)
	if err != nil {
		return "", false, fmt.Errorf("read role package asset %q: %w", assetPath, err)
	}
	if status == http.StatusNotFound {
		return "", false, nil
	}
	if status < 200 || status >= 300 {
		return "", false, fmt.Errorf("read role package asset %q: status %d: %s", assetPath, status, strings.TrimSpace(string(body)))
	}
	var payload struct {
		SHA string `json:"sha"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return "", false, fmt.Errorf("read role package asset %q: decode content: %w", assetPath, err)
	}
	return strings.TrimSpace(payload.SHA), strings.TrimSpace(payload.SHA) != "", nil
}

func (s AGSHTTPRolePackageContentStore) contentRef(repo repos.RepositoryIdentity, assetPath string, commitSHA string) string {
	baseURL := strings.TrimRight(strings.TrimSpace(s.BaseURL), "/")
	return baseURL + "/api/v3/repos/" + url.PathEscape(repo.Owner) + "/" + url.PathEscape(repo.Name) + "/contents/" + escapeAGSPath(assetPath) + "?ref=" + url.QueryEscape(commitSHA)
}

func (s AGSHTTPRolePackageContentStore) resolveRequestPath(ref string) (string, error) {
	trimmed := strings.TrimSpace(ref)
	if trimmed == "" {
		return "", fmt.Errorf("empty ref")
	}
	if strings.HasPrefix(trimmed, "/") {
		return trimmed, nil
	}
	parsed, err := url.Parse(trimmed)
	if err != nil {
		return "", err
	}
	if parsed.Scheme == "" && parsed.Host == "" && strings.HasPrefix(parsed.Path, "/") {
		if parsed.RawQuery == "" {
			return parsed.Path, nil
		}
		return parsed.Path + "?" + parsed.RawQuery, nil
	}
	if parsed.Scheme == "" || parsed.Host == "" {
		return "", fmt.Errorf("invalid content_ref")
	}
	if parsed.RawQuery == "" {
		return parsed.Path, nil
	}
	return parsed.Path + "?" + parsed.RawQuery, nil
}

func rolePackageBranchContentRef(ref string, versionID string) string {
	versionID = strings.TrimSpace(versionID)
	if versionID == "" {
		return ""
	}
	parsed, err := url.Parse(strings.TrimSpace(ref))
	if err != nil || parsed.Path == "" {
		return ""
	}
	if !strings.Contains(parsed.Path, ".rolepkg/contents/") {
		return ""
	}
	query := parsed.Query()
	query.Set("ref", rolePackageRepositoryBranch(versionID))
	parsed.RawQuery = query.Encode()
	return parsed.String()
}

func (s AGSHTTPRolePackageContentStore) doJSON(ctx context.Context, method string, path string, payload any) (int, []byte, error) {
	var body io.Reader
	if payload != nil {
		encoded, err := json.Marshal(payload)
		if err != nil {
			return 0, nil, err
		}
		body = bytes.NewReader(encoded)
	}
	if s.Handler != nil {
		request := httptest.NewRequest(method, path, body).WithContext(agsbridge.ContextWithSiteAdmin(ctx))
		if payload != nil {
			request.Header.Set("Content-Type", "application/json")
		}
		setRolePackageAGSAuthHeader(request.Header, s.AuthHeader, s.AuthorizationHeader)
		recorder := httptest.NewRecorder()
		s.Handler.ServeHTTP(recorder, request)
		return recorder.Code, recorder.Body.Bytes(), nil
	}
	baseURL := strings.TrimRight(strings.TrimSpace(s.BaseURL), "/")
	if baseURL == "" {
		return 0, nil, errRolePackageContentRequired()
	}
	fullURL := path
	if !strings.HasPrefix(fullURL, "http://") && !strings.HasPrefix(fullURL, "https://") {
		fullURL = baseURL + path
	}
	request, err := http.NewRequestWithContext(ctx, method, fullURL, body)
	if err != nil {
		return 0, nil, err
	}
	if payload != nil {
		request.Header.Set("Content-Type", "application/json")
	}
	setRolePackageAGSAuthHeader(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(response.Body)
	if err != nil {
		return 0, nil, err
	}
	return response.StatusCode, responseBody, nil
}

func (s AGSHTTPRolePackageContentStore) doJSONBounded(ctx context.Context, method string, path string, maxResponseBytes int64) (int, []byte, error) {
	if maxResponseBytes <= 0 {
		return 0, nil, fmt.Errorf("bounded AGS response limit is required")
	}
	if s.Handler != nil {
		request := httptest.NewRequest(method, path, nil).WithContext(agsbridge.ContextWithSiteAdmin(ctx))
		setRolePackageAGSAuthHeader(request.Header, s.AuthHeader, s.AuthorizationHeader)
		recorder := newBoundedRolePackageResponse(maxResponseBytes)
		s.Handler.ServeHTTP(recorder, request)
		return recorder.result()
	}
	baseURL := strings.TrimRight(strings.TrimSpace(s.BaseURL), "/")
	if baseURL == "" {
		return 0, nil, errRolePackageContentRequired()
	}
	fullURL := path
	if !strings.HasPrefix(fullURL, "http://") && !strings.HasPrefix(fullURL, "https://") {
		fullURL = baseURL + path
	}
	request, err := http.NewRequestWithContext(ctx, method, fullURL, nil)
	if err != nil {
		return 0, nil, err
	}
	setRolePackageAGSAuthHeader(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 byte limit")
	}
	return response.StatusCode, responseBody, nil
}

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

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

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

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

func (response *boundedRolePackageResponse) Write(data []byte) (int, error) {
	if response.status == 0 {
		response.status = http.StatusOK
	}
	remaining := response.limit + 1 - int64(response.body.Len())
	if remaining > 0 {
		keep := int64(len(data))
		if keep > remaining {
			keep = remaining
		}
		_, _ = response.body.Write(data[:keep])
	}
	if int64(response.body.Len()) > response.limit || int64(len(data)) > remaining {
		response.exceeded = true
	}
	return len(data), nil
}

func (response *boundedRolePackageResponse) result() (int, []byte, error) {
	if response.exceeded {
		return 0, nil, fmt.Errorf("AGS response exceeds byte limit")
	}
	status := response.status
	if status == 0 {
		status = http.StatusOK
	}
	return status, append([]byte(nil), response.body.Bytes()...), nil
}

func rolePackageDependencyError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, errRolePackageContentRequiredSentinel) ||
		errors.Is(err, repos.ErrInvalidArgument) {
		return err
	}
	return fmt.Errorf("%w: %w", errRolePackageDependencyUnavailable, err)
}

func setRolePackageAGSAuthHeader(headers http.Header, configuredHeader string, value string) {
	value = strings.TrimSpace(value)
	if value == "" {
		return
	}
	header := strings.TrimSpace(configuredHeader)
	if header == "" {
		header = "Authorization"
	}
	headers.Set(header, value)
}

func rolePackageRepositoryName(roleID string) string {
	name := sanitizedRolePackageAssetName(strings.TrimPrefix(strings.TrimSpace(roleID), "role_"), "role-package")
	name = strings.Trim(name, "-.")
	if name == "" {
		name = "role-package"
	}
	return name + ".rolepkg"
}

func rolePackageRepositoryBranch(versionID string) string {
	return rolePackageRepositoryBranchPrefix + sanitizedRolePackageAssetName(versionID, "published")
}

func rolePackageCommitMessage(versionID string, assetPath string) string {
	return fmt.Sprintf("Store role package %s asset %s", strings.TrimSpace(versionID), strings.TrimSpace(assetPath))
}

func rolePackageContentHash(content string) string {
	sum := sha256.Sum256([]byte(content))
	return hex.EncodeToString(sum[:])
}

func escapeAGSPath(value string) string {
	return strings.ReplaceAll(url.PathEscape(strings.TrimSpace(value)), "%2F", "/")
}

func removeASCIIWhitespace(value string) string {
	return strings.Map(func(r rune) rune {
		switch r {
		case ' ', '\t', '\n', '\r':
			return -1
		default:
			return r
		}
	}, value)
}
