// Package worker composes MCP product tools with published Runtime clients.
// It has no local token verifier, business permission store or CRM persistence.
package worker

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"sync/atomic"
	"time"

	"github.com/pingcap-inc/nexuscrm-app-packs/workers/mcp/internal/transport"
	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	query "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	mutation "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordmutationv1"
)

// Ports match the released Runtime clients. User bearers remain request-local;
// workload credentials are owned by the transport client, never MCP arguments.
type Ports struct {
	Authorize func(context.Context, string, []string) (context.Context, error)
	Query     func(context.Context, string, query.Request) (query.Response, error)
	Mutate    func(context.Context, string, mutation.Request) (mutation.Response, error)
	Status    func(context.Context, string, mutation.Request) (mutation.StatusResponse, error)
}

type Config struct {
	PublicOrigin   string
	Version        string
	RequestTimeout time.Duration
	Ports          Ports
}

type Worker struct {
	handler  http.Handler
	ports    Ports
	timeout  time.Duration
	draining atomic.Bool
}

type userBearerKey struct{}

func New(config Config) (*Worker, error) {
	if config.RequestTimeout <= 0 || config.Ports.Authorize == nil || config.Ports.Query == nil || config.Ports.Mutate == nil || config.Ports.Status == nil {
		return nil, errors.New("MCP Worker requires complete Runtime clients and request timeout")
	}
	w := &Worker{ports: config.Ports, timeout: config.RequestTimeout}
	handler, err := transport.NewHandler(transport.Config{
		PublicOrigin: config.PublicOrigin, ResourceURL: config.PublicOrigin + "/mcp",
		ServerName: "nexuscrm-mcp", ServerVersion: config.Version,
		AllowedOrigin: func(origin string) bool { return origin == config.PublicOrigin },
		Authorize: func(ctx context.Context, bearer string, toolName string) (context.Context, error) {
			// Discovery does not require write permission. Runtime API grants are
			// installation/workload contracts; object/FLS/RLS are checked on data RPCs.
			capability := query.CapabilityName
			if toolName == "crm_mutate_record" || toolName == "crm_mutation_status" {
				capability = mutation.CapabilityName
			}
			admitted, err := w.ports.Authorize(ctx, bearer, []string{capability})
			if err != nil {
				return nil, admissionError(err)
			}
			if admitted == nil {
				return nil, transport.Unavailable
			}
			return context.WithValue(admitted, userBearerKey{}, bearer), nil
		},
		Tools: toolCatalog(), Execute: w.execute,
	})
	if err != nil {
		return nil, err
	}
	w.handler = handler
	return w, nil
}

func (w *Worker) BeginDrain() { w.draining.Store(true) }

func (w *Worker) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	if w.draining.Load() {
		response.Header().Set("Cache-Control", "no-store")
		response.WriteHeader(http.StatusServiceUnavailable)
		return
	}
	ctx, cancel := context.WithTimeout(request.Context(), w.timeout)
	defer cancel()
	w.handler.ServeHTTP(response, request.WithContext(ctx))
}

func admissionError(err error) error {
	var failure *common.Failure
	if errors.As(err, &failure) {
		switch failure.Category {
		case common.FailureUnauthenticated:
			return transport.Unauthenticated
		case common.FailureForbidden:
			return transport.Forbidden
		case common.FailureResourceExhausted:
			return transport.RateLimited
		}
	}
	return transport.Unavailable
}

// Result emits only product data and caller-safe failure categories. Runtime
// receipts, delegation tokens and internal diagnostics are not public MCP data.
func result(value any, failed bool) (any, error) {
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}
	return map[string]any{"isError": failed, "content": []map[string]string{{"type": "text", "text": string(encoded)}}}, nil
}

func failureResult(err error, mutationAttempt bool) (any, error) {
	code := "UNAVAILABLE"
	var failure *common.Failure
	if errors.As(err, &failure) {
		switch failure.Category {
		case common.FailureUnauthenticated:
			code = "UNAUTHENTICATED"
		case common.FailureForbidden:
			code = "FORBIDDEN"
		case common.FailureMissing:
			code = "MISSING_OR_DENIED"
		case common.FailureInvalidRequest:
			code = "INVALID_ARGUMENTS"
		case common.FailureResourceExhausted:
			code = "RESOURCE_EXHAUSTED"
		}
	}
	value := map[string]any{"error": code}
	if mutationAttempt {
		// Do not report rollback from an ambiguous/canceled transport outcome.
		value["next_step"] = "Use crm_mutation_status with the same operation and idempotency_key before starting a new command."
	}
	return result(value, true)
}
