package wecom

import (
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha1"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"log/slog"
	"mime"
	"mime/multipart"
	"net/http"
	"net/url"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/chenhg5/cc-connect/core"
)

func init() {
	core.RegisterPlatform("wecom", New)
}

// Incoming XML envelope from WeChat Work callback.
type xmlEncryptedMsg struct {
	XMLName    xml.Name `xml:"xml"`
	ToUserName string   `xml:"ToUserName"`
	AgentID    string   `xml:"AgentID"`
	Encrypt    string   `xml:"Encrypt"`
}

// Decrypted message body.
type xmlMessage struct {
	XMLName      xml.Name `xml:"xml"`
	ToUserName   string   `xml:"ToUserName"`
	FromUserName string   `xml:"FromUserName"`
	CreateTime   int64    `xml:"CreateTime"`
	MsgType      string   `xml:"MsgType"`
	Content      string   `xml:"Content"`
	PicUrl       string   `xml:"PicUrl"`
	MediaId      string   `xml:"MediaId"`
	FileName     string   `xml:"FileName"` // inbound file messages (MsgType=file)
	Format       string   `xml:"Format"`   // voice format: amr, speex, etc.
	MsgId        int64    `xml:"MsgId"`
	AgentID      int64    `xml:"AgentID"`
}

type replyContext struct {
	userID string
}

type tokenCache struct {
	mu        sync.Mutex
	token     string
	expiresAt time.Time
}

type Platform struct {
	corpID         string
	corpSecret     string
	agentID        string
	apiBaseURL     string
	allowFrom      string
	token          string // callback verification token
	aesKey         []byte // decoded EncodingAESKey (32 bytes)
	port           string
	callbackPath   string
	enableMarkdown bool
	server         *http.Server
	handler        core.MessageHandler
	apiClient      *http.Client // HTTP client for outbound API calls (may use proxy)
	tokenCache     tokenCache
	dedup          msgDedup
	userNameCache  sync.Map // userID -> display name
}

const defaultAPIBaseURL = "https://qyapi.weixin.qq.com"

// msgDedup tracks recently processed MsgIds to avoid WeChat Work retry duplicates.
type msgDedup struct {
	mu   sync.Mutex
	seen map[int64]time.Time
}

