package app

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"html"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	nethtml "golang.org/x/net/html"
)

const (
	telegramMailMaxAttempts = 8
	telegramMessageBudget   = 3800
	telegramPairingTTL      = 10 * time.Minute
)

type telegramPairing struct {
	TokenFingerprint string
	ExpiresAt        time.Time
}

type telegramMailPayload struct {
	From            string   `json:"from"`
	FromName        string   `json:"fromName,omitempty"`
	Recipient       string   `json:"recipient"`
	Subject         string   `json:"subject"`
	ReceivedAt      string   `json:"receivedAt"`
	Body            string   `json:"body"`
	BodyMode        string   `json:"bodyMode"`
	OTP             string   `json:"otp,omitempty"`
	AttachmentNames []string `json:"attachmentNames,omitempty"`
	AttachmentCount int      `json:"attachmentCount,omitempty"`
}

type telegramCredentialsRequest struct {
	BotToken    string `json:"botToken"`
	ChatID      string `json:"chatId"`
	PairingCode string `json:"pairingCode"`
}

type telegramAPIResponse struct {
	OK          bool            `json:"ok"`
	ErrorCode   int             `json:"error_code"`
	Description string          `json:"description"`
	Result      json.RawMessage `json:"result"`
	Parameters  struct {
		RetryAfter int `json:"retry_after"`
	} `json:"parameters"`
}

type telegramUpdate struct {
	UpdateID int64 `json:"update_id"`
	Message  *struct {
		Text string `json:"text"`
		Chat struct {
			ID        int64  `json:"id"`
			Type      string `json:"type"`
			Title     string `json:"title"`
			FirstName string `json:"first_name"`
			LastName  string `json:"last_name"`
			Username  string `json:"username"`
		} `json:"chat"`
	} `json:"message"`
}

type telegramAPIError struct {
	HTTPStatus  int
	ErrorCode   int
	Description string
	RetryAfter  time.Duration
}

func (e *telegramAPIError) Error() string {
	description := strings.TrimSpace(e.Description)
	if description == "" {
		description = fmt.Sprintf("HTTP %d", e.HTTPStatus)
	}
	return "Telegram 发送失败: " + description
}

type telegramSentMessage struct {
	MessageID int64 `json:"message_id"`
}

type telegramFormattedMessage struct {
	HTML      string
	PlainText string
	OTP       string
}

func normalizeTelegramBodyMode(value string) string {
	if strings.EqualFold(strings.TrimSpace(value), "full") {
		return "full"
	}
	return "summary"
}

func validTelegramPrivateChatID(value string) bool {
	id, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
	return err == nil && id != 0
}

func (a *App) handleCreateTelegramPairing(w http.ResponseWriter, r *http.Request) {
	var req telegramCredentialsRequest
	if err := decodeJSON(r, &req); err != nil {
		badRequest(w, err)
		return
	}
	token := strings.TrimSpace(req.BotToken)
	if token == "" {
		token = strings.TrimSpace(a.config().TelegramBotToken)
	}
	if token == "" {
		badRequest(w, errors.New("请先填写 Telegram Bot Token"))
		return
	}
	var bot struct {
		Username string `json:"username"`
	}
	if err := a.callTelegram(r.Context(), token, "getMe", map[string]any{}, &bot); err != nil {
		respondError(w, http.StatusBadGateway, err.Error())
		return
	}
	if strings.TrimSpace(bot.Username) == "" {
		respondError(w, http.StatusBadGateway, "Telegram 机器人没有可用的用户名")
		return
	}
	code, err := newTelegramPairingCode()
	if err != nil {
		respondError(w, http.StatusInternalServerError, "无法生成 Telegram 绑定码")
		return
	}
	expiresAt := a.now().UTC().Add(telegramPairingTTL)
	a.telegramPairMu.Lock()
	for value, pairing := range a.telegramPairs {
		if !pairing.ExpiresAt.After(a.now().UTC()) {
			delete(a.telegramPairs, value)
		}
	}
	a.telegramPairs[code] = telegramPairing{TokenFingerprint: telegramTokenFingerprint(token), ExpiresAt: expiresAt}
	a.telegramPairMu.Unlock()
	respondJSON(w, http.StatusOK, map[string]string{
		"code":        code,
		"botUsername": bot.Username,
		"deepLink":    "https://t.me/" + url.PathEscape(bot.Username) + "?start=" + url.QueryEscape(code),
		"expiresAt":   expiresAt.Format(time.RFC3339Nano),
	})
}

