package workbench

import (
	"context"
	"encoding/json"
	"errors"
	"net/url"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/jackc/pgx/v5"
	"github.com/redis/go-redis/v9"
)

// InboxMessage 是站内消息的一行:一条消息对应一个收件人(admin 或 portal)。
// 管理员广播时按目标用户逐行落库,因此每行独立维护 read_at 已读回执。
type InboxMessage struct {
	ID              string          `json:"id"`
	RecipientKind   string          `json:"recipient_kind"`
	RecipientUserID string          `json:"recipient_user_id"`
	SenderType      string          `json:"sender_type"`
	Category        string          `json:"category"`
	Title           string          `json:"title"`
	Body            string          `json:"body"`
	Link            string          `json:"link"`
	Payload         json.RawMessage `json:"payload,omitempty"`
	ReadAt          *time.Time      `json:"read_at,omitempty"`
	CreatedAt       time.Time       `json:"created_at"`
}

type InboxInput struct {
	RecipientKind string          `json:"recipient_kind"`
	Category      string          `json:"category"`
	Title         string          `json:"title"`
	Body          string          `json:"body"`
	Link          string          `json:"link"`
	Payload       json.RawMessage `json:"payload,omitempty"`
}

// InboxService 负责将 outbox 事件物化为站内消息,并对外提供收件箱读写。
// 未读数以 PostgreSQL 为权威源(部分索引 COUNT 快速),Redis 仅作实时 PUBLISH
// 提示(为未来 SSE 预留);因此未读计数不会因 Redis 抖动或消息漂移而失真。
type InboxService struct {
	assets  *Service
	redis   *redis.Client
	channel string
}

func NewInboxService(assets *Service, client *redis.Client, channel string) *InboxService {
	return &InboxService{assets: assets, redis: client, channel: channel}
}

const inboxSelect = `SELECT id::text,recipient_kind,recipient_user_id::text,sender_type,category,title,body,link,payload,read_at,created_at FROM gateway.inbox_messages`

func scanInboxMessage(row pgx.Row) (InboxMessage, error) {
	var m InboxMessage
	err := row.Scan(&m.ID, &m.RecipientKind, &m.RecipientUserID, &m.SenderType, &m.Category, &m.Title, &m.Body, &m.Link, &m.Payload, &m.ReadAt, &m.CreatedAt)
	return m, err
}

// inboxDraft 描述一个 outbox 事件要落成的一条站内消息(收件人解析方式不同)。
type inboxDraft struct {
	RecipientKind string // admin | portal
	Category      string
	Title         string
	Body          string
	Link          string
	UserID        string // 直接收件人(从 payload 取),空串表示需额外解析
	AllAdmins     bool   // 收件人 = 全部启用管理员
	RequestUser   bool   // 收件人 = model_access_requests.portal_user_id(payload.request_id)
	NotifyPref    bool   // 收件人 = payload.portal_user_id,且其登录通知偏好开启
}

func payloadValue(payload json.RawMessage, key string) string {
	var values map[string]any
	if err := json.Unmarshal(payload, &values); err != nil {
		return ""
	}
	value, ok := values[key]
	if !ok {
		return ""
	}
	switch v := value.(type) {
	case string:
		return v
	case float64:
		return strconv.FormatFloat(v, 'f', -1, 64)
	}
	encoded, _ := json.Marshal(value)
	return string(encoded)
}

