// Package notify 内的 notifier_im.go 实现 IMNotifier：以通用 webhook POST
// （Slack incoming-webhook 兼容形态）把通知投递到用户配置的 IM 地址。
//
// 投递地址是 per-user 的 im_webhook_url，以密文落库于 notification_prefs
// （im_webhook_url_enc）；发送时经 Decryptor 解密，明文 URL 不在领域内存层
// 长期驻留。投递前置条件与 EmailNotifier 对称：
//   - 全局 cfg.Enabled 为 true；
//   - 用户 im_enabled 为 true 且配置了 webhook URL；
//   - 消息 severity 达到用户 min_severity。
//
// 任一不满足静默返回 nil；HTTP/解密失败返回错误（Dispatcher 仅记日志）。
package notify

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strings"
)

// IMConfig 是 IMNotifier 的全局配置，对应 cfg.Notify.IM。
type IMConfig struct {
	Enabled bool
}

// Decryptor 抽象 im_webhook_url 密文的解密。由 crypto.Encryptor 满足
// （Decrypt([]byte) ([]byte, error)），以窄接口注入避免 notify 依赖 crypto 包。
type Decryptor interface {
	Decrypt(ciphertext []byte) ([]byte, error)
}

// httpDoer 抽象单次 HTTP 调用，便于测试注入 fake transport。
type httpDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

// IMNotifier 通过 webhook POST 投递 IM 消息，实现 Notifier。
type IMNotifier struct {
	cfg   IMConfig
	prefs PrefsRepository
	dec   Decryptor
	cl    httpDoer
	log   *slog.Logger
}

// NewIMNotifier 构造 IMNotifier。cl 为 nil 时用默认 http.Client（含 30s 超时
// 由 Dispatcher 的 sendCtx 兜底）；log 为 nil 时回落 slog.Default。
func NewIMNotifier(cfg IMConfig, prefs PrefsRepository, dec Decryptor, cl httpDoer, log *slog.Logger) *IMNotifier {
	if cl == nil {
		cl = http.DefaultClient
	}
	if log == nil {
		log = slog.Default()
	}
	return &IMNotifier{cfg: cfg, prefs: prefs, dec: dec, cl: cl, log: log}
}

// Name 返回 ChannelIM。
func (n *IMNotifier) Name() Channel { return ChannelIM }

// Send 在满足全部前置条件时向用户的 IM webhook POST 一条消息。
func (n *IMNotifier) Send(ctx context.Context, msg Message) error {
	if !n.cfg.Enabled {
		return nil
	}
	p, err := n.prefs.Get(ctx, msg.UserID)
	if err != nil {
		return err
	}
	if !p.IMEnabled || len(p.ImWebhookURLEnc) == 0 {
		return nil
	}
	if !meetsMinSeverity(msg.Severity, p.MinSeverity) {
		return nil
	}
	urlBytes, err := n.dec.Decrypt(p.ImWebhookURLEnc)
	if err != nil {
		return fmt.Errorf("decrypt im webhook url: %w", err)
	}
	url := strings.TrimSpace(string(urlBytes))
	if url == "" {
		return nil
	}

	payload, err := json.Marshal(buildIMPayload(msg))
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := n.cl.Do(req)
	if err != nil {
		return fmt.Errorf("im webhook post: %w", err)
	}
	defer resp.Body.Close()
	_, _ = io.Copy(io.Discard, resp.Body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("im webhook status %d", resp.StatusCode)
	}
	return nil
}

// buildIMPayload 把 Message 渲染为 Slack incoming-webhook 兼容的 JSON 体。
// 主要字段是 text（含级别、标题、正文、可选链接）；附加结构化字段方便富客户端。
func buildIMPayload(msg Message) map[string]any {
	title := msg.Title
	if title == "" {
		title = msg.Topic
	}
	var b strings.Builder
	fmt.Fprintf(&b, "[%s] %s", strings.ToUpper(string(msg.Severity)), title)
	if msg.Body != "" {
		fmt.Fprintf(&b, "\n%s", msg.Body)
	}
	if msg.Link != "" {
		fmt.Fprintf(&b, "\n%s", msg.Link)
	}
	return map[string]any{
		"text":     b.String(),
		"topic":    msg.Topic,
		"severity": string(msg.Severity),
		"title":    title,
	}
}