func (a *App) handleDiscoverTelegramChat(w http.ResponseWriter, r *http.Request) {
	var req telegramCredentialsRequest
	if err := decodeJSON(r, &req); err != nil {
		badRequest(w, err)
		return
	}
	token := strings.TrimSpace(req.BotToken)
	if token == "" {
		token = strings.TrimSpace(a.config().TelegramBotToken)
	}
	code := strings.ToUpper(strings.TrimSpace(req.PairingCode))
	if token == "" || code == "" {
		badRequest(w, errors.New("请先生成 Telegram 一次性绑定码"))
		return
	}
	a.telegramPairMu.Lock()
	pairing, ok := a.telegramPairs[code]
	a.telegramPairMu.Unlock()
	if !ok || !pairing.ExpiresAt.After(a.now().UTC()) || pairing.TokenFingerprint != telegramTokenFingerprint(token) {
		badRequest(w, errors.New("Telegram 绑定码无效或已过期，请重新生成"))
		return
	}
	chatID, displayName, err := a.discoverTelegramPrivateChat(r.Context(), token, code)
	if err != nil {
		respondError(w, http.StatusBadGateway, err.Error())
		return
	}
	a.telegramPairMu.Lock()
	delete(a.telegramPairs, code)
	a.telegramPairMu.Unlock()
	respondJSON(w, http.StatusOK, map[string]string{"chatId": chatID, "displayName": displayName})
}

func (a *App) handleTestTelegram(w http.ResponseWriter, r *http.Request) {
	var req telegramCredentialsRequest
	if err := decodeJSON(r, &req); err != nil {
		badRequest(w, err)
		return
	}
	token, chatID := a.telegramCredentials(req)
	if token == "" {
		badRequest(w, errors.New("请先填写 Telegram Bot Token"))
		return
	}
	if !validTelegramPrivateChatID(chatID) {
		badRequest(w, errors.New("请先获取或填写有效的私聊 Chat ID"))
		return
	}
	now := a.now().Local().Format("2006-01-02 15:04:05 MST")
	text := "<b>NewSzxcn 邮箱通知测试</b>\n\nTelegram 私聊邮件通知连接正常。\n\n<b>测试时间：</b>" + html.EscapeString(now)
	if err := a.sendTelegramMessage(r.Context(), token, chatID, text); err != nil {
		respondError(w, http.StatusBadGateway, err.Error())
		return
	}
	respondJSON(w, http.StatusOK, map[string]any{"ok": true})
}

func (a *App) telegramCredentials(req telegramCredentialsRequest) (string, string) {
	cfg := a.config()
	token := strings.TrimSpace(req.BotToken)
	if token == "" {
		token = strings.TrimSpace(cfg.TelegramBotToken)
	}
	chatID := strings.TrimSpace(req.ChatID)
	if chatID == "" {
		chatID = strings.TrimSpace(cfg.TelegramPrivateChatID)
	}
	return token, chatID
}

func (a *App) discoverTelegramPrivateChat(ctx context.Context, token, pairingCode string) (string, string, error) {
	var updates []telegramUpdate
	if err := a.callTelegram(ctx, token, "getUpdates", map[string]any{
		"limit":           100,
		"timeout":         0,
		"allowed_updates": []string{"message"},
	}, &updates); err != nil {
		return "", "", err
	}
	for i := len(updates) - 1; i >= 0; i-- {
		message := updates[i].Message
		if message == nil || message.Chat.Type != "private" || message.Chat.ID <= 0 {
			continue
		}
		text := strings.TrimSpace(message.Text)
		if text != pairingCode && text != "/start "+pairingCode {
			continue
		}
		name := strings.TrimSpace(strings.Join([]string{message.Chat.FirstName, message.Chat.LastName}, " "))
		if name == "" && message.Chat.Username != "" {
			name = "@" + message.Chat.Username
		}
		return strconv.FormatInt(message.Chat.ID, 10), name, nil
	}
	return "", "", errors.New("未找到匹配的私聊，请打开机器人发送绑定码后重试")
}

type telegramDiscoveredChat struct {
	ChatID      string `json:"chatId"`
	DisplayName string `json:"displayName"`
}

func (a *App) discoverTelegramGroups(ctx context.Context, token, pairingCode string) ([]telegramDiscoveredChat, error) {
	var updates []telegramUpdate
	if err := a.callTelegram(ctx, token, "getUpdates", map[string]any{
		"limit": 100, "timeout": 0, "allowed_updates": []string{"message"},
	}, &updates); err != nil {
		return nil, err
	}
	found := make([]telegramDiscoveredChat, 0)
	seen := make(map[int64]bool)
	for i := len(updates) - 1; i >= 0; i-- {
		message := updates[i].Message
		if message == nil || (message.Chat.Type != "group" && message.Chat.Type != "supergroup") || message.Chat.ID >= 0 {
			continue
		}
		text := strings.TrimSpace(message.Text)
		fields := strings.Fields(text)
		matches := strings.EqualFold(text, pairingCode)
		if len(fields) == 2 && strings.HasPrefix(strings.ToLower(fields[0]), "/newszxcn") {
			matches = strings.EqualFold(fields[1], pairingCode)
		}
		if !matches {
			continue
		}
		if seen[message.Chat.ID] {
			continue
		}
		seen[message.Chat.ID] = true
		name := strings.TrimSpace(message.Chat.Title)
		if name == "" {
			name = "Telegram 群组"
		}
		found = append(found, telegramDiscoveredChat{ChatID: strconv.FormatInt(message.Chat.ID, 10), DisplayName: name})
	}
	if len(found) == 0 {
		return nil, errors.New("未找到匹配的群组，请确认机器人已加入群组，并在群里发送查询命令")
	}
	return found, nil
}