func (d *msgDedup) isDuplicate(msgID int64) bool {
	if msgID == 0 {
		return false
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.seen == nil {
		d.seen = make(map[int64]time.Time)
	}
	// Evict old entries (older than 60s)
	now := time.Now()
	for k, t := range d.seen {
		if now.Sub(t) > 60*time.Second {
			delete(d.seen, k)
		}
	}
	if _, exists := d.seen[msgID]; exists {
		return true
	}
	d.seen[msgID] = now
	return false
}

func New(opts map[string]any) (core.Platform, error) {
	mode, _ := opts["mode"].(string)
	if mode == "websocket" {
		return newWebSocket(opts)
	}

	corpID, _ := opts["corp_id"].(string)
	corpSecret, _ := opts["corp_secret"].(string)
	agentID, _ := opts["agent_id"].(string)
	callbackToken, _ := opts["callback_token"].(string)
	callbackAESKey, _ := opts["callback_aes_key"].(string)

	if corpID == "" || corpSecret == "" || agentID == "" {
		return nil, fmt.Errorf("wecom: corp_id, corp_secret, and agent_id are required")
	}
	if callbackToken == "" || callbackAESKey == "" {
		return nil, fmt.Errorf("wecom: callback_token and callback_aes_key are required")
	}

	aesKey, err := decodeAESKey(callbackAESKey)
	if err != nil {
		return nil, fmt.Errorf("wecom: invalid callback_aes_key: %w", err)
	}

	port, _ := opts["port"].(string)
	if port == "" {
		port = "8081"
	}
	path, _ := opts["callback_path"].(string)
	if path == "" {
		path = "/wecom/callback"
	}
	apiBaseURL, _ := opts["api_base_url"].(string)
	apiBaseURL = strings.TrimRight(strings.TrimSpace(apiBaseURL), "/")
	if apiBaseURL == "" {
		apiBaseURL = defaultAPIBaseURL
	} else {
		parsed, err := url.Parse(apiBaseURL)
		if err != nil || (parsed.Scheme != "https" && parsed.Scheme != "http") || parsed.Host == "" {
			return nil, fmt.Errorf("wecom: invalid api_base_url %q: must be a valid http(s) URL", apiBaseURL)
		}
	}

	transport := &http.Transport{
		MaxIdleConns:        2,
		MaxIdleConnsPerHost: 1,
		IdleConnTimeout:     10 * time.Second,
	}
	if proxyURL, _ := opts["proxy"].(string); proxyURL != "" {
		u, err := url.Parse(proxyURL)
		if err != nil {
			return nil, fmt.Errorf("wecom: invalid proxy URL %q: %w", proxyURL, err)
		}
		proxyUser, _ := opts["proxy_username"].(string)
		proxyPass, _ := opts["proxy_password"].(string)
		if proxyUser != "" {
			u.User = url.UserPassword(proxyUser, proxyPass)
		}
		transport.Proxy = http.ProxyURL(u)
		transport.DisableKeepAlives = true // prevent CONNECT tunnel accumulation on proxy
		slog.Info("wecom: outbound API requests will use proxy (keep-alive disabled)", "proxy", u.Host, "auth", proxyUser != "")
	}
	apiClient := &http.Client{Timeout: 30 * time.Second, Transport: transport}

	enableMarkdown, _ := opts["enable_markdown"].(bool)
	allowFrom, _ := opts["allow_from"].(string)
	core.CheckAllowFrom("wecom", allowFrom)

	return &Platform{
		corpID:         corpID,
		corpSecret:     corpSecret,
		agentID:        agentID,
		apiBaseURL:     apiBaseURL,
		allowFrom:      allowFrom,
		token:          callbackToken,
		aesKey:         aesKey,
		port:           port,
		callbackPath:   path,
		enableMarkdown: enableMarkdown,
		apiClient:      apiClient,
	}, nil
}

func (p *Platform) Name() string { return "wecom" }

func (p *Platform) wecomAPIURL(path string, query url.Values) string {
	base := strings.TrimRight(strings.TrimSpace(p.apiBaseURL), "/")
	if base == "" {
		base = defaultAPIBaseURL
	}
	u := base + path
	if len(query) == 0 {
		return u
	}
	return u + "?" + query.Encode()
}

func (p *Platform) Start(handler core.MessageHandler) error {
	p.handler = handler

	mux := http.NewServeMux()
	mux.HandleFunc(p.callbackPath, p.callbackHandler)

	p.server = &http.Server{
		Addr:    ":" + p.port,
		Handler: mux,
	}

	go func() {
		slog.Info("wecom: webhook server listening", "port", p.port, "path", p.callbackPath)
		if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			slog.Error("wecom: server error", "error", err)
		}
	}()

	return nil
}

func (p *Platform) callbackHandler(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	msgSignature := q.Get("msg_signature")
	timestamp := q.Get("timestamp")
	nonce := q.Get("nonce")

	if r.Method == http.MethodGet {
		p.handleVerify(w, msgSignature, timestamp, nonce, q.Get("echostr"))
		return
	}

	if r.Method == http.MethodPost {
		p.handleMessage(w, r, msgSignature, timestamp, nonce)
		return
	}

	w.WriteHeader(http.StatusMethodNotAllowed)
}

