package service

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"html"
	"log"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"
)

// notifySendTimeout 是单条通知发送的超时时间。
const notifySendTimeout = 10 * time.Second

// notifyTextLimit 是通知正文长度上限（Telegram 上限 4096 字符，预留转义余量）。
const notifyTextLimit = 3800

// telegramAPIBase 是 Telegram Bot API 官方根地址。
const telegramAPIBase = "https://api.telegram.org"

// Notifier 向 Telegram 与全部启用的通知渠道(webhook/ntfy/bark/smtp)推送;
// 未启用或未配置的渠道静默跳过。云外 HTTP 调用,不属于 internal/oci 的职责范围。
type Notifier struct {
	settings *SettingService
	client   *http.Client
	base     string // Telegram API 根地址,测试时注入 httptest 服务
	wg       sync.WaitGroup
}

// NewNotifier 组装依赖，指向官方 API 地址。
func NewNotifier(settings *SettingService) *Notifier {
	return &Notifier{
		settings: settings,
		client:   &http.Client{Timeout: notifySendTimeout},
		base:     telegramAPIBase,
	}
}

// Send 向全部启用渠道发送一条纯文本消息;无渠道可用时返回 nil。
func (n *Notifier) Send(ctx context.Context, text string) error {
	return n.dispatch(ctx, notifyMsg{Title: "oci-portal 通知", Text: text})
}

// dispatch 把消息投递到 Telegram 与全部启用渠道;渠道间互不影响,错误合并返回供日志。
func (n *Notifier) dispatch(ctx context.Context, msg notifyMsg) error {
	msg.Text = truncateText(msg.Text, notifyTextLimit)
	var errs []error
	if err := n.sendTelegram(ctx, msg); err != nil {
		errs = append(errs, fmt.Errorf("telegram: %w", err))
	}
	for _, chType := range NotifyChannelTypes {
		if err := n.sendChannel(ctx, chType, msg); err != nil {
			errs = append(errs, fmt.Errorf("%s: %w", chType, err))
		}
	}
	return errors.Join(errs...)
}

// sendTelegram 走既有 Telegram 路径;未启用或未配置时静默跳过。
func (n *Notifier) sendTelegram(ctx context.Context, msg notifyMsg) error {
	cfg, err := n.settings.TelegramConfig(ctx)
	if err != nil {
		return err
	}
	if !cfg.Enabled || cfg.BotToken == "" || cfg.ChatID == "" {
		return nil
	}
	return n.postHTML(ctx, cfg, mdToTelegramHTML(msg.Text))
}

// sendChannel 向单个新增渠道投递;未启用或关键字段缺失时静默跳过。
func (n *Notifier) sendChannel(ctx context.Context, chType string, msg notifyMsg) error {
	switch chType {
	case ChannelWebhook:
		cfg, err := n.settings.webhookChannel(ctx)
		if err != nil || !cfg.Enabled || cfg.URL == "" {
			return err
		}
		return sendWebhook(ctx, n.client, cfg, msg)
	case ChannelNtfy:
		cfg, err := n.settings.ntfyChannel(ctx)
		if err != nil || !cfg.Enabled || cfg.Topic == "" {
			return err
		}
		return sendNtfy(ctx, n.client, cfg, msg)
	case ChannelBark:
		cfg, err := n.settings.barkChannel(ctx)
		if err != nil || !cfg.Enabled || cfg.DeviceKey == "" {
			return err
		}
		return sendBark(ctx, n.client, cfg, msg)
	case ChannelSMTP:
		cfg, err := n.settings.smtpChannel(ctx)
		if err != nil || !cfg.Enabled || cfg.Host == "" {
			return err
		}
		return sendSMTP(ctx, cfg, msg)
	}
	return nil
}

