// Package ones registers the ONES Builtin Extension: the project management
// suite's Open API over per-connection host origins with dual-header token
// auth. Project resources live under /project/api/project and the Wiki
// knowledge base under /wiki/api/wiki on the same origin.
package ones

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/extension"
	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
)

const (
	// Domain is the Extension domain.
	Domain = "ones"
	// Revision is the implementation identity folded into the runtime digest.
	Revision = "ones-rest-v1"
	// BaseURLField is the connection credential carrying the host origin.
	BaseURLField = "base_url"
	// ProjectPrefix is the ONES project API path prefix on the host origin.
	ProjectPrefix = "/project/api/project"
	// WikiPrefix is the ONES wiki API path prefix on the same origin.
	WikiPrefix = "/wiki/api/wiki"
)

// Extension composes the ONES Builtin. It fails closed when the credential
// source or provider client template cannot be constructed.
func Extension(credentials runner.CredentialSource) (extension.Extension, error) {
	executor, err := runner.NewConnectionScopedRESTExecutor(
		Domain, runner.RESTTemplate{
			Auth:             AuthSpec(),
			BaseURLField:     BaseURLField,
			StatusClassifier: ClassifyStatus,
		},
		credentials,
		Handlers(),
	)
	if err != nil {
		return extension.Extension{}, fmt.Errorf("ones: build REST executor: %w", err)
	}
	return extension.Extension{
		Definition: Definition(),
		Implementation: runner.Implementation{
			Executor:            executor,
			Revision:            Revision,
			ConnectionPolicy:    registry.ConnectionRequired,
			Auth:                AuthSpec(),
			ValidateCredentials: ValidateCredentials,
		},
		Distribution: extension.Distribution{
			AppID:          "app_ones",
			AppSlug:        "ones",
			DefaultName:    Domain,
			DefaultEnabled: true,
			IconURL:        "/app-icons/ones.svg",
		},
	}, nil
}

// AuthSpec declares the ONES credential contract: the deployment host origin,
// the login user uuid, and the login token injected as two provider headers.
func AuthSpec() runner.AuthSpec {
	return runner.AuthSpec{
		Kind: runner.AuthHeaders,
		Fields: []runner.CredentialField{
			{
				Name:        BaseURLField,
				DisplayName: "ONES host origin",
				Description: "The https origin of the ONES deployment, for example https://ones.example.com.",
				Required:    true,
			},
			{
				Name:        "user_id",
				DisplayName: "ONES user uuid",
				Description: "The user uuid returned by auth/login, sent as the Ones-User-Id header.",
				Required:    true,
				HeaderName:  "Ones-User-Id",
			},
			{
				Name:        "token",
				DisplayName: "ONES auth token",
				Description: "The token returned by auth/login, sent as the Ones-Auth-Token header.",
				Required:    true,
				Secret:      true,
				HeaderName:  "Ones-Auth-Token",
			},
		},
	}
}

// ValidateCredentials live-checks the connection with one bounded token_info
// GET and requires a non-empty user identity.
func ValidateCredentials(ctx context.Context, credentials runner.Credentials) error {
	client, err := runner.BuildConnectionClient(runner.RESTTemplate{
		Auth:             AuthSpec(),
		BaseURLField:     BaseURLField,
		StatusClassifier: ClassifyStatus,
	}, credentials)
	if err != nil {
		return err
	}
	var info struct {
		User struct {
			UUID string `json:"uuid"`
		} `json:"user"`
	}
	if err := client.CallJSON(ctx, credentials, request(credentials, "GET",
		ProjectPrefix+"/auth/token_info", nil, nil), &info); err != nil {
		return err
	}
	if info.User.UUID == "" {
		return runner.Errorf(
			runner.ErrorProviderAuthUnavailable, "ones rejected the credentials", nil)
	}
	return nil
}

// ClassifyStatus maps ONES responses onto the closed runtime error set.
// ONES carries business semantics both in non-standard HTTP statuses (6xx/8xx)
// and in the bounded error body's errcode/type fields, so the classifier
// consults both: a recognized errcode/type wins, then the status table, then
// the shared default. Unrecognized codes never map to the auth class.
func ClassifyStatus(status int, body []byte) runner.ErrorCode {
	if code, ok := classifyErrorBody(body); ok {
		return code
	}
	switch status {
	case 401, 403, 802, 808, 813, 814:
		return runner.ErrorProviderAuthUnavailable
	case 404, 603, 609, 610, 620, 630:
		return runner.ErrorNotFound
	case 429:
		return runner.ErrorRateLimited
	case 400, 607, 611, 801, 817, 818, 819, 900, 901:
		return runner.ErrorValidation
	default:
		if status >= 500 && status < 600 {
			return runner.ErrorUpstreamFailure
		}
		if status >= 400 && status < 500 {
			return runner.ErrorValidation
		}
		return runner.ErrorUpstreamFailure
	}
}

// classifyErrorBody refines the classification with the upstream error body
// when ONES stamps an errcode or type (for example a 403 that is really a
// constraint violation or a resource limit, not a permission failure).
func classifyErrorBody(body []byte) (runner.ErrorCode, bool) {
	if len(body) == 0 {
		return "", false
	}
	var envelope struct {
		ErrCode string `json:"errcode"`
		Type    string `json:"type"`
	}
	if err := json.Unmarshal(body, &envelope); err != nil {
		return "", false
	}
	for _, marker := range [...]string{envelope.ErrCode, envelope.Type} {
		switch {
		case strings.HasPrefix(marker, "NotFound"):
			return runner.ErrorNotFound, true
		case strings.HasPrefix(marker, "PermissionDenied"):
			return runner.ErrorProviderAuthUnavailable, true
		case strings.HasPrefix(marker, "LimitExceeded"),
			strings.HasPrefix(marker, "ConstraintViolation"),
			strings.HasPrefix(marker, "VersionConflict"):
			return runner.ErrorValidation, true
		}
	}
	return "", false
}
