package feishubot

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"net/url"
	"strconv"
	"strings"
	"time"

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

// Handlers returns the Action dispatch table. The keys must exactly match the
// Definition catalog; bootstrap verifies the coverage.
func Handlers() map[string]runner.RESTHandler {
	return map[string]runner.RESTHandler{
		ActionSendTextMessage:      handleSendTextMessage,
		ActionSendPostMessage:      handleSendPostMessage,
		ActionSendImageMessage:     handleSendImageMessage,
		ActionSendShareChatMessage: handleSendShareChatMessage,
		ActionSendInteractiveCard:  handleSendInteractiveMessage,
	}
}

const (
	webhookPathPrefix      = "/open-apis/bot/v2/hook/"
	maxPayloadBytes        = 20 * 1024
	probeAcceptedCode      = 9499
	probeKeywordMissCode   = 19024
	rateLimitedCode        = 11232
	signInvalidCode        = 19021
	ipWhitelistCode        = 19022
	probeAcceptedMessage   = "params error, unknown content value"
	requestTimeoutFallback = "feishu custom bot request failed"
)

// callPhase selects the error classification contract: validate blames the
// credential for any structured rejection, execute keeps the upstream
// business-code semantics.
type callPhase string

const (
	phaseValidate callPhase = "validate"
	phaseExecute  callPhase = "execute"
)

// flexInt decodes the tolerant Feishu integer: a JSON number or a numeric
// string, anything else decoding to nil.
type flexInt struct {
	Value   int64
	Present bool
}

func (value *flexInt) UnmarshalJSON(raw []byte) error {
	trimmed := strings.TrimSpace(string(raw))
	if trimmed == "null" {
		return nil
	}
	digits := strings.Trim(trimmed, `"`)
	if digits == "" {
		return nil
	}
	parsed, err := strconv.ParseInt(digits, 10, 64)
	if err != nil {
		return nil
	}
	value.Value, value.Present = parsed, true
	return nil
}

// feishuEnvelope is the normalized Feishu webhook response. data defaults to
// an empty object exactly like the upstream normalizer.
type feishuEnvelope struct {
	Code          flexInt         `json:"code"`
	Msg           string          `json:"msg"`
	Data          json.RawMessage `json:"data"`
	StatusCode    flexInt         `json:"StatusCode"`
	StatusMessage string          `json:"StatusMessage"`
}

func decodeInput(input json.RawMessage, target any) error {
	if len(input) == 0 {
		return nil
	}
	if err := json.Unmarshal(input, target); err != nil {
		return runner.Errorf(
			runner.ErrorValidation, "action input was not decodable", err)
	}
	return nil
}

func handleSendTextMessage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed struct {
		Text string `json:"text"`
	}
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	return sendWebhookMessage(ctx, client, credentials, map[string]any{
		"msg_type": "text",
		"content":  map[string]any{"text": parsed.Text},
	})
}

func handleSendPostMessage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed struct {
		Post json.RawMessage `json:"post"`
	}
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	return sendWebhookMessage(ctx, client, credentials, map[string]any{
		"msg_type": "post",
		"content":  map[string]any{"post": parsed.Post},
	})
}

func handleSendImageMessage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed struct {
		ImageKey string `json:"imageKey"`
	}
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	return sendWebhookMessage(ctx, client, credentials, map[string]any{
		"msg_type": "image",
		"content":  map[string]any{"image_key": parsed.ImageKey},
	})
}

func handleSendShareChatMessage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed struct {
		ShareChatID string `json:"shareChatId"`
	}
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	return sendWebhookMessage(ctx, client, credentials, map[string]any{
		"msg_type": "share_chat",
		"content":  map[string]any{"share_chat_id": parsed.ShareChatID},
	})
}

func handleSendInteractiveMessage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed struct {
		Card json.RawMessage `json:"card"`
	}
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	return sendWebhookMessage(ctx, client, credentials, map[string]any{
		"msg_type": "interactive",
		"card":     parsed.Card,
	})
}

