// Package notify 内的 repository.go 是 notifysqlc 生成查询包之上的薄包装层。
// 职责：将 "驱动层类型"（pgtype.UUID / pgtype.Timestamptz）与 "领域类型"
// （uuid.UUID / time.Time / *time.Time）做双向转换，并把底层驱动错误统一包装
// 为 errs.AppError，使 service 层可以面向干净的 Repository 接口编程，无需直接
// 依赖 pgx 驱动细节。
//
// 设计取舍：
//   - Repository interface 提供可替换的注入点，供 F4/F5 阶段 fake 实现注入。
//   - pgtype 转换适配器刻意保持包内私有，避免对外泄露驱动依赖；若其它子包
//     需要类似映射，应各自维护或上抽到 infra/db。
//   - Insert 中 Metadata 字段用 json.Marshal 序列化；空 map 与 nil 均会写入
//     合法 JSON，保证列约束不违反。
package notify

import (
	"context"
	"encoding/json"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	notifysqlc "github.com/yan1h/agent-coding-workflow/internal/infra/notify/sqlc"
)

// Repository 抽象 notify 模块对持久层的全部依赖。所有方法接受 context 并
// 返回领域类型，既方便 service 层 mock，也方便日后切换驱动实现。
type Repository interface {
	Insert(ctx context.Context, msg Message) error
	List(ctx context.Context, userID uuid.UUID, unreadOnly bool, limit int) ([]Message, error)
	CountUnread(ctx context.Context, userID uuid.UUID) (int, error)
	MarkRead(ctx context.Context, userID, msgID uuid.UUID) error
	MarkAllRead(ctx context.Context, userID uuid.UUID) error
}

// pgRepo 是 Repository 的 Postgres 实现，内部持有 sqlc 生成的 Queries。
// 字段保持小写避免外泄；外部只能通过 NewPostgresRepository 构造。
type pgRepo struct {
	q *notifysqlc.Queries
}

// NewPostgresRepository 用现有的 pgxpool 构造 Repository 实现。pool 必须由
// 调用方负责 Close；本仓库不接管其生命周期。
func NewPostgresRepository(pool *pgxpool.Pool) Repository {
	return &pgRepo{q: notifysqlc.New(pool)}
}

// Insert 把一条 Message 持久化到 notifications 表。Metadata 先经 json.Marshal
// 序列化为字节，Link 空字符串写为 NULL（见 ptr 函数语义）。
func (r *pgRepo) Insert(ctx context.Context, msg Message) error {
	meta, err := json.Marshal(msg.Metadata)
	if err != nil {
		return errs.Wrap(err, errs.CodeInternal, "marshal metadata")
	}
	if err := r.q.InsertNotification(ctx, notifysqlc.InsertNotificationParams{
		ID:        toPgUUID(msg.ID),
		UserID:    toPgUUID(msg.UserID),
		Topic:     msg.Topic,
		Severity:  string(msg.Severity),
		Title:     msg.Title,
		Body:      msg.Body,
		Link:      ptr(msg.Link),
		Metadata:  meta,
		CreatedAt: toPgTimestamptz(msg.CreatedAt),
	}); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "insert notification")
	}
	return nil
}

// List 查询指定用户的通知列表。unreadOnly=true 时仅返回未读条目；limit 控制
// 最大返回行数，由调用方保证为合理正整数。
func (r *pgRepo) List(ctx context.Context, userID uuid.UUID, unreadOnly bool, limit int) ([]Message, error) {
	rows, err := r.q.ListNotificationsByUser(ctx, notifysqlc.ListNotificationsByUserParams{
		UserID:  toPgUUID(userID),
		Column2: unreadOnly,
		Limit:   int32(limit),
	})
	if err != nil {
		return nil, errs.Wrap(err, errs.CodeInternal, "list notifications")
	}

	msgs := make([]Message, 0, len(rows))
	for _, row := range rows {
		var meta map[string]any
		if len(row.Metadata) > 0 {
			if err := json.Unmarshal(row.Metadata, &meta); err != nil {
				return nil, errs.Wrap(err, errs.CodeInternal, "unmarshal notification metadata")
			}
		}
		msgs = append(msgs, Message{
			ID:        fromPgUUID(row.ID),
			UserID:    fromPgUUID(row.UserID),
			Topic:     row.Topic,
			Severity:  Severity(row.Severity),
			Title:     row.Title,
			Body:      row.Body,
			Link:      deref(row.Link),
			Metadata:  meta,
			ReadAt:    fromPgTimestamptzPtr(row.ReadAt),
			CreatedAt: fromPgTimestamptz(row.CreatedAt),
		})
	}
	return msgs, nil
}