func newTelegramPairingCode() (string, error) {
	raw := make([]byte, 6)
	if _, err := rand.Read(raw); err != nil {
		return "", err
	}
	return strings.ToUpper(hex.EncodeToString(raw)), nil
}

func telegramTokenFingerprint(token string) string {
	sum := sha256.Sum256([]byte(strings.TrimSpace(token)))
	return hex.EncodeToString(sum[:])
}

func telegramMailboxAllowed(cfg Config, mailboxID string) bool {
	mailboxID = strings.TrimSpace(mailboxID)
	if mailboxID == "" {
		return cfg.TelegramIncludeUnregistered
	}
	for _, id := range cleanIDList(strings.Split(cfg.TelegramMailboxIDs, ",")) {
		if id == mailboxID {
			return true
		}
	}
	return false
}

func (a *App) activeTelegramMailboxIDs(ctx context.Context, values []string) []string {
	ids := cleanIDList(values)
	active := make([]string, 0, len(ids))
	for _, id := range ids {
		var exists int
		if err := a.db.QueryRowContext(ctx, `SELECT 1 FROM mailboxes WHERE id=? AND status='active'`, id).Scan(&exists); err == nil && exists == 1 {
			active = append(active, id)
		}
	}
	return active
}

func (a *App) enqueueTelegramMailNotification(ctx context.Context, messageID string, msg storedMessage, attachments []AttachmentInput) {
	cfg := a.config()
	if !cfg.TelegramMailEnabled || strings.TrimSpace(cfg.TelegramBotToken) == "" || !validTelegramPrivateChatID(cfg.TelegramPrivateChatID) || !telegramMailboxAllowed(cfg, msg.MailboxID) {
		return
	}
	recipient := normalizeEmail(msg.RecipientAddr)
	if recipient == "" && len(msg.To) > 0 {
		recipient = normalizeEmail(msg.To[0])
	}
	body := telegramMessageBody(msg)
	otp := detectTelegramOTP(msg.Subject, body)
	mode := normalizeTelegramBodyMode(cfg.TelegramBodyMode)
	limit := 800
	if mode == "full" {
		limit = 2600
	}
	body, truncated := truncateRunes(body, limit)
	if truncated {
		body += "..."
	}
	if body == "" {
		body = normalizeTelegramText(msg.Snippet)
	}
	from, _ := truncateRunes(strings.TrimSpace(msg.From), 254)
	fromName, _ := truncateRunes(strings.TrimSpace(msg.FromName), 160)
	subject, _ := truncateRunes(strings.TrimSpace(msg.Subject), 240)
	names := make([]string, 0, min(len(attachments), 5))
	for _, attachment := range attachments {
		name := sanitizeTelegramAttachmentName(attachment.Filename)
		if name != "" {
			names = append(names, name)
		}
		if len(names) >= 5 {
			break
		}
	}
	payload := telegramMailPayload{
		From:            from,
		FromName:        fromName,
		Recipient:       recipient,
		Subject:         subject,
		ReceivedAt:      a.now().UTC().Format(time.RFC3339Nano),
		Body:            body,
		BodyMode:        mode,
		OTP:             otp,
		AttachmentNames: names,
		AttachmentCount: len(attachments),
	}
	now := a.now().UTC().Format(time.RFC3339Nano)
	if _, err := a.db.ExecContext(ctx, `INSERT OR IGNORE INTO telegram_mail_outbox(id,message_id,payload_json,next_attempt_at,created_at,updated_at) VALUES(?,?,?,?,?,?)`, newID("tgm"), messageID, jsonEncode(payload), now, now, now); err != nil {
		a.log.Warn("failed to enqueue Telegram mail notification", "messageId", messageID, "error", err)
	}
}

func telegramMessageBody(msg storedMessage) string {
	text := strings.TrimSpace(msg.BodyText)
	text, _ = truncateRunes(text, 128*1024)
	if text != "" && looksLikeHTMLDocument(text) {
		text = telegramHTMLToText(text)
	}
	if strings.TrimSpace(text) == "" {
		text = telegramHTMLToText(msg.BodyHTML)
	}
	return stripTelegramQuotedContent(normalizeTelegramText(text))
}