// SendAsync 异步发送并自带超时，失败只记日志，绝不影响调用链路。
func (n *Notifier) SendAsync(text string) {
	n.wg.Add(1)
	go func() {
		defer n.wg.Done()
		ctx, cancel := context.WithTimeout(context.Background(), notifySendTimeout)
		defer cancel()
		if err := n.Send(ctx, text); err != nil {
			log.Printf("notify: %v", err)
		}
	}()
}

// SendTemplateAsync 按 kind 的生效模板(自定义或默认)渲染变量后异步发送;
// 模板支持轻量 Markdown,Telegram 按 HTML 语义投递,其余渠道按纯文本投递。
func (n *Notifier) SendTemplateAsync(kind string, vars map[string]string) {
	n.wg.Add(1)
	go func() {
		defer n.wg.Done()
		ctx, cancel := context.WithTimeout(context.Background(), notifySendTimeout)
		defer cancel()
		if err := n.sendTemplate(ctx, kind, "", vars); err != nil {
			log.Printf("notify %s: %v", kind, err)
		}
	}()
}

// SendTemplateTest 用 kind 的示例变量同步发送一条测试消息;
// tplOverride 非空时按给定模板渲染(供编辑表单预览未保存内容)。
func (n *Notifier) SendTemplateTest(ctx context.Context, kind, tplOverride string) error {
	def, ok := notifyTplDefs[kind]
	if !ok {
		return fmt.Errorf("未知通知类型 %q", kind)
	}
	enabled, err := n.anyChannelEnabled(ctx)
	if err != nil {
		return err
	}
	if !enabled {
		return fmt.Errorf("没有已启用的通知渠道,请先在「通知方式」配置并启用")
	}
	vars := map[string]string{}
	for k, v := range def.Sample {
		vars[k] = v
	}
	return n.sendTemplate(ctx, kind, tplOverride, vars)
}

// anyChannelEnabled 报告是否存在至少一个已启用且配置齐全的渠道(含 Telegram)。
func (n *Notifier) anyChannelEnabled(ctx context.Context) (bool, error) {
	tg, err := n.settings.TelegramConfig(ctx)
	if err != nil {
		return false, err
	}
	if tg.Enabled && tg.BotToken != "" && tg.ChatID != "" {
		return true, nil
	}
	views, err := n.settings.NotifyChannels(ctx)
	if err != nil {
		return false, err
	}
	for _, v := range views {
		if v.Enabled {
			return true, nil
		}
	}
	return false, nil
}

// sendTemplate 渲染并分发:取生效模板 → 变量替换 → 投递到全部启用渠道。
func (n *Notifier) sendTemplate(ctx context.Context, kind, tplOverride string, vars map[string]string) error {
	tpl := tplOverride
	if strings.TrimSpace(tpl) == "" {
		custom, err := n.settings.NotifyTemplate(ctx, kind)
		if err != nil {
			log.Printf("notify template %s: %v", kind, err)
		}
		tpl = notifyTplText(custom, kind)
	}
	text := renderNotifyTemplate(tpl, vars)
	return n.dispatch(ctx, notifyMsg{Title: notifyTplLabel(kind), Text: text})
}

// SendChannelTest 向指定渠道同步发送测试消息;不看启用开关,便于保存后即时验证。
func (n *Notifier) SendChannelTest(ctx context.Context, chType string) error {
	msg := notifyMsg{Title: "oci-portal 测试消息", Text: "✅ 通知渠道配置成功:" + chType}
	switch chType {
	case ChannelWebhook:
		cfg, err := n.settings.webhookChannel(ctx)
		if err != nil {
			return err
		}
		if cfg.URL == "" {
			return fmt.Errorf("请先保存 Webhook URL")
		}
		return sendWebhook(ctx, n.client, cfg, msg)
	case ChannelNtfy:
		cfg, err := n.settings.ntfyChannel(ctx)
		if err != nil {
			return err
		}
		if cfg.Topic == "" {
			return fmt.Errorf("请先保存 ntfy topic")
		}
		return sendNtfy(ctx, n.client, cfg, msg)
	}
	return n.sendChannelTestRest(ctx, chType, msg)
}

