package dingtalkrobot

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// Config 保存钉钉机器人请求所需的敏感参数；调用方必须从运行环境注入真实 webhook。
type Config struct {
	WebhookURL     string
	Secret         string
	AtMobiles      []string
	AtAll          bool
	RequestTimeout time.Duration
}

// MarkdownMessage 是当前财务通知使用的唯一消息格式，避免业务层拼钉钉专属 JSON。
type MarkdownMessage struct {
	Title string
	Text  string
}

// Client 封装钉钉机器人加签、HTTP 请求和返回码校验，gateway/admin 复用同一套边界。
type Client struct {
	webhookURL string
	secret     string
	atMobiles  []string
	atAll      bool
	httpClient *http.Client
	now        func() time.Time
}

// New 创建钉钉机器人客户端；httpClient 仅用于测试或特殊运行时注入。
func New(config Config, httpClient *http.Client) (*Client, error) {
	webhookURL := strings.TrimSpace(config.WebhookURL)
	if webhookURL == "" {
		return nil, errors.New("dingtalk webhook url is required")
	}
	if _, err := url.ParseRequestURI(webhookURL); err != nil {
		return nil, fmt.Errorf("dingtalk webhook url is invalid: %w", err)
	}
	timeout := config.RequestTimeout
	if timeout <= 0 {
		timeout = 3 * time.Second
	}
	if httpClient == nil {
		httpClient = &http.Client{Timeout: timeout}
	}
	return &Client{
		webhookURL: webhookURL,
		secret:     strings.TrimSpace(config.Secret),
		atMobiles:  compactStrings(config.AtMobiles),
		atAll:      config.AtAll,
		httpClient: httpClient,
		now:        func() time.Time { return time.Now().UTC() },
	}, nil
}

// SendMarkdown 发送 markdown 消息；钉钉业务错误码也视为失败，调用方决定是否回滚业务。
func (c *Client) SendMarkdown(ctx context.Context, message MarkdownMessage) error {
	if c == nil || c.httpClient == nil {
		return errors.New("dingtalk client is not configured")
	}
	title := strings.TrimSpace(message.Title)
	text := strings.TrimSpace(message.Text)
	if title == "" || text == "" {
		return errors.New("dingtalk markdown title and text are required")
	}
	body, err := json.Marshal(markdownPayload{
		MsgType: "markdown",
		Markdown: markdownBody{
			Title: title,
			Text:  text,
		},
		At: atBody{
			AtMobiles: c.atMobiles,
			IsAtAll:   c.atAll,
		},
	})
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.signedWebhookURL(), bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json; charset=utf-8")
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	payload, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
	if err != nil {
		return err
	}
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		return fmt.Errorf("dingtalk returned http %d: %s", resp.StatusCode, strings.TrimSpace(string(payload)))
	}
	var result sendResponse
	if err := json.Unmarshal(payload, &result); err != nil {
		return fmt.Errorf("decode dingtalk response: %w", err)
	}
	if result.ErrCode != 0 {
		return fmt.Errorf("dingtalk returned errcode=%d errmsg=%s", result.ErrCode, result.ErrMsg)
	}
	return nil
}

func (c *Client) signedWebhookURL() string {
	if c.secret == "" {
		return c.webhookURL
	}
	parsed, err := url.Parse(c.webhookURL)
	if err != nil {
		return c.webhookURL
	}
	timestamp := c.now().UnixMilli()
	query := parsed.Query()
	query.Set("timestamp", fmt.Sprintf("%d", timestamp))
	query.Set("sign", sign(timestamp, c.secret))
	parsed.RawQuery = query.Encode()
	return parsed.String()
}

func sign(timestamp int64, secret string) string {
	// 钉钉加签要求 timestamp + "\n" + secret 作为明文，secret 同时作为 HMAC key。
	payload := fmt.Sprintf("%d\n%s", timestamp, secret)
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write([]byte(payload))
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

func compactStrings(values []string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			out = append(out, trimmed)
		}
	}
	return out
}

type markdownPayload struct {
	MsgType  string       `json:"msgtype"`
	Markdown markdownBody `json:"markdown"`
	At       atBody       `json:"at"`
}

type markdownBody struct {
	Title string `json:"title"`
	Text  string `json:"text"`
}

type atBody struct {
	AtMobiles []string `json:"atMobiles"`
	IsAtAll   bool     `json:"isAtAll"`
}

type sendResponse struct {
	ErrCode int    `json:"errcode"`
	ErrMsg  string `json:"errmsg"`
}