func looksLikeHTMLDocument(value string) bool {
	value, _ = truncateRunes(value, 128*1024)
	value = strings.ToLower(strings.TrimSpace(value))
	if strings.HasPrefix(value, "<!doctype html") || strings.HasPrefix(value, "<html") || strings.HasPrefix(value, "<head") || strings.HasPrefix(value, "<body") || strings.HasPrefix(value, "<style") {
		return true
	}
	matches := telegramHTMLTagRe.FindAllStringIndex(value, 4)
	return len(matches) >= 3
}

func telegramHTMLToText(value string) string {
	value = strings.ToValidUTF8(value, "�")
	value, _ = truncateRunes(value, 128*1024)
	doc, err := nethtml.Parse(strings.NewReader(value))
	if err != nil {
		return stripTags(value)
	}
	var out strings.Builder
	var walk func(*nethtml.Node, bool)
	walk = func(node *nethtml.Node, skipped bool) {
		if node.Type == nethtml.ElementNode {
			switch strings.ToLower(node.Data) {
			case "script", "style", "head", "noscript", "svg":
				skipped = true
			case "br":
				if !skipped {
					out.WriteByte('\n')
				}
			}
		}
		if node.Type == nethtml.TextNode && !skipped {
			out.WriteString(node.Data)
		}
		for child := node.FirstChild; child != nil; child = child.NextSibling {
			walk(child, skipped)
		}
		if node.Type == nethtml.ElementNode && !skipped {
			switch strings.ToLower(node.Data) {
			case "p", "div", "li", "tr", "table", "section", "article", "header", "footer", "h1", "h2", "h3", "h4", "h5", "h6":
				out.WriteByte('\n')
			}
		}
	}
	walk(doc, false)
	return normalizeTelegramText(out.String())
}

func normalizeTelegramText(value string) string {
	value = strings.ReplaceAll(strings.ToValidUTF8(value, "�"), "\r\n", "\n")
	value = strings.ReplaceAll(value, "\r", "\n")
	lines := strings.Split(value, "\n")
	out := make([]string, 0, len(lines))
	empty := false
	for _, line := range lines {
		line = strings.TrimSpace(strings.Map(func(r rune) rune {
			if r == '\t' {
				return ' '
			}
			if unicode.IsControl(r) {
				return -1
			}
			return r
		}, line))
		line = strings.Join(strings.Fields(line), " ")
		if line == "" {
			if !empty && len(out) > 0 {
				out = append(out, "")
			}
			empty = true
			continue
		}
		empty = false
		out = append(out, line)
	}
	return strings.TrimSpace(strings.Join(out, "\n"))
}

var telegramQuoteBoundaryRe = regexp.MustCompile(`(?i)^(?:-{2,}\s*(?:original message|原始邮件)\s*-*|on .+ wrote:|发件人[：:]|from[：:].+|_{5,})$`)
var telegramHTMLTagRe = regexp.MustCompile(`(?i)</?(?:div|p|table|tr|td|br|span|a|img)(?:\s[^>]*)?>`)

func stripTelegramQuotedContent(value string) string {
	lines := strings.Split(value, "\n")
	for i, line := range lines {
		trimmed := strings.TrimSpace(line)
		if i > 0 && (trimmed == "--" || telegramQuoteBoundaryRe.MatchString(trimmed)) {
			lines = lines[:i]
			break
		}
	}
	return strings.TrimSpace(strings.Join(lines, "\n"))
}

func sanitizeTelegramAttachmentName(value string) string {
	value = strings.TrimSpace(strings.Map(func(r rune) rune {
		if unicode.IsControl(r) {
			return -1
		}
		return r
	}, strings.ToValidUTF8(value, "�")))
	value = strings.Join(strings.Fields(value), " ")
	value, truncated := truncateRunes(value, 100)
	if truncated {
		value += "..."
	}
	return value
}

var (
	telegramOTPKeywordRe   = regexp.MustCompile(`(?i)(验证码|校验码|动态码|登录码|安全码|一次性密码|otp|verification[ -]?code|security[ -]?code|login[ -]?code|passcode|one[ -]?time[ -]?(?:password|code))`)
	telegramOTPCandidateRe = regexp.MustCompile(`(?i)[a-z0-9]{4,10}`)
	telegramEmailRe        = regexp.MustCompile(`(?i)[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}`)
	telegramURLRe          = regexp.MustCompile(`(?i)https?://[^\s<>"']+`)
)

