package cpnotice

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
)

const (
	sourceUserOutbox         = "user_outbox"
	deliveryStatusDelivering = "delivering"
	deliveryStatusDelivered  = "delivered"
	deliveryStatusRetryable  = "retryable"
	deliveryStatusFailed     = "failed"
)

// CPDelivery is one concrete IM side effect derived from a CP user_outbox fact.
type CPDelivery struct {
	AppCode      string
	EventID      string
	EventType    string
	Channel      string
	NoticeType   string
	SenderUserID int64
	TargetUserID int64
	GroupID      string
	PayloadJSON  []byte
	RetryCount   int
	CreatedAtMS  int64
}

// MySQLRepository stores CP delivery state in notice_delivery_events only; it never mutates user_outbox.
type MySQLRepository struct {
	db *sql.DB
}

// NewMySQLRepository creates CP notice repository.
func NewMySQLRepository(db *sql.DB) *MySQLRepository {
	return &MySQLRepository{db: db}
}

// ClaimCPDelivery claims one per-channel CP delivery row. Delivered rows are acknowledged as already handled.
func (r *MySQLRepository) ClaimCPDelivery(ctx context.Context, workerID string, delivery CPDelivery, lockTTL time.Duration) (CPDelivery, bool, error) {
	if r == nil || r.db == nil {
		return CPDelivery{}, false, xerr.New(xerr.Unavailable, "notice repository is not configured")
	}
	workerID = strings.TrimSpace(workerID)
	if workerID == "" {
		return CPDelivery{}, false, xerr.New(xerr.InvalidArgument, "worker_id is required")
	}
	if lockTTL <= 0 {
		lockTTL = 30 * time.Second
	}
	if err := validateDelivery(delivery); err != nil {
		return CPDelivery{}, false, err
	}
	delivery.AppCode = appcode.Normalize(delivery.AppCode)
	now := time.Now()
	lockUntilMS := now.Add(lockTTL).UnixMilli()
	nowMS := now.UnixMilli()

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return CPDelivery{}, false, err
	}
	defer func() { _ = tx.Rollback() }()

	status, retryCount, lockUntil, nextRetry, err := r.lockDeliveryRow(ctx, tx, delivery)
	if errors.Is(err, sql.ErrNoRows) {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO notice_delivery_events (
				source_name, app_code, source_event_id, channel, notice_type, target_user_id,
				status, retry_count, locked_by, lock_until_ms, next_retry_at_ms,
				payload_json, last_error, delivered_at_ms, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 0, CAST(? AS JSON), NULL, 0, ?, ?)`,
			sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel, delivery.NoticeType, delivery.TargetUserID,
			deliveryStatusDelivering, workerID, lockUntilMS, string(delivery.PayloadJSON), nowMS, nowMS,
		); err != nil {
			return CPDelivery{}, false, err
		}
		delivery.RetryCount = 0
		if err := tx.Commit(); err != nil {
			return CPDelivery{}, false, err
		}
		return delivery, true, nil
	}
	if err != nil {
		return CPDelivery{}, false, err
	}
	if status == deliveryStatusDelivered || status == deliveryStatusFailed {
		return CPDelivery{}, false, tx.Commit()
	}
	if status == deliveryStatusDelivering && lockUntil > nowMS {
		return CPDelivery{}, false, tx.Commit()
	}
	if status == deliveryStatusRetryable && nextRetry > nowMS {
		return CPDelivery{}, false, tx.Commit()
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE notice_delivery_events
		SET status = ?, notice_type = ?, target_user_id = ?, retry_count = ?, locked_by = ?,
		    lock_until_ms = ?, next_retry_at_ms = 0, payload_json = CAST(? AS JSON),
		    last_error = NULL, updated_at_ms = ?
		WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
		deliveryStatusDelivering, delivery.NoticeType, delivery.TargetUserID, retryCount, workerID,
		lockUntilMS, string(delivery.PayloadJSON), nowMS,
		sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
	); err != nil {
		return CPDelivery{}, false, err
	}
	delivery.RetryCount = retryCount
	if err := tx.Commit(); err != nil {
		return CPDelivery{}, false, err
	}
	return delivery, true, nil
}

func (r *MySQLRepository) lockDeliveryRow(ctx context.Context, tx *sql.Tx, delivery CPDelivery) (string, int, int64, int64, error) {
	var status string
	var retryCount int
	var lockUntil int64
	var nextRetry int64
	err := tx.QueryRowContext(ctx, `
		SELECT status, retry_count, lock_until_ms, next_retry_at_ms
		FROM notice_delivery_events
		WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?
		FOR UPDATE`,
		sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
	).Scan(&status, &retryCount, &lockUntil, &nextRetry)
	return status, retryCount, lockUntil, nextRetry, err
}

// MarkCPDelivered records a successful Tencent IM response for the specific channel.
func (r *MySQLRepository) MarkCPDelivered(ctx context.Context, delivery CPDelivery, deliveredPayload []byte, nowMs int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "notice repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE notice_delivery_events
		SET status = ?, payload_json = CAST(? AS JSON), last_error = NULL,
		    delivered_at_ms = ?, locked_by = '', lock_until_ms = 0, next_retry_at_ms = 0, updated_at_ms = ?
		WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
		deliveryStatusDelivered, string(deliveredPayload), nowMs, nowMs,
		sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
	)
	return err
}

// MarkCPRetryable releases the current lock and schedules a channel-level retry.
func (r *MySQLRepository) MarkCPRetryable(ctx context.Context, delivery CPDelivery, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "notice repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE notice_delivery_events
		SET status = ?, retry_count = ?, locked_by = '', lock_until_ms = 0,
		    next_retry_at_ms = ?, last_error = ?, updated_at_ms = ?
		WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
		deliveryStatusRetryable, retryCount, nextRetryAtMS, truncateError(lastErr), nowMs,
		sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
	)
	return err
}

// MarkCPFailed dead-letters a channel-level delivery after retry budget is exhausted.
func (r *MySQLRepository) MarkCPFailed(ctx context.Context, delivery CPDelivery, retryCount int, lastErr string, nowMs int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "notice repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE notice_delivery_events
		SET status = ?, retry_count = ?, locked_by = '', lock_until_ms = 0,
		    next_retry_at_ms = 0, last_error = ?, updated_at_ms = ?
		WHERE source_name = ? AND app_code = ? AND source_event_id = ? AND channel = ?`,
		deliveryStatusFailed, retryCount, truncateError(lastErr), nowMs,
		sourceUserOutbox, delivery.AppCode, delivery.EventID, delivery.Channel,
	)
	return err
}