// handleVerify handles the one-time URL verification from WeChat Work.
func (p *Platform) handleVerify(w http.ResponseWriter, msgSig, timestamp, nonce, echostr string) {
	if !p.verifySignature(msgSig, timestamp, nonce, echostr) {
		slog.Warn("wecom: verify signature failed")
		w.WriteHeader(http.StatusForbidden)
		return
	}

	plain, err := p.decrypt(echostr)
	if err != nil {
		slog.Error("wecom: decrypt echostr failed", "error", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	slog.Info("wecom: URL verification succeeded")
	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, plain)
}

// wecomLogXMLPreview returns a short prefix of XML for debug only (may contain user content).
func wecomLogXMLPreview(s string, max int) string {
	if max <= 0 || len(s) <= max {
		return s
	}
	return s[:max] + "…"
}

// handleMessage processes incoming encrypted message POSTs.
func (p *Platform) handleMessage(w http.ResponseWriter, r *http.Request, msgSig, timestamp, nonce string) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		slog.Warn("wecom: read callback body failed", "error", err)
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	slog.Info("wecom: callback POST received",
		"body_bytes", len(body),
		"content_length", r.ContentLength,
		"has_msg_signature", msgSig != "",
		"has_timestamp", timestamp != "",
		"has_nonce", nonce != "")
	if len(body) == 0 {
		slog.Warn("wecom: empty callback POST body")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	var encMsg xmlEncryptedMsg
	if err := xml.Unmarshal(body, &encMsg); err != nil {
		slog.Error("wecom: parse outer xml failed", "error", err, "body_bytes", len(body))
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	if !p.verifySignature(msgSig, timestamp, nonce, encMsg.Encrypt) {
		slog.Warn("wecom: message signature verification failed")
		w.WriteHeader(http.StatusForbidden)
		return
	}

	plainXML, err := p.decrypt(encMsg.Encrypt)
	if err != nil {
		slog.Error("wecom: decrypt message failed", "error", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	slog.Debug("wecom: decrypted xml preview", "preview", wecomLogXMLPreview(plainXML, 512))

	// Return 200 immediately (WeChat Work requires response within 5 seconds)
	w.WriteHeader(http.StatusOK)

	var msg xmlMessage
	if err := xml.Unmarshal([]byte(plainXML), &msg); err != nil {
		slog.Error("wecom: parse decrypted xml failed", "error", err,
			"plain_len", len(plainXML),
			"preview", wecomLogXMLPreview(plainXML, 256))
		return
	}

	slog.Info("wecom: inbound parsed",
		"msg_type", msg.MsgType,
		"msg_id", msg.MsgId,
		"from_user", msg.FromUserName,
		"create_time", msg.CreateTime,
		"has_media_id", msg.MediaId != "",
		"file_name", msg.FileName)

	if p.dedup.isDuplicate(msg.MsgId) {
		slog.Info("wecom: dropping duplicate message", "msg_id", msg.MsgId, "msg_type", msg.MsgType)
		return
	}

	if msg.CreateTime > 0 {
		if core.IsOldMessage(time.Unix(msg.CreateTime, 0)) {
			slog.Info("wecom: ignoring old message after restart", "create_time", msg.CreateTime, "msg_type", msg.MsgType)
			return
		}
	}

	if !core.AllowList(p.allowFrom, msg.FromUserName) {
		slog.Warn("wecom: message rejected by allow_from", "user", msg.FromUserName, "msg_type", msg.MsgType)
		return
	}

	sessionKey := fmt.Sprintf("wecom:%s", msg.FromUserName)
	rctx := replyContext{userID: msg.FromUserName}

	switch msg.MsgType {
	case "text":
		text := stripWeComAtMentions(msg.Content, p.agentID)
		slog.Debug("wecom: message received", "user", msg.FromUserName, "text_len", len(text))
		go p.handler(p, &core.Message{
			SessionKey: sessionKey, Platform: "wecom",
			MessageID: strconv.FormatInt(msg.MsgId, 10),
			UserID:    msg.FromUserName, UserName: p.resolveUserName(msg.FromUserName),
			Content: text, ReplyCtx: rctx,
		})

	case "image":
		slog.Debug("wecom: image received", "user", msg.FromUserName)
		go func() {
			imgData, err := p.downloadMedia(msg.MediaId)
			if err != nil {
				slog.Error("wecom: download image failed", "error", err)
				return
			}
			p.handler(p, &core.Message{
				SessionKey: sessionKey, Platform: "wecom",
				MessageID: strconv.FormatInt(msg.MsgId, 10),
				UserID:    msg.FromUserName, UserName: p.resolveUserName(msg.FromUserName),
				Images:   []core.ImageAttachment{{MimeType: "image/jpeg", Data: imgData}},
				ReplyCtx: rctx,
			})
		}()

	case "voice":
		slog.Debug("wecom: voice received", "user", msg.FromUserName, "format", msg.Format)
		go func() {
			audioData, err := p.downloadMedia(msg.MediaId)
			if err != nil {
				slog.Error("wecom: download voice failed", "error", err)
				return
			}
			format := strings.ToLower(msg.Format)
			if format == "" {
				format = "amr"
			}
			p.handler(p, &core.Message{
				SessionKey: sessionKey, Platform: "wecom",
				MessageID: strconv.FormatInt(msg.MsgId, 10),
				UserID:    msg.FromUserName, UserName: p.resolveUserName(msg.FromUserName),
				Audio:    &core.AudioAttachment{MimeType: "audio/" + format, Data: audioData, Format: format},
				ReplyCtx: rctx,
			})
		}()

	case "file":
		slog.Info("wecom: file message accepted", "user", msg.FromUserName, "file_name", msg.FileName, "media_id_len", len(msg.MediaId))
		if msg.MediaId == "" {
			slog.Warn("wecom: file message missing MediaId")
			return
		}
		go func() {
			fileData, err := p.downloadMedia(msg.MediaId)
			if err != nil {
				slog.Error("wecom: download file failed", "error", err)
				return
			}
			baseName := filepath.Base(strings.TrimSpace(msg.FileName))
			if baseName == "" || baseName == "." {
				baseName = "attachment"
			}
			mt := wecomInboundFileMime(baseName, fileData)
			p.handler(p, &core.Message{
				SessionKey: sessionKey, Platform: "wecom",
				MessageID: strconv.FormatInt(msg.MsgId, 10),
				UserID:    msg.FromUserName, UserName: p.resolveUserName(msg.FromUserName),
				Files: []core.FileAttachment{{
					MimeType: mt,
					Data:     fileData,
					FileName: baseName,
				}},
				ReplyCtx: rctx,
			})
		}()

	default:
		slog.Warn("wecom: unsupported inbound message type (no handler)",
			"msg_type", msg.MsgType,
			"msg_id", msg.MsgId,
			"from_user", msg.FromUserName)
	}
}

func (p *Platform) Reply(ctx context.Context, rctx any, content string) error {
	rc, ok := rctx.(replyContext)
	if !ok {
		return fmt.Errorf("wecom: invalid reply context type %T", rctx)
	}
	if content == "" {
		return nil
	}

	accessToken, err := p.getAccessToken()
	if err != nil {
		slog.Error("wecom: get access_token failed", "error", err)
		return fmt.Errorf("wecom: get access_token: %w", err)
	}

	if !p.enableMarkdown {
		content = core.StripMarkdown(content)
	}

	chunks := splitByBytes(content, 2000)
	for i, chunk := range chunks {
		var sendErr error
		if p.enableMarkdown {
			sendErr = p.sendMarkdown(accessToken, rc.userID, chunk)
		} else {
			sendErr = p.sendText(accessToken, rc.userID, chunk)
		}
		if sendErr != nil {
			slog.Error("wecom: send failed", "user", rc.userID, "chunk", i, "error", sendErr)
			return sendErr
		}
	}
	slog.Debug("wecom: message sent", "user", rc.userID, "chunks", len(chunks), "total_len", len(content))
	return nil
}

// Send sends a new message (same as Reply for WeChat Work)
func (p *Platform) Send(ctx context.Context, rctx any, content string) error {
	return p.Reply(ctx, rctx, content)
}

// SendImage uploads and sends an image to the user.
// Implements core.ImageSender.
func (p *Platform) SendImage(ctx context.Context, rctx any, img core.ImageAttachment) error {
	rc, ok := rctx.(replyContext)
	if !ok {
		return fmt.Errorf("wecom: SendImage: invalid reply context type %T", rctx)
	}

	accessToken, err := p.getAccessToken()
	if err != nil {
		return fmt.Errorf("wecom: send image: %w", err)
	}

	mediaID, err := p.uploadImageMedia(accessToken, img)
	if err != nil {
		return fmt.Errorf("wecom: send image: %w", err)
	}

	payload := map[string]any{
		"touser":  rc.userID,
		"msgtype": "image",
		"agentid": p.agentID,
		"image":   map[string]string{"media_id": mediaID},
	}

	body, _ := json.Marshal(payload)
	apiURL := p.wecomAPIURL("/cgi-bin/message/send", url.Values{
		"access_token": []string{accessToken},
	})

	resp, err := p.apiClient.Post(apiURL, "application/json", strings.NewReader(string(body)))
	if err != nil {
		return fmt.Errorf("wecom: send image: %w", err)
	}
	defer resp.Body.Close()

	var result struct {
		ErrCode int    `json:"errcode"`
		ErrMsg  string `json:"errmsg"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return fmt.Errorf("wecom: decode send image response: %w", err)
	}
	if result.ErrCode != 0 {
		return fmt.Errorf("wecom: send image failed: %d %s", result.ErrCode, result.ErrMsg)
	}
	return nil
}

// uploadImageMedia uploads an image to WeChat Work media API and returns the media_id.
func (p *Platform) uploadImageMedia(accessToken string, img core.ImageAttachment) (string, error) {
	name := img.FileName
	if name == "" {
		name = "image.png"
	}

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	part, err := writer.CreateFormFile("media", name)
	if err != nil {
		return "", fmt.Errorf("wecom: create form file: %w", err)
	}
	if _, err := part.Write(img.Data); err != nil {
		return "", fmt.Errorf("wecom: write image data: %w", err)
	}
	if err := writer.Close(); err != nil {
		return "", fmt.Errorf("wecom: close multipart writer: %w", err)
	}

	apiURL := p.wecomAPIURL("/cgi-bin/media/upload", url.Values{
		"access_token": []string{accessToken},
		"type":         []string{"image"},
	})
	resp, err := p.apiClient.Post(apiURL, writer.FormDataContentType(), body)
	if err != nil {
		return "", fmt.Errorf("wecom: upload image: %w", err)
	}
	defer resp.Body.Close()

	var result struct {
		ErrCode int    `json:"errcode"`
		ErrMsg  string `json:"errmsg"`
		MediaID string `json:"media_id"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", fmt.Errorf("wecom: decode upload response: %w", err)
	}
	if result.ErrCode != 0 {
		return "", fmt.Errorf("wecom: upload image failed: %d %s", result.ErrCode, result.ErrMsg)
	}
	if result.MediaID == "" {
		return "", fmt.Errorf("wecom: upload image: empty media_id")
	}
	return result.MediaID, nil
}

var _ core.ImageSender = (*Platform)(nil)

func (p *Platform) sendMarkdown(accessToken, toUser, content string) error {
	payload := map[string]any{
		"touser":   toUser,
		"msgtype":  "markdown",
		"agentid":  p.agentID,
		"markdown": map[string]string{"content": content},
	}

	body, _ := json.Marshal(payload)
	apiURL := p.wecomAPIURL("/cgi-bin/message/send", url.Values{
		"access_token": []string{accessToken},
	})

	resp, err := p.apiClient.Post(apiURL, "application/json", strings.NewReader(string(body)))
	if err != nil {
		return fmt.Errorf("wecom: send markdown: %w", err)
	}
	defer resp.Body.Close()

	var result struct {
		ErrCode int    `json:"errcode"`
		ErrMsg  string `json:"errmsg"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return fmt.Errorf("wecom: decode send response: %w", err)
	}
	if result.ErrCode != 0 {
		return fmt.Errorf("wecom: send markdown failed: %d %s", result.ErrCode, result.ErrMsg)
	}
	return nil
}

func (p *Platform) sendText(accessToken, toUser, text string) error {
	payload := map[string]any{
		"touser":  toUser,
		"msgtype": "text",
		"agentid": p.agentID,
		"text":    map[string]string{"content": text},
		"safe":    0,
	}

	body, _ := json.Marshal(payload)
	apiURL := p.wecomAPIURL("/cgi-bin/message/send", url.Values{
		"access_token": []string{accessToken},
	})

	resp, err := p.apiClient.Post(apiURL, "application/json", strings.NewReader(string(body)))
	if err != nil {
		return fmt.Errorf("wecom: send message: %w", err)
	}
	defer resp.Body.Close()

	var result struct {
		ErrCode int    `json:"errcode"`
		ErrMsg  string `json:"errmsg"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return fmt.Errorf("wecom: decode send response: %w", err)
	}
	if result.ErrCode != 0 {
		return fmt.Errorf("wecom: send failed: %d %s", result.ErrCode, result.ErrMsg)
	}
	return nil
}

func (p *Platform) getAccessToken() (string, error) {
	p.tokenCache.mu.Lock()
	defer p.tokenCache.mu.Unlock()

	if p.tokenCache.token != "" && time.Now().Before(p.tokenCache.expiresAt) {
		return p.tokenCache.token, nil
	}

	apiURL := p.wecomAPIURL("/cgi-bin/gettoken", url.Values{
		"corpid":     []string{p.corpID},
		"corpsecret": []string{p.corpSecret},
	})

	resp, err := p.apiClient.Get(apiURL)
	if err != nil {
		return "", fmt.Errorf("wecom: request access_token: %w", err)
	}
	defer resp.Body.Close()

	var result struct {
		ErrCode     int    `json:"errcode"`
		ErrMsg      string `json:"errmsg"`
		AccessToken string `json:"access_token"`
		ExpiresIn   int    `json:"expires_in"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", fmt.Errorf("wecom: decode token response: %w", err)
	}
	if result.ErrCode != 0 {
		return "", fmt.Errorf("wecom: get token failed: %d %s", result.ErrCode, result.ErrMsg)
	}

	// Compute the cache window from expires_in with a 60-second safety
	// margin. When the server omits or zeroes the field, fall back to
	// WeCom's documented 7200s default; without this, the raw value would
	// land at -60 and the cache would be stale on the very next call,
	// turning every outbound API request into a fresh /gettoken round-trip.
	expires := result.ExpiresIn
	if expires <= 0 {
		slog.Warn("wecom: missing/invalid expires_in in token response, defaulting to 7200s", "got", result.ExpiresIn)
		expires = 7200
	}
	if expires > 60 {
		expires -= 60
	}
	p.tokenCache.token = result.AccessToken
	p.tokenCache.expiresAt = time.Now().Add(time.Duration(expires) * time.Second)

	slog.Debug("wecom: access_token refreshed", "expires_in", result.ExpiresIn)
	return result.AccessToken, nil
}

func (p *Platform) ReconstructReplyCtx(sessionKey string) (any, error) {
	// wecom:{userID}
	parts := strings.SplitN(sessionKey, ":", 2)
	if len(parts) < 2 || parts[0] != "wecom" {
		return nil, fmt.Errorf("wecom: invalid session key %q", sessionKey)
	}
	return replyContext{userID: parts[1]}, nil
}

func (p *Platform) Stop() error {
	if p.server != nil {
		return p.server.Shutdown(context.Background())
	}
	return nil
}

// --- Crypto helpers ---

// verifySignature checks SHA1(sort(token, timestamp, nonce, encrypt)).
func (p *Platform) verifySignature(expected, timestamp, nonce, encrypt string) bool {
	parts := []string{p.token, timestamp, nonce, encrypt}
	sort.Strings(parts)
	h := sha1.New()
	h.Write([]byte(strings.Join(parts, "")))
	got := fmt.Sprintf("%x", h.Sum(nil))
	return got == expected
}

// decodeAESKey converts the 43-char Base64 EncodingAESKey to 32 bytes.
func decodeAESKey(encodingAESKey string) ([]byte, error) {
	if len(encodingAESKey) != 43 {
		return nil, fmt.Errorf("EncodingAESKey must be 43 characters, got %d", len(encodingAESKey))
	}
	return base64.StdEncoding.DecodeString(encodingAESKey + "=")
}

// decrypt decodes and decrypts a Base64-encoded AES-256-CBC ciphertext.
// Layout after decryption + PKCS#7 unpad:
//
//	[16 bytes random] [4 bytes msg_len (big-endian)] [msg_len bytes message] [corp_id]
func (p *Platform) decrypt(cipherBase64 string) (string, error) {
	cipherData, err := base64.StdEncoding.DecodeString(cipherBase64)
	if err != nil {
		return "", fmt.Errorf("base64 decode: %w", err)
	}

	block, err := aes.NewCipher(p.aesKey)
	if err != nil {
		return "", fmt.Errorf("aes new cipher: %w", err)
	}

	if len(cipherData) < aes.BlockSize || len(cipherData)%aes.BlockSize != 0 {
		return "", fmt.Errorf("invalid ciphertext length %d", len(cipherData))
	}

	iv := p.aesKey[:16]
	mode := cipher.NewCBCDecrypter(block, iv)
	plain := make([]byte, len(cipherData))
	mode.CryptBlocks(plain, cipherData)

	plain = pkcs7Unpad(plain)

	if len(plain) < 20 {
		return "", fmt.Errorf("decrypted data too short")
	}

	msgLen := int(binary.BigEndian.Uint32(plain[16:20]))
	if 20+msgLen > len(plain) {
		return "", fmt.Errorf("invalid message length %d in decrypted data (total %d)", msgLen, len(plain))
	}

	msg := string(plain[20 : 20+msgLen])
	corpID := string(plain[20+msgLen:])

	if corpID != p.corpID {
		return "", fmt.Errorf("corp_id mismatch: expected %s, got %s", p.corpID, corpID)
	}

	return msg, nil
}

func pkcs7Unpad(data []byte) []byte {
	if len(data) == 0 {
		return data
	}
	pad := int(data[len(data)-1])
	if pad < 1 || pad > 32 || pad > len(data) {
		return data
	}
	return data[:len(data)-pad]
}

// downloadMedia fetches a temporary media file from WeChat Work by media_id.
func (p *Platform) resolveUserName(userID string) string {
	if cached, ok := p.userNameCache.Load(userID); ok {
		return cached.(string)
	}
	accessToken, err := p.getAccessToken()
	if err != nil {
		slog.Debug("wecom: resolve user name: get token failed", "error", err)
		return userID
	}
	apiURL := p.wecomAPIURL("/cgi-bin/user/get", url.Values{
		"access_token": []string{accessToken},
		"userid":       []string{userID},
	})
	resp, err := p.apiClient.Get(apiURL)
	if err != nil {
		slog.Debug("wecom: resolve user name failed", "user", userID, "error", err)
		return userID
	}
	defer resp.Body.Close()
	var result struct {
		ErrCode int    `json:"errcode"`
		Name    string `json:"name"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || result.ErrCode != 0 {
		slog.Debug("wecom: resolve user name: api error", "user", userID, "errcode", result.ErrCode)
		return userID
	}
	if result.Name != "" {
		p.userNameCache.Store(userID, result.Name)
		return result.Name
	}
	return userID
}

// wecomInboundFileMime infers MIME type from filename extension, then from content sniffing.
func wecomInboundFileMime(fileName string, data []byte) string {
	ext := strings.ToLower(filepath.Ext(fileName))
	if ext != "" {
		if mt := mime.TypeByExtension(ext); mt != "" {
			if mt != "application/octet-stream" {
				return mt
			}
		}
	}
	if len(data) > 0 {
		if mt := wecomInboundFileMagicMime(data); mt != "" {
			return mt
		}
		if sniff := http.DetectContentType(data); sniff != "" {
			return sniff
		}
	}
	return "application/octet-stream"
}

func wecomInboundFileMagicMime(data []byte) string {
	if len(data) >= 8 && string(data[:8]) == "\x89PNG\r\n\x1a\n" {
		return "image/png"
	}
	if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
		return "image/jpeg"
	}
	if len(data) >= 6 {
		head := string(data[:6])
		if head == "GIF87a" || head == "GIF89a" {
			return "image/gif"
		}
	}
	if len(data) >= 12 && string(data[:4]) == "RIFF" && string(data[8:12]) == "WEBP" {
		return "image/webp"
	}
	if len(data) >= 4 && string(data[:4]) == "%PDF" {
		return "application/pdf"
	}
	return ""
}

func (p *Platform) downloadMedia(mediaID string) ([]byte, error) {
	accessToken, err := p.getAccessToken()
	if err != nil {
		return nil, fmt.Errorf("get token: %w", err)
	}
	u := p.wecomAPIURL("/cgi-bin/media/get", url.Values{
		"access_token": []string{accessToken},
		"media_id":     []string{mediaID},
	})
	resp, err := p.apiClient.Get(u)
	if err != nil {
		return nil, fmt.Errorf("download: %w", err)
	}
	defer resp.Body.Close()
	return io.ReadAll(resp.Body)
}