func detectTelegramOTP(subject, body string) string {
	text := normalizeTelegramText(strings.TrimSpace(subject) + "\n" + body)
	keywords := telegramOTPKeywordRe.FindAllStringIndex(text, -1)
	if len(keywords) == 0 {
		return ""
	}
	type candidateScore struct {
		value string
		score int
		count int
	}
	scores := map[string]candidateScore{}
	subjectEnd := len(strings.TrimSpace(subject))
	excludedRanges := append(telegramEmailRe.FindAllStringIndex(text, -1), telegramURLRe.FindAllStringIndex(text, -1)...)
	for _, match := range telegramOTPCandidateRe.FindAllStringIndex(text, -1) {
		if telegramRangeOverlaps(match, excludedRanges) {
			continue
		}
		if match[0] > 0 && isTelegramOTPAlphaNumeric(rune(text[match[0]-1])) {
			continue
		}
		if match[1] < len(text) && isTelegramOTPAlphaNumeric(rune(text[match[1]])) {
			continue
		}
		value := strings.ToUpper(text[match[0]:match[1]])
		hasDigit := false
		for _, r := range value {
			if unicode.IsDigit(r) {
				hasDigit = true
				break
			}
		}
		if !hasDigit || telegramOTPKeywordRe.MatchString(value) {
			continue
		}
		if isTelegramOTPNonCode(value) {
			continue
		}
		best := 0
		for _, keyword := range keywords {
			distance := match[0] - keyword[1]
			if distance < 0 {
				distance = keyword[0] - match[1]
			}
			if distance < 0 {
				distance = 0
			}
			score := 0
			switch {
			case distance <= 16:
				score = 100
			case distance <= 48:
				score = 80
			case distance <= 100:
				score = 55
			}
			if match[0] <= subjectEnd {
				score += 15
			}
			if score > best {
				best = score
			}
		}
		if best == 0 {
			continue
		}
		current := scores[value]
		current.value = value
		current.count++
		if best > current.score {
			current.score = best
		}
		scores[value] = current
	}
	items := make([]candidateScore, 0, len(scores))
	for _, item := range scores {
		item.score += min(item.count-1, 2) * 5
		items = append(items, item)
	}
	sort.Slice(items, func(i, j int) bool { return items[i].score > items[j].score })
	if len(items) == 0 || items[0].score < 55 {
		return ""
	}
	if len(items) > 1 && items[1].score >= items[0].score-25 {
		return ""
	}
	return items[0].value
}

func telegramRangeOverlaps(candidate []int, ranges [][]int) bool {
	for _, item := range ranges {
		if len(item) == 2 && candidate[0] < item[1] && candidate[1] > item[0] {
			return true
		}
	}
	return false
}

func isTelegramOTPNonCode(value string) bool {
	if len(value) == 4 {
		if year, err := strconv.Atoi(value); err == nil && year >= 1900 && year <= 2099 {
			return true
		}
	}
	if len(value) == 8 {
		if _, err := time.Parse("20060102", value); err == nil {
			return true
		}
	}
	return false
}

func isTelegramOTPAlphaNumeric(r rune) bool {
	return r <= unicode.MaxASCII && (unicode.IsLetter(r) || unicode.IsDigit(r))
}

func (a *App) shouldNotifyTelegramMessage(ctx context.Context, messageID string) bool {
	var folder string
	if err := a.db.QueryRowContext(ctx, `SELECT lower(COALESCE(NULLIF(f.role,''),f.name,'')) FROM messages m LEFT JOIN folders f ON f.id=m.folder_id WHERE m.id=?`, messageID).Scan(&folder); err != nil {
		return false
	}
	switch strings.TrimSpace(folder) {
	case "spam", "junk", "trash", "deleted":
		return false
	default:
		return true
	}
}

func (a *App) telegramMailWorker(ctx context.Context) {
	a.log.Info("Telegram mail notification worker started")
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()
	for {
		if err := a.processDueTelegramMailNotifications(ctx); err != nil && !errors.Is(err, context.Canceled) {
			a.log.Warn("Telegram mail notification worker failed", "error", err)
		}
		select {
		case <-ctx.Done():
			a.log.Info("Telegram mail notification worker stopped")
			return
		case <-ticker.C:
		}
	}
}