// inboxPlan 把事件类型映射成站内消息草稿(纯函数,便于单测)。未知事件返回 nil,
// 不构成错误——并非所有 outbox 事件都需要站内信。
func inboxPlan(eventType string, payload json.RawMessage) []inboxDraft {
	switch eventType {
	case "model_access.requested":
		return []inboxDraft{{RecipientKind: "admin", Category: "approval", Title: "新的模型访问申请", Body: "用户申请访问模型 " + payloadValue(payload, "model"), Link: "/security/governance", AllAdmins: true}}
	case "model_access.decided":
		text := "已批准"
		if payloadValue(payload, "status") == "rejected" {
			text = "已驳回"
		}
		return []inboxDraft{{RecipientKind: "portal", Category: "approval", Title: "模型申请已处理", Body: "您的模型访问申请已被" + text, Link: "/portal/access", RequestUser: true}}
	case "marketplace.installed":
		return []inboxDraft{{RecipientKind: "portal", Category: "resource", Title: "资源已安装", Body: "资源 " + payloadValue(payload, "code") + " 已安装到您的工作区", Link: "/portal/marketplace", UserID: payloadValue(payload, "portal_user_id")}}
	case "knowledge_document.ready":
		return []inboxDraft{{RecipientKind: "admin", Category: "system", Title: "知识文档已入库", Body: "文档已分块入库(" + payloadValue(payload, "chunk_count") + " 切片)", Link: "/assets/knowledge", UserID: payloadValue(payload, "actor_id")}}
	case "knowledge_document.reprocessed":
		return []inboxDraft{{RecipientKind: "admin", Category: "system", Title: "知识文档已重新处理", Body: "文档已重新分块入库", Link: "/assets/knowledge", UserID: payloadValue(payload, "actor_id")}}
	case "knowledge_document.embedding_failed":
		return []inboxDraft{{RecipientKind: "admin", Category: "system", Title: "知识文档向量化失败", Body: "文档已入库但向量化失败,检索将回退全文检索;请检查 Ollama 后重新处理", Link: "/assets/knowledge", UserID: payloadValue(payload, "actor_id")}}
	case "scheduled_task.completed":
		return []inboxDraft{{RecipientKind: "admin", Category: "task_result", Title: "定时任务已执行", Body: "定时任务 " + payloadValue(payload, "task_code") + " 已完成", Link: "/system/scheduled-tasks", UserID: payloadValue(payload, "actor_id")}}
	case "scheduled_task.failed":
		return []inboxDraft{{RecipientKind: "admin", Category: "task_result", Title: "定时任务执行失败", Body: "定时任务 " + payloadValue(payload, "task_code") + " 执行失败: " + payloadValue(payload, "error"), Link: "/system/scheduled-tasks", UserID: payloadValue(payload, "actor_id")}}
	case "security.login_detected":
		ip := payloadValue(payload, "ip")
		if ip == "" {
			ip = "未知地址"
		}
		return []inboxDraft{{RecipientKind: "portal", Category: "security", Title: "新设备登录提醒", Body: "你的账号刚刚从 " + ip + " 登录,如非本人操作请立即修改密码", Link: "/portal/security", UserID: payloadValue(payload, "portal_user_id"), NotifyPref: true}}
	}
	return nil
}

func (s *InboxService) resolveRecipients(ctx context.Context, draft inboxDraft, payload json.RawMessage) ([]string, error) {
	switch {
	case draft.UserID != "" && draft.NotifyPref:
		// 登录提醒:尊重账号的安全偏好(默认开启)。
		var notify bool
		if err := s.assets.pool.QueryRow(ctx, `SELECT COALESCE((SELECT login_notify FROM gateway.portal_security_prefs WHERE portal_user_id=$1),true)`, draft.UserID).Scan(&notify); err != nil {
			return nil, err
		}
		if !notify {
			return nil, nil
		}
		return []string{draft.UserID}, nil
	case draft.UserID != "":
		return []string{draft.UserID}, nil
	case draft.RequestUser:
		var userID string
		if err := s.assets.pool.QueryRow(ctx, `SELECT portal_user_id::text FROM gateway.model_access_requests WHERE id=$1`, payloadValue(payload, "request_id")).Scan(&userID); err != nil {
			return nil, err
		}
		return []string{userID}, nil
	case draft.AllAdmins:
		rows, err := s.assets.pool.Query(ctx, `SELECT id::text FROM gateway.admin_accounts WHERE active`)
		if err != nil {
			return nil, err
		}
		defer rows.Close()
		ids := []string{}
		for rows.Next() {
			var id string
			if err := rows.Scan(&id); err != nil {
				return nil, err
			}
			ids = append(ids, id)
		}
		return ids, rows.Err()
	}
	return nil, nil
}

// Materialize 把一条 outbox 事件落成站内消息。以 (source_event_id, 收件人) 幂等:
// 事件重放时 ON CONFLICT DO NOTHING,不产生重复消息,也不报错。
func (s *InboxService) Materialize(ctx context.Context, eventID, eventType string, payload json.RawMessage) error {
	for _, draft := range inboxPlan(eventType, payload) {
		recipients, err := s.resolveRecipients(ctx, draft, payload)
		if err != nil {
			return err
		}
		for _, userID := range recipients {
			if err := s.notify(ctx, eventID, draft, userID, payload); err != nil {
				return err
			}
		}
	}
	return nil
}