// sendWebhookMessage executes one signed webhook POST and enforces the
// upstream success contract (code == 0).
func sendWebhookMessage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	payload map[string]any,
) (json.RawMessage, error) {
	envelope, raw, err := callWebhook(ctx, client, credentials, payload)
	if err != nil {
		return nil, err
	}
	if envelope == nil {
		return nil, classifyMalformedBody()
	}
	if !envelope.Code.Present || envelope.Code.Value != 0 {
		return nil, classifyEnvelope(envelope, raw, phaseExecute)
	}
	output := map[string]any{
		"code": envelope.Code.Value,
		"msg":  envelope.Msg,
		"data": rawObjectOrEmpty(envelope.Data),
	}
	if envelope.StatusCode.Present {
		output["statusCode"] = envelope.StatusCode.Value
	}
	if envelope.StatusMessage != "" {
		output["statusMessage"] = envelope.StatusMessage
	}
	return json.Marshal(output)
}

// callWebhook performs the shared webhook round trip: resolve the token,
// stamp the signature when configured, bound the payload, and classify
// failures for the validation probe path.
func callWebhook(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	payload map[string]any,
) (*feishuEnvelope, json.RawMessage, error) {
	token, err := resolveWebhookToken(credentials[APIKeyField])
	if err != nil {
		if isValidateProbe(payload) {
			return nil, nil, err
		}
		// Outside the probe the credential comes from the stored
		// connection: an unresolvable stored value is a connection
		// problem, not an action-input validation failure.
		return nil, nil, runner.Errorf(
			runner.ErrorConnectionUnavailable,
			"stored provider credential is unusable", err)
	}
	requestBody := payload
	if secret := strings.TrimSpace(credentials[SigningSecretField]); secret != "" {
		timestamp := strconv.FormatInt(time.Now().Unix(), 10)
		signed := map[string]any{
			"timestamp": timestamp,
			"sign":      buildSignature(timestamp, secret),
		}
		for key, value := range payload {
			signed[key] = value
		}
		requestBody = signed
	}
	encoded, err := json.Marshal(requestBody)
	if err != nil {
		return nil, nil, runner.Errorf(
			runner.ErrorUpstreamFailure, "provider request was not encodable", err)
	}
	if len(encoded) > maxPayloadBytes {
		return nil, nil, runner.Errorf(
			runner.ErrorValidation,
			"feishu custom bot request body must not exceed 20 KB", nil)
	}
	raw, callErr := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   webhookPathPrefix + token,
		Body:   json.RawMessage(encoded),
	})
	if callErr != nil {
		code := runner.CodeOf(callErr)
		// Rate limits and transport/decode failures keep their own class in
		// every phase; a rejected request during the validation probe blames
		// the credential, exactly like the upstream validate mapping.
		if code == runner.ErrorRateLimited || code == runner.ErrorUpstreamFailure {
			return nil, nil, callErr
		}
		if isValidateProbe(payload) {
			return nil, nil, runner.Errorf(
				runner.ErrorProviderAuthUnavailable,
				"feishu rejected the webhook credential", callErr)
		}
		return nil, nil, callErr
	}
	envelope, parseErr := parseEnvelope(raw)
	if parseErr != nil {
		return nil, raw, parseErr
	}
	if isValidateProbe(payload) && !probeAccepted(envelope) {
		return nil, raw, classifyEnvelope(envelope, raw, phaseValidate)
	}
	return envelope, raw, nil
}

func isValidateProbe(payload map[string]any) bool {
	return payload["msg_type"] == "__validation_probe__"
}

func probeAccepted(envelope *feishuEnvelope) bool {
	if envelope == nil {
		return false
	}
	if envelope.Code.Present {
		if envelope.Code.Value == probeAcceptedCode ||
			envelope.Code.Value == probeKeywordMissCode {
			return true
		}
	}
	return strings.TrimSpace(strings.ToLower(envelope.Msg)) == probeAcceptedMessage
}

// parseEnvelope decodes the tolerant Feishu envelope: a JSON object with a
// number-or-numeric-string code. A body that is not a JSON object fails
// closed.
func parseEnvelope(raw json.RawMessage) (*feishuEnvelope, error) {
	trimmed := strings.TrimSpace(string(raw))
	if trimmed == "" || trimmed == "null" {
		return nil, nil
	}
	if trimmed[0] != '{' {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure, "provider response was not an object", nil)
	}
	envelope := &feishuEnvelope{}
	if err := json.Unmarshal(raw, envelope); err != nil {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure, "provider response was not decodable", err)
	}
	return envelope, nil
}