func (a *App) processDueTelegramMailNotifications(ctx context.Context) error {
	a.telegramDeliveryMu.Lock()
	defer a.telegramDeliveryMu.Unlock()
	_, _ = a.db.ExecContext(ctx, `DELETE FROM telegram_mail_outbox WHERE updated_at<? AND (delivered_at IS NOT NULL OR attempt_count>=?)`, a.now().UTC().Add(-30*24*time.Hour).Format(time.RFC3339Nano), telegramMailMaxAttempts)
	cfg := a.config()
	if !cfg.TelegramMailEnabled || strings.TrimSpace(cfg.TelegramBotToken) == "" || !validTelegramPrivateChatID(cfg.TelegramPrivateChatID) {
		return nil
	}
	nowText := a.now().UTC().Format(time.RFC3339Nano)
	rows, err := a.db.QueryContext(ctx, `SELECT id,payload_json,attempt_count FROM telegram_mail_outbox WHERE delivered_at IS NULL AND attempt_count<? AND next_attempt_at<=? AND (lease_until='' OR lease_until<=?) ORDER BY next_attempt_at,created_at LIMIT 20`, telegramMailMaxAttempts, nowText, nowText)
	if err != nil {
		return err
	}
	type queueItem struct {
		id      string
		payload telegramMailPayload
		attempt int
		invalid bool
	}
	items := []queueItem{}
	for rows.Next() {
		var item queueItem
		var raw string
		if err := rows.Scan(&item.id, &raw, &item.attempt); err != nil {
			rows.Close()
			return err
		}
		if err := json.Unmarshal([]byte(raw), &item.payload); err != nil {
			item.invalid = true
		}
		items = append(items, item)
	}
	if err := rows.Close(); err != nil {
		return err
	}
	for _, item := range items {
		if item.invalid {
			now := a.now().UTC().Format(time.RFC3339Nano)
			if _, err := a.db.ExecContext(ctx, `UPDATE telegram_mail_outbox SET attempt_count=?,last_error='通知数据损坏',updated_at=?,lease_until='',payload_json='{}' WHERE id=?`, telegramMailMaxAttempts, now, item.id); err != nil {
				return err
			}
			continue
		}
		now := a.now().UTC()
		leaseUntil := now.Add(2 * time.Minute).Format(time.RFC3339Nano)
		result, err := a.db.ExecContext(ctx, `UPDATE telegram_mail_outbox SET lease_until=?,updated_at=? WHERE id=? AND delivered_at IS NULL AND (lease_until='' OR lease_until<=?)`, leaseUntil, now.Format(time.RFC3339Nano), item.id, now.Format(time.RFC3339Nano))
		if err != nil {
			return err
		}
		if affected, _ := result.RowsAffected(); affected != 1 {
			continue
		}
		formatted := formatTelegramMailMessage(item.payload)
		telegramMessageID, err := a.deliverTelegramMailMessage(ctx, cfg.TelegramBotToken, cfg.TelegramPrivateChatID, formatted)
		now = a.now().UTC()
		if err != nil {
			attempts := item.attempt + 1
			delay := sendRetryDelay(attempts)
			var apiErr *telegramAPIError
			if errors.As(err, &apiErr) {
				if apiErr.RetryAfter > 0 {
					delay = apiErr.RetryAfter
				}
				code := apiErr.ErrorCode
				if code == 0 {
					code = apiErr.HTTPStatus
				}
				if code == http.StatusUnauthorized || code == http.StatusForbidden || (code >= 400 && code < 500 && code != http.StatusTooManyRequests) {
					attempts = telegramMailMaxAttempts
				}
			}
			next := now.Add(delay)
			if _, updateErr := a.db.ExecContext(ctx, `UPDATE telegram_mail_outbox SET attempt_count=?,next_attempt_at=?,last_error=?,updated_at=?,lease_until='',payload_json=CASE WHEN ?>=? THEN '{}' ELSE payload_json END WHERE id=? AND delivered_at IS NULL`, attempts, next.Format(time.RFC3339Nano), truncateWebhookError(err.Error()), now.Format(time.RFC3339Nano), attempts, telegramMailMaxAttempts, item.id); updateErr != nil {
				return updateErr
			}
			continue
		}
		stamp := now.Format(time.RFC3339Nano)
		if _, err := a.db.ExecContext(ctx, `UPDATE telegram_mail_outbox SET attempt_count=attempt_count+1,last_error='',updated_at=?,delivered_at=?,lease_until='',telegram_message_id=?,payload_json='{}' WHERE id=? AND delivered_at IS NULL`, stamp, stamp, telegramMessageID, item.id); err != nil {
			return err
		}
	}
	return nil
}