// sendChannelTestRest 承接 SendChannelTest 的 bark/smtp 分支(拆分控制函数长度)。
func (n *Notifier) sendChannelTestRest(ctx context.Context, chType string, msg notifyMsg) error {
	switch chType {
	case ChannelBark:
		cfg, err := n.settings.barkChannel(ctx)
		if err != nil {
			return err
		}
		if cfg.DeviceKey == "" {
			return fmt.Errorf("请先保存 Bark device key")
		}
		return sendBark(ctx, n.client, cfg, msg)
	case ChannelSMTP:
		cfg, err := n.settings.smtpChannel(ctx)
		if err != nil {
			return err
		}
		if cfg.Host == "" || cfg.From == "" || cfg.To == "" {
			return fmt.Errorf("请先保存 SMTP 主机、发件人与收件人")
		}
		return sendSMTP(ctx, cfg, msg)
	}
	return ErrUnknownNotifyChannel
}

// Wait 等待全部在途异步通知发完，供退出前收尾与测试同步。
func (n *Notifier) Wait() {
	n.wg.Wait()
}

// Test 同步发送一条测试消息，配置无效时透出可读错误。
func (n *Notifier) Test(ctx context.Context) error {
	cfg, err := n.settings.TelegramConfig(ctx)
	if err != nil {
		return err
	}
	if cfg.BotToken == "" || cfg.ChatID == "" {
		return fmt.Errorf("telegram test: bot token and chat id are required")
	}
	return n.post(ctx, cfg, "✅ oci-portal 测试消息：Telegram 通知配置成功")
}

// post 调用 sendMessage；先截断再做 HTML 转义，避免把转义实体截成两半。
func (n *Notifier) post(ctx context.Context, cfg TelegramConfig, text string) error {
	return n.postHTML(ctx, cfg, html.EscapeString(truncateText(text, notifyTextLimit)))
}

// postHTML 调用 sendMessage 发送已按 Telegram HTML 组装的文本(调用方保证转义)。
func (n *Notifier) postHTML(ctx context.Context, cfg TelegramConfig, htmlText string) error {
	body, err := json.Marshal(map[string]any{
		"chat_id":                  cfg.ChatID,
		"text":                     htmlText,
		"parse_mode":               "HTML",
		"disable_web_page_preview": true,
	})
	if err != nil {
		return fmt.Errorf("telegram send: %w", err)
	}
	apiURL := fmt.Sprintf("%s/bot%s/sendMessage", n.base, cfg.BotToken)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("telegram send: %w", sanitizeURLError(err))
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := n.client.Do(req)
	if err != nil {
		return fmt.Errorf("telegram send: %w", sanitizeURLError(err))
	}
	defer resp.Body.Close()
	return parseTelegramResponse(resp)
}

// telegramResponse 是 Telegram Bot API 的通用响应包。
type telegramResponse struct {
	OK          bool   `json:"ok"`
	Description string `json:"description"`
}

// parseTelegramResponse 解析响应；ok=false 时透出 Telegram 的错误描述。
func parseTelegramResponse(resp *http.Response) error {
	var r telegramResponse
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		return fmt.Errorf("telegram response (%d): %w", resp.StatusCode, err)
	}
	if !r.OK {
		return fmt.Errorf("telegram: %s", r.Description)
	}
	return nil
}

// sanitizeURLError 剥掉 *url.Error 外壳只保留底层原因：
// 其 Error() 会拼出完整请求 URL，而路径中含 bot token，
// 原样向上返回会把 token 泄漏进日志与接口响应。
func sanitizeURLError(err error) error {
	var uerr *url.Error
	if errors.As(err, &uerr) {
		return uerr.Err
	}
	return err
}

// truncateText 按 rune 截断超长文本，追加省略号提示。
func truncateText(text string, limit int) string {
	runes := []rune(text)
	if len(runes) <= limit {
		return text
	}
	return string(runes[:limit]) + "…"
}