// CountUnread 返回指定用户的未读通知数量。
func (r *pgRepo) CountUnread(ctx context.Context, userID uuid.UUID) (int, error) {
	n, err := r.q.CountUnreadByUser(ctx, toPgUUID(userID))
	if err != nil {
		return 0, errs.Wrap(err, errs.CodeInternal, "count unread")
	}
	return int(n), nil
}

// MarkRead 将单条通知标记为已读。msgID 与 userID 同时匹配才会更新，防止
// 用户越权操作他人通知（数据库层已作 WHERE user_id = $2 约束）。
func (r *pgRepo) MarkRead(ctx context.Context, userID, msgID uuid.UUID) error {
	if err := r.q.MarkRead(ctx, notifysqlc.MarkReadParams{
		ID:     toPgUUID(msgID),
		UserID: toPgUUID(userID),
	}); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "mark read")
	}
	return nil
}

// MarkAllRead 将指定用户的所有未读通知一次性标记为已读。
func (r *pgRepo) MarkAllRead(ctx context.Context, userID uuid.UUID) error {
	if err := r.q.MarkAllReadByUser(ctx, toPgUUID(userID)); err != nil {
		return errs.Wrap(err, errs.CodeInternal, "mark all read")
	}
	return nil
}

// --------------------------------------------------------------------------
// 包内私有适配器：pgtype ↔ 领域类型转换
// --------------------------------------------------------------------------

// toPgUUID 把领域 uuid.UUID 转为 sqlc 期望的 pgtype.UUID。
// 始终标记 Valid=true；调用方传 uuid.Nil 即视为显式写入零值。
func toPgUUID(u uuid.UUID) pgtype.UUID {
	return pgtype.UUID{Bytes: u, Valid: true}
}

// fromPgUUID 把 pgtype.UUID 转回领域 uuid.UUID。Valid=false 时返回 uuid.Nil，
// 让上层用零值判定 "无值"，无需关心驱动层的可空封装。
func fromPgUUID(p pgtype.UUID) uuid.UUID {
	if !p.Valid {
		return uuid.Nil
	}
	return uuid.UUID(p.Bytes)
}

// toPgTimestamptz 把 time.Time 转为 pgtype.Timestamptz，始终标 Valid=true。
// IsZero 的时间由 dispatcher 或上层兜底，repository 层不做额外校验。
func toPgTimestamptz(t time.Time) pgtype.Timestamptz {
	return pgtype.Timestamptz{Time: t, Valid: true}
}

// fromPgTimestamptz 把 pgtype.Timestamptz 转回 time.Time。Valid=false 时
// 返回 time.Time 零值，调用方可通过 IsZero() 判断是否有实际时间。
func fromPgTimestamptz(p pgtype.Timestamptz) time.Time {
	if !p.Valid {
		return time.Time{}
	}
	return p.Time
}

// fromPgTimestamptzPtr 把可空的 pgtype.Timestamptz 转为 *time.Time。
// Valid=false（即数据库 NULL）返回 nil，表示 "未发生"（例如通知未读时 read_at 为 NULL）；
// Valid=true 返回指向时间副本的指针，避免调用方误操作原始值。
func fromPgTimestamptzPtr(p pgtype.Timestamptz) *time.Time {
	if !p.Valid {
		return nil
	}
	t := p.Time
	return &t
}

// ptr 将非空字符串转为指针，空字符串返回 nil，令数据库列写入 NULL。
// 这与 notifications.link 列的语义一致：无链接时存 NULL 而非空字符串。
func ptr(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}

// deref 将字符串指针解引用为字符串值，nil 时返回空字符串。
// 用于把数据库 NULL link 列还原为领域 Message.Link 零值。
func deref(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}