func formatTelegramMailMessage(payload telegramMailPayload) telegramFormattedMessage {
	subject := strings.TrimSpace(payload.Subject)
	if subject == "" || subject == "(no subject)" {
		subject = "（无主题）"
	}
	from := strings.TrimSpace(payload.From)
	if name := strings.TrimSpace(payload.FromName); name != "" {
		from = name + " <" + from + ">"
	}
	receivedAt := parseTime(payload.ReceivedAt)
	timeText := strings.TrimSpace(payload.ReceivedAt)
	if !receivedAt.IsZero() {
		timeText = receivedAt.Local().Format("2006-01-02 15:04:05 MST")
	}
	subject, _ = truncateRunes(subject, 180)
	from, _ = truncateRunes(from, 220)
	recipient, _ := truncateRunes(strings.TrimSpace(payload.Recipient), 160)
	lines := []string{
		"📩 <b>新邮件通知</b>",
		"",
		"<b>主题：</b>" + escapeTelegramWithinBudget(subject, 420),
		"<b>发件人：</b>" + escapeTelegramWithinBudget(from, 500),
		"<b>收件邮箱：</b><code>" + escapeTelegramWithinBudget(recipient, 320) + "</code>",
		"<b>收件时间：</b>" + html.EscapeString(timeText),
	}
	if payload.OTP != "" {
		lines = append(lines, "", "🔐 <b>验证码</b>", "<code>"+html.EscapeString(payload.OTP)+"</code>")
	}
	if len(payload.AttachmentNames) > 0 {
		names := make([]string, 0, len(payload.AttachmentNames))
		for _, name := range payload.AttachmentNames {
			names = append(names, escapeTelegramWithinBudget(name, 180))
		}
		attachmentText := strings.Join(names, "、")
		if payload.AttachmentCount > len(payload.AttachmentNames) {
			attachmentText += fmt.Sprintf("，其余 %d 个未显示", payload.AttachmentCount-len(payload.AttachmentNames))
		}
		lines = append(lines, "", fmt.Sprintf("📎 <b>附件：%d 个</b>", max(payload.AttachmentCount, len(payload.AttachmentNames))), attachmentText)
	}
	body := strings.TrimSpace(payload.Body)
	if body != "" {
		label := "正文摘要"
		if normalizeTelegramBodyMode(payload.BodyMode) == "full" {
			label = "邮件正文"
		}
		prefix := strings.Join(lines, "\n") + "\n\n<b>" + label + "</b>\n<blockquote>"
		suffix := "</blockquote>"
		body = formatTelegramBodyHTML(body, telegramMessageBudget-utf8.RuneCountInString(prefix)-utf8.RuneCountInString(suffix))
		lines = []string{prefix + body + suffix}
	}
	htmlText := strings.Join(lines, "\n")
	plain := formatTelegramMailPlainText(payload)
	return telegramFormattedMessage{HTML: htmlText, PlainText: plain, OTP: payload.OTP}
}

func formatTelegramBodyHTML(value string, budget int) string {
	if budget <= 3 {
		return ""
	}
	var out strings.Builder
	used := 0
	truncated := false
	appendEscaped := func(text string) bool {
		for _, r := range text {
			escaped := html.EscapeString(string(r))
			length := utf8.RuneCountInString(escaped)
			if used+length > budget-3 {
				return false
			}
			out.WriteString(escaped)
			used += length
		}
		return true
	}
	last := 0
	for _, match := range telegramURLRe.FindAllStringIndex(value, -1) {
		if !appendEscaped(value[last:match[0]]) {
			truncated = true
			break
		}
		rawURL, trailing := trimTelegramURL(value[match[0]:match[1]])
		parsed, err := url.Parse(rawURL)
		if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
			if !appendEscaped(value[match[0]:match[1]]) {
				truncated = true
				break
			}
			last = match[1]
			continue
		}
		display := rawURL
		if utf8.RuneCountInString(display) > 72 {
			display = "🔗 " + parsed.Hostname() + " 链接"
		}
		anchor := `<a href="` + html.EscapeString(rawURL) + `">` + html.EscapeString(display) + `</a>`
		length := utf8.RuneCountInString(anchor)
		if used+length > budget-3 {
			truncated = true
			break
		}
		out.WriteString(anchor)
		used += length
		if !appendEscaped(trailing) {
			truncated = true
			break
		}
		last = match[1]
	}
	if !truncated && last < len(value) && !appendEscaped(value[last:]) {
		truncated = true
	}
	if truncated {
		out.WriteString("...")
	}
	return out.String()
}

func trimTelegramURL(value string) (string, string) {
	trimmed := strings.TrimRight(value, ".,;:!?)]}，。；：！？）》】")
	return trimmed, value[len(trimmed):]
}

func (a *App) sendTelegramMessage(ctx context.Context, token, chatID, text string) error {
	_, err := a.sendTelegramPayload(ctx, token, map[string]any{
		"chat_id":                  chatID,
		"text":                     text,
		"parse_mode":               "HTML",
		"disable_web_page_preview": true,
	})
	return err
}