func (s *InboxService) notify(ctx context.Context, sourceEventID string, draft inboxDraft, userID string, payload json.RawMessage) error {
	id, err := newUUID()
	if err != nil {
		return err
	}
	// 数据库 CHECK 按字符数(length)校验,Go 的 len() 是字节数;多字节文本下
	// 字节校验通过但字符数超限,INSERT 会失败并把事件永远卡在 pending。
	// 落库前按 rune 数截断,保证任何语言正文都能入库。
	title := runeTruncate(draft.Title, 256)
	body := runeTruncate(draft.Body, 4000)
	link := runeTruncate(draft.Link, 512)
	tag, err := s.assets.pool.Exec(ctx, `INSERT INTO gateway.inbox_messages(id,source_event_id,recipient_kind,recipient_user_id,sender_type,category,title,body,link,payload)
VALUES($1,$2,$3,$4,'system',$5,$6,$7,$8,$9)
ON CONFLICT (source_event_id, recipient_kind, recipient_user_id) WHERE source_event_id IS NOT NULL DO NOTHING`,
		id, sourceEventID, draft.RecipientKind, userID, draft.Category, title, body, link, payload)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return nil // 同一来源事件对同一收件人的重放,跳过
	}
	s.publish(draft.RecipientKind, userID)
	return nil
}

// runeTruncate 按 rune(字符)数截断;超出 limit 个字符时截到第 limit 个
// 完整 rune,绝不产生无效 UTF-8。
func runeTruncate(value string, limit int) string {
	if utf8.RuneCountInString(value) <= limit {
		return value
	}
	runes := []rune(value)
	return string(runes[:limit])
}

// publish 仅作实时提示(未来 SSE 可订阅);收件箱未读数以 DB 为准。
func (s *InboxService) publish(kind, userID string) {
	if s.redis == nil {
		return
	}
	_ = s.redis.Publish(context.WithoutCancel(context.Background()), s.channel, kind+":"+userID).Err()
}