// classifyEnvelope maps one structured Feishu rejection onto the closed
// runtime set, keeping the upstream business-code semantics: 11232 rate
// limits; 19021 signature/timestamp and 19022 IP-whitelist rejections blame
// the connection in every phase; 19024 keyword misses are the only
// action-input validation; unknown or absent codes fail closed as upstream
// failures instead of masquerading as input problems.
func classifyEnvelope(envelope *feishuEnvelope, raw json.RawMessage, phase callPhase) error {
	if envelope == nil {
		// An empty or null 2xx body carries no structured verdict for the
		// probe to read; fail closed as a protocol anomaly instead of
		// dereferencing a missing envelope.
		return runner.Errorf(
			runner.ErrorUpstreamFailure, "provider response was empty", nil)
	}
	message := strings.TrimSpace(envelopeMessage(envelope, raw))
	if envelope.Code.Present {
		switch envelope.Code.Value {
		case rateLimitedCode:
			return runner.Errorf(runner.ErrorRateLimited, message, nil)
		case signInvalidCode, ipWhitelistCode:
			return runner.Errorf(
				runner.ErrorProviderAuthUnavailable, message, nil)
		case probeKeywordMissCode:
			if phase == phaseValidate {
				return runner.Errorf(
					runner.ErrorProviderAuthUnavailable, message, nil)
			}
			return runner.Errorf(runner.ErrorValidation, message, nil)
		}
	}
	if phase == phaseValidate {
		return runner.Errorf(
			runner.ErrorProviderAuthUnavailable,
			"feishu rejected the webhook credential", nil)
	}
	return runner.Errorf(runner.ErrorUpstreamFailure, message, nil)
}

func classifyMalformedBody() error {
	return runner.Errorf(
		runner.ErrorUpstreamFailure, "provider response was not decodable", nil)
}

func envelopeMessage(envelope *feishuEnvelope, raw json.RawMessage) string {
	if envelope != nil {
		if envelope.Msg != "" {
			return envelope.Msg
		}
		if envelope.StatusMessage != "" {
			return envelope.StatusMessage
		}
	}
	fallback := strings.TrimSpace(string(raw))
	if fallback != "" {
		if unquoted, err := strconv.Unquote(fallback); err == nil {
			return unquoted
		}
		return fallback
	}
	return requestTimeoutFallback
}

func rawObjectOrEmpty(raw json.RawMessage) map[string]any {
	trimmed := strings.TrimSpace(string(raw))
	if trimmed == "" || trimmed == "null" || trimmed[0] != '{' {
		return map[string]any{}
	}
	value := map[string]any{}
	if err := json.Unmarshal(raw, &value); err != nil {
		return map[string]any{}
	}
	return value
}

// buildSignature stamps the Feishu custom-bot signature: base64 of
// HMAC-SHA256 keyed by "<timestamp>\n<secret>" over an empty message.
func buildSignature(timestamp, secret string) string {
	mac := hmac.New(sha256.New, []byte(timestamp+"\n"+secret))
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

// resolveWebhookToken accepts the bare webhook token or the full official
// webhook URL, rejecting any other origin or shape.
func resolveWebhookToken(apiKey string) (string, error) {
	trimmed := strings.TrimSpace(apiKey)
	if trimmed == "" {
		return "", runner.Errorf(
			runner.ErrorValidation, "credential field \"apiKey\" is empty", nil)
	}
	if !strings.Contains(trimmed, "://") {
		if strings.Contains(trimmed, "/") {
			return "", runner.Errorf(
				runner.ErrorValidation,
				"apiKey must be a Feishu webhook token or webhook URL", nil)
		}
		return trimmed, nil
	}
	parsed, err := url.Parse(trimmed)
	if err != nil {
		return "", runner.Errorf(
			runner.ErrorValidation, "webhook URL must be a valid URL", nil)
	}
	if parsed.Scheme != "https" {
		return "", runner.Errorf(
			runner.ErrorValidation, "webhook URL must use https", nil)
	}
	if parsed.Host != "open.feishu.cn" {
		return "", runner.Errorf(
			runner.ErrorValidation, "webhook URL must use an official Feishu webhook host", nil)
	}
	if !strings.HasPrefix(parsed.EscapedPath(), webhookPathPrefix) {
		return "", runner.Errorf(
			runner.ErrorValidation, "webhook URL must be a Feishu custom bot webhook URL", nil)
	}
	token := strings.TrimPrefix(parsed.EscapedPath(), webhookPathPrefix)
	if token == "" || strings.Contains(token, "/") {
		return "", runner.Errorf(
			runner.ErrorValidation, "webhook URL must include a valid Feishu custom bot token", nil)
	}
	return token, nil
}