func validateDelivery(delivery CPDelivery) error {
	if appcode.Normalize(delivery.AppCode) == "" || strings.TrimSpace(delivery.EventID) == "" ||
		strings.TrimSpace(delivery.Channel) == "" || strings.TrimSpace(delivery.NoticeType) == "" ||
		len(delivery.PayloadJSON) == 0 {
		return xerr.New(xerr.InvalidArgument, "cp delivery is incomplete")
	}
	if isTencentIMC2CChannel(delivery.Channel) && delivery.TargetUserID <= 0 {
		return xerr.New(xerr.InvalidArgument, "cp c2c delivery target_user_id is required")
	}
	if isUserAuthoredTencentIMC2CChannel(delivery.Channel) && delivery.SenderUserID <= 0 {
		return xerr.New(xerr.InvalidArgument, "cp c2c delivery sender_user_id is required")
	}
	if (delivery.Channel == channelTencentIMRoom || delivery.Channel == channelTencentIMRoomSender || delivery.Channel == channelTencentIMRegion) && strings.TrimSpace(delivery.GroupID) == "" {
		return xerr.New(xerr.InvalidArgument, "cp group delivery group_id is required")
	}
	return nil
}

func isTencentIMC2CChannel(channel string) bool {
	switch channel {
	case channelTencentIMC2C, channelTencentIMC2CAcceptor, channelTencentIMSystemC2C:
		return true
	default:
		return false
	}
}

func isUserAuthoredTencentIMC2CChannel(channel string) bool {
	// 系统 C2C 必须让 REST client 使用管理员账号；只有关系双方代发的 C2C 才要求 sender_user_id。
	return channel == channelTencentIMC2C || channel == channelTencentIMC2CAcceptor
}

func truncateError(value string) string {
	value = strings.TrimSpace(value)
	if len(value) <= 512 {
		return value
	}
	return value[:512]
}