func (a *App) deliverTelegramMailMessage(ctx context.Context, token, chatID string, message telegramFormattedMessage) (int64, error) {
	payload := map[string]any{
		"chat_id":                  chatID,
		"text":                     message.HTML,
		"parse_mode":               "HTML",
		"disable_web_page_preview": true,
	}
	if markup := telegramCopyMarkup(message.OTP); markup != nil {
		payload["reply_markup"] = markup
	}
	result, err := a.sendTelegramPayload(ctx, token, payload)
	if err == nil {
		return result.MessageID, nil
	}
	var apiErr *telegramAPIError
	if !errors.As(err, &apiErr) || apiErr.ErrorCode != http.StatusBadRequest {
		return 0, err
	}
	fallback := map[string]any{
		"chat_id":                  chatID,
		"text":                     message.PlainText,
		"disable_web_page_preview": true,
	}
	if markup := telegramCopyMarkup(message.OTP); markup != nil {
		fallback["reply_markup"] = markup
	}
	result, err = a.sendTelegramPayload(ctx, token, fallback)
	if err != nil {
		return 0, err
	}
	return result.MessageID, nil
}

func (a *App) sendTelegramPayload(ctx context.Context, token string, payload map[string]any) (telegramSentMessage, error) {
	var result telegramSentMessage
	err := a.callTelegram(ctx, token, "sendMessage", payload, &result)
	return result, err
}

func telegramCopyMarkup(otp string) map[string]any {
	otp = strings.TrimSpace(otp)
	if otp == "" || utf8.RuneCountInString(otp) > 256 {
		return nil
	}
	return map[string]any{"inline_keyboard": [][]map[string]any{{{
		"text":      "复制验证码",
		"copy_text": map[string]string{"text": otp},
	}}}}
}

func escapeTelegramWithinBudget(value string, budget int) string {
	if budget <= 3 {
		return ""
	}
	var out strings.Builder
	used := 0
	truncated := false
	for _, r := range value {
		escaped := html.EscapeString(string(r))
		length := utf8.RuneCountInString(escaped)
		if used+length > budget-3 {
			truncated = true
			break
		}
		out.WriteString(escaped)
		used += length
	}
	if truncated {
		out.WriteString("...")
	}
	return out.String()
}

func formatTelegramMailPlainText(payload telegramMailPayload) string {
	subject := strings.TrimSpace(payload.Subject)
	if subject == "" || subject == "(no subject)" {
		subject = "（无主题）"
	}
	from := strings.TrimSpace(payload.From)
	if name := strings.TrimSpace(payload.FromName); name != "" {
		from = name + " <" + from + ">"
	}
	receivedAt := parseTime(payload.ReceivedAt)
	timeText := strings.TrimSpace(payload.ReceivedAt)
	if !receivedAt.IsZero() {
		timeText = receivedAt.Local().Format("2006-01-02 15:04:05 MST")
	}
	parts := []string{"新邮件通知", "", "主题：" + subject, "发件人：" + from, "收件邮箱：" + payload.Recipient, "收件时间：" + timeText}
	if payload.OTP != "" {
		parts = append(parts, "", "验证码", payload.OTP)
	}
	if payload.AttachmentCount > 0 {
		parts = append(parts, "", fmt.Sprintf("附件：%d 个", payload.AttachmentCount))
	}
	if body := strings.TrimSpace(payload.Body); body != "" {
		parts = append(parts, "", "正文摘要", body)
	}
	text := normalizeTelegramText(strings.Join(parts, "\n"))
	text, truncated := truncateRunes(text, telegramMessageBudget-3)
	if truncated {
		text += "..."
	}
	return text
}

func (a *App) callTelegram(ctx context.Context, token, method string, payload any, result any) error {
	token = strings.TrimSpace(token)
	if token == "" || strings.ContainsAny(token, "/\\\r\n") {
		return errors.New("Telegram Bot Token 无效")
	}
	base := strings.TrimRight(strings.TrimSpace(a.telegramURL), "/")
	endpoint := base + "/bot" + url.PathEscape(token) + "/" + method
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "NewSzxcn-Email-Telegram/1.0")
	client := &http.Client{Timeout: 12 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
	resp, err := client.Do(req)
	if err != nil {
		return errors.New("Telegram 请求失败，请检查网络连接和机器人配置")
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return err
	}
	var apiResponse telegramAPIResponse
	if err := json.Unmarshal(raw, &apiResponse); err != nil {
		return fmt.Errorf("Telegram 返回了无效响应（HTTP %d）", resp.StatusCode)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 || !apiResponse.OK {
		description := strings.TrimSpace(apiResponse.Description)
		if description == "" {
			description = fmt.Sprintf("HTTP %d", resp.StatusCode)
		}
		return &telegramAPIError{HTTPStatus: resp.StatusCode, ErrorCode: apiResponse.ErrorCode, Description: description, RetryAfter: time.Duration(apiResponse.Parameters.RetryAfter) * time.Second}
	}
	if result != nil && len(apiResponse.Result) > 0 {
		if err := json.Unmarshal(apiResponse.Result, result); err != nil {
			return err
		}
	}
	return nil
}