// List 返回某个收件人的收件箱(倒序)。
func (s *InboxService) List(ctx context.Context, kind, userID string, limit int) ([]InboxMessage, error) {
	if limit < 1 {
		limit = 50
	}
	if limit > 200 {
		limit = 200
	}
	rows, err := s.assets.pool.Query(ctx, inboxSelect+` WHERE recipient_kind=$1 AND recipient_user_id=$2 ORDER BY created_at DESC LIMIT $3`, kind, userID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []InboxMessage{}
	for rows.Next() {
		m, err := scanInboxMessage(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, m)
	}
	return items, rows.Err()
}

// UnreadCount 以 PostgreSQL 为权威源统计未读消息(部分索引快速扫描)。
func (s *InboxService) UnreadCount(ctx context.Context, kind, userID string) (int, error) {
	var count int
	err := s.assets.pool.QueryRow(ctx, `SELECT count(*) FROM gateway.inbox_messages WHERE recipient_kind=$1 AND recipient_user_id=$2 AND read_at IS NULL`, kind, userID).Scan(&count)
	return count, err
}

// MarkRead 把某条消息标记为已读;只允许收件人本人操作。
func (s *InboxService) MarkRead(ctx context.Context, id, kind, userID string) (bool, error) {
	tag, err := s.assets.pool.Exec(ctx, `UPDATE gateway.inbox_messages SET read_at=coalesce(read_at,clock_timestamp()) WHERE id=$1 AND recipient_kind=$2 AND recipient_user_id=$3`, id, kind, userID)
	if err != nil {
		return false, err
	}
	return tag.RowsAffected() > 0, nil
}

// MarkAllRead 把某收件人的全部未读标记为已读,返回标记条数。
func (s *InboxService) MarkAllRead(ctx context.Context, kind, userID string) (int, error) {
	tag, err := s.assets.pool.Exec(ctx, `UPDATE gateway.inbox_messages SET read_at=clock_timestamp() WHERE recipient_kind=$1 AND recipient_user_id=$2 AND read_at IS NULL`, kind, userID)
	if err != nil {
		return 0, err
	}
	return int(tag.RowsAffected()), nil
}

// AdminList 返回管理端消息中心列表。scope=mine 看发给自己(admin)的消息;
// scope=broadcasts 看管理员发起的广播(portal 收件)。其余 scope 全部返回。
func (s *InboxService) AdminList(ctx context.Context, adminID, scope string, limit int) ([]InboxMessage, error) {
	if limit < 1 {
		limit = 50
	}
	if limit > 500 {
		limit = 500
	}
	query, args := inboxSelect+` WHERE`, []any{}
	switch scope {
	case "mine":
		query += ` recipient_kind='admin' AND recipient_user_id=$1`
		args = append(args, adminID)
	case "broadcasts":
		query += ` sender_type='admin' AND recipient_kind='portal'`
	default:
		query += ` recipient_kind='admin' AND recipient_user_id=$1`
		args = append(args, adminID)
	}
	args = append(args, limit)
	query += ` ORDER BY created_at DESC LIMIT $` + strconv.Itoa(len(args))
	rows, err := s.assets.pool.Query(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := []InboxMessage{}
	for rows.Next() {
		m, err := scanInboxMessage(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, m)
	}
	return items, rows.Err()
}

// Broadcast 由管理员向 portal(可按部门过滤)或全部 admin 发广播;逐收件人落库,
// 返回实际送达用户数。sender_type='admin',无 source_event_id(不与事件幂等键冲突)。
func (s *InboxService) Broadcast(ctx context.Context, input InboxInput, departmentIDs []string, actorID string) (int, error) {
	input.RecipientKind = strings.TrimSpace(input.RecipientKind)
	input.Category = strings.TrimSpace(input.Category)
	input.Title = strings.TrimSpace(input.Title)
	input.Body = strings.TrimSpace(input.Body)
	input.Link = strings.TrimSpace(input.Link)
	if input.RecipientKind != "portal" && input.RecipientKind != "admin" {
		return 0, errors.New("广播对象必须是 admin 或 portal")
	}
	if input.Title == "" || utf8.RuneCountInString(input.Title) > 256 || utf8.RuneCountInString(input.Body) > 4000 || utf8.RuneCountInString(input.Link) > 512 {
		return 0, errors.New("广播标题或正文格式无效")
	}
	if !validInboxLink(input.Link) {
		return 0, errors.New("跳转链接仅支持站内绝对路径或 http(s) 地址")
	}
	if input.Category != "system" && input.Category != "approval" && input.Category != "task_result" && input.Category != "resource" {
		input.Category = "system"
	}
	var query string
	var args []any
	if input.RecipientKind == "admin" {
		query = `SELECT id::text FROM gateway.admin_accounts WHERE active`
	} else {
		query = `SELECT id::text FROM gateway.portal_users WHERE active AND (array_length($1::uuid[],1) IS NULL OR department_id = ANY($1::uuid[]))`
		args = append(args, departmentIDs)
	}
	rows, err := s.assets.pool.Query(ctx, query, args...)
	if err != nil {
		return 0, err
	}
	defer rows.Close()
	recipients := []string{}
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return 0, err
		}
		recipients = append(recipients, id)
	}
	if err := rows.Err(); err != nil {
		return 0, err
	}
	// 广播整体包在单个事务里:中途失败不留半套消息,收件人数目与落库一致。
	tx, err := s.assets.pool.Begin(ctx)
	if err != nil {
		return 0, err
	}
	defer rollback(ctx, tx)
	count := 0
	for _, userID := range recipients {
		id, idErr := newUUID()
		if idErr != nil {
			return 0, idErr
		}
		tag, insertErr := tx.Exec(ctx, `INSERT INTO gateway.inbox_messages(id,recipient_kind,recipient_user_id,sender_type,category,title,body,link,payload) VALUES($1,$2,$3,'admin',$4,$5,$6,$7,$8)`, id, input.RecipientKind, userID, input.Category, input.Title, input.Body, input.Link, input.Payload)
		if insertErr != nil {
			return 0, insertErr
		}
		count += int(tag.RowsAffected())
		s.publish(input.RecipientKind, userID)
	}
	if err := tx.Commit(ctx); err != nil {
		return 0, err
	}
	return count, nil
}

// validInboxLink blocks executable and browser-special schemes. Empty links are allowed;
// internal links must be root-relative, while external links are limited to HTTP(S).
func validInboxLink(link string) bool {
	if link == "" {
		return true
	}
	if strings.HasPrefix(link, "/") && !strings.HasPrefix(link, "//") {
		return true
	}
	parsed, err := url.ParseRequestURI(link)
	return err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != ""
}
