package withdrawalsource

import (
	"bytes"
	"context"
	"crypto/subtle"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"

	"hyapp-admin-server/internal/config"
)

const maxErrorBodyBytes = 2048

type DecisionRequest struct {
	SourceApplicationID string   `json:"sourceApplicationId"`
	Decision            string   `json:"decision"`
	CredentialURLs      []string `json:"credentialUrls"`
	Remark              string   `json:"remark"`
	ReviewerUserID      uint     `json:"reviewerUserId"`
	ReviewerName        string   `json:"reviewerName"`
	CommandID           string   `json:"commandId"`
}

type Registry struct {
	sources map[string]source
}

type source struct {
	config config.WithdrawalSourceConfig
	client *http.Client
}

func New(configs []config.WithdrawalSourceConfig) *Registry {
	registry := &Registry{sources: make(map[string]source)}
	for _, item := range configs {
		if !item.Enabled {
			continue
		}
		key := sourceKey(item.AppCode, item.SourceSystem)
		registry.sources[key] = source{
			config: item,
			client: &http.Client{Timeout: item.RequestTimeout},
		}
	}
	return registry
}

// AuthenticateSubmit 对 app_code + source_system 组合做独立令牌校验。
// 常量时间比较避免从鉴权耗时侧信道逐字符猜测资金接口令牌。
func (r *Registry) AuthenticateSubmit(appCode string, sourceSystem string, authorization string) bool {
	item, ok := r.lookup(appCode, sourceSystem)
	if !ok {
		return false
	}
	authorization = strings.TrimSpace(authorization)
	if !strings.HasPrefix(authorization, "Bearer ") {
		return false
	}
	provided := strings.TrimSpace(strings.TrimPrefix(authorization, "Bearer "))
	expected := strings.TrimSpace(item.config.SubmitToken)
	return provided != "" && len(provided) == len(expected) && subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) == 1
}

func (r *Registry) HasSource(appCode string, sourceSystem string) bool {
	_, ok := r.lookup(appCode, sourceSystem)
	return ok
}

// ApplyDecision 只负责把终态回调来源钱包；来源端必须按 commandId 幂等。
// admin 本地状态只有在 2xx 后才会提交，因此任何网络错误都会保留可重试的审核状态。
func (r *Registry) ApplyDecision(ctx context.Context, appCode string, sourceSystem string, request DecisionRequest) (string, error) {
	item, ok := r.lookup(appCode, sourceSystem)
	if !ok {
		return "", errors.New("withdrawal source is not configured")
	}
	body, err := json.Marshal(request)
	if err != nil {
		return "", fmt.Errorf("encode withdrawal source decision: %w", err)
	}
	httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, item.config.CallbackURL, bytes.NewReader(body))
	if err != nil {
		return "", fmt.Errorf("build withdrawal source decision: %w", err)
	}
	httpRequest.Header.Set("Authorization", "Bearer "+item.config.CallbackToken)
	httpRequest.Header.Set("Content-Type", "application/json")
	response, err := item.client.Do(httpRequest)
	if err != nil {
		return "", fmt.Errorf("call withdrawal source decision: %w", err)
	}
	defer response.Body.Close()
	if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
		message, _ := io.ReadAll(io.LimitReader(response.Body, maxErrorBodyBytes))
		return "", fmt.Errorf("withdrawal source decision returned %d: %s", response.StatusCode, strings.TrimSpace(string(message)))
	}
	// 来源申请号已单独持久化；阶段 commandId 作为短事务号即可关联 admin 审核与来源幂等执行，且不会突破字段长度上限。
	return "external:" + strings.TrimSpace(request.CommandID), nil
}

func (r *Registry) lookup(appCode string, sourceSystem string) (source, bool) {
	if r == nil {
		return source{}, false
	}
	item, ok := r.sources[sourceKey(appCode, sourceSystem)]
	return item, ok
}

func sourceKey(appCode string, sourceSystem string) string {
	return strings.ToLower(strings.TrimSpace(appCode)) + "\x00" + strings.ToLower(strings.TrimSpace(sourceSystem))
}
