package mysql

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

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
)

// SaveBroadcastOutbox 插入一条持久化播报 outbox。
// 主键是 (app_code,event_id)，重复写入会返回已有记录，保证 room outbox 重放、gRPC 重试和 cron 重跑不会重复发播报。
func (r *Repository) SaveBroadcastOutbox(ctx context.Context, record broadcastdomain.OutboxRecord) (bool, broadcastdomain.OutboxRecord, error) {
	if r == nil || r.db == nil {
		return false, broadcastdomain.OutboxRecord{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	record.AppCode = appcode.FromContext(ctx)
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO im_broadcast_outbox (
			app_code, event_id, scope, group_id, broadcast_type, payload_json, status,
			attempt_count, next_retry_at_ms, last_error, locked_by, locked_until_ms,
			created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, '', 0, ?, ?)`,
		record.AppCode,
		record.EventID,
		record.Scope,
		record.GroupID,
		record.BroadcastType,
		record.PayloadJSON,
		record.Status,
		record.AttemptCount,
		record.NextRetryAtMS,
		record.LastError,
		record.CreatedAtMS,
		record.UpdatedAtMS,
	)
	if err == nil {
		return true, record, nil
	}
	if !isMySQLDuplicate(err) {
		return false, broadcastdomain.OutboxRecord{}, err
	}
	// duplicate 不能直接当成功丢弃；返回已有记录让上层知道当前状态和目标 group_id。
	existing, exists, findErr := r.GetBroadcastOutbox(ctx, record.EventID)
	if findErr != nil {
		return false, broadcastdomain.OutboxRecord{}, findErr
	}
	if !exists {
		return false, broadcastdomain.OutboxRecord{}, xerr.New(xerr.Conflict, "broadcast outbox event_id already exists")
	}
	return false, existing, nil
}

// ListBroadcastOutboxAppCodes 枚举实际存在的租户键，常驻 worker 再按 App 使用现有 claim 前缀索引。
// 主键以 app_code 开头，DISTINCT 只枚举少量租户，不需要为了跨 App 投递移除 app_code 条件扫描热表。
func (r *Repository) ListBroadcastOutboxAppCodes(ctx context.Context) ([]string, error) {
	rows, err := r.db.QueryContext(ctx, `SELECT DISTINCT app_code FROM im_broadcast_outbox ORDER BY app_code`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	appCodes := make([]string, 0, 4)
	for rows.Next() {
		var appCode string
		if err := rows.Scan(&appCode); err != nil {
			return nil, err
		}
		appCodes = append(appCodes, appCode)
	}
	return appCodes, rows.Err()
}

// GetBroadcastOutbox 按幂等键读取一条播报 outbox，测试和后台排障都会用到。
func (r *Repository) GetBroadcastOutbox(ctx context.Context, eventID string) (broadcastdomain.OutboxRecord, bool, error) {
	row := r.db.QueryRowContext(ctx, `
		SELECT app_code, event_id, scope, group_id, broadcast_type, CAST(payload_json AS CHAR),
		       status, attempt_count, next_retry_at_ms, last_error, locked_by, locked_until_ms,
		       created_at_ms, updated_at_ms
		FROM im_broadcast_outbox
		WHERE app_code = ? AND event_id = ?`,
		appcode.FromContext(ctx), eventID,
	)
	record, err := scanBroadcastOutbox(row)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return broadcastdomain.OutboxRecord{}, false, nil
		}
		return broadcastdomain.OutboxRecord{}, false, err
	}
	return record, true, nil
}

// ClaimPendingBroadcastOutbox 抢占一页可投递播报消息。
// FOR UPDATE SKIP LOCKED 让多个 worker 可以并行拉取不同记录；locked_until_ms 处理 worker 崩溃后的接管。
func (r *Repository) ClaimPendingBroadcastOutbox(ctx context.Context, workerID string, nowMS int64, lockUntilMS int64, limit int, maxRetry int) ([]broadcastdomain.OutboxRecord, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if strings.TrimSpace(workerID) == "" {
		workerID = "activity-broadcast"
	}
	if limit <= 0 {
		limit = 100
	}
	if maxRetry <= 0 {
		maxRetry = 8
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	appCode := appcode.FromContext(ctx)
	records := make([]broadcastdomain.OutboxRecord, 0, limit)
	claimBranches := []struct {
		query string
		args  []any
	}{
		{
			query: `
		SELECT app_code, event_id, scope, group_id, broadcast_type, CAST(payload_json AS CHAR),
		       status, attempt_count, next_retry_at_ms, last_error, locked_by, locked_until_ms,
		       created_at_ms, updated_at_ms
		FROM im_broadcast_outbox FORCE INDEX (idx_im_broadcast_outbox_pending)
		WHERE app_code = ?
		  AND attempt_count < ?
		  AND status IN (?, ?)
		  AND next_retry_at_ms <= ?
		ORDER BY created_at_ms ASC
		LIMIT ?
		FOR UPDATE SKIP LOCKED`,
			args: []any{appCode, maxRetry, broadcastdomain.StatusPending, broadcastdomain.StatusRetryable, nowMS},
		},
		{
			query: `
		SELECT app_code, event_id, scope, group_id, broadcast_type, CAST(payload_json AS CHAR),
		       status, attempt_count, next_retry_at_ms, last_error, locked_by, locked_until_ms,
		       created_at_ms, updated_at_ms
		FROM im_broadcast_outbox FORCE INDEX (idx_im_broadcast_outbox_lock)
		WHERE app_code = ?
		  AND attempt_count < ?
		  AND status = ?
		  AND locked_until_ms <= ?
		ORDER BY created_at_ms ASC
		LIMIT ?
		FOR UPDATE SKIP LOCKED`,
			args: []any{appCode, maxRetry, broadcastdomain.StatusDelivering, nowMS},
		},
	}
	for _, branch := range claimBranches {
		remaining := limit - len(records)
		if remaining <= 0 {
			break
		}
		args := append(append([]any{}, branch.args...), remaining)
		branchRecords, err := queryBroadcastOutboxRecords(ctx, tx, branch.query, args...)
		if err != nil {
			_ = tx.Rollback()
			return nil, err
		}
		records = append(records, branchRecords...)
	}
	for _, record := range records {
		// 先在同一事务里改成 delivering，再提交给调用方发送，避免两个 worker 同时发送同一 event_id。
		if _, err := tx.ExecContext(ctx, `
			UPDATE im_broadcast_outbox
			SET status = ?, locked_by = ?, locked_until_ms = ?, updated_at_ms = ?
			WHERE app_code = ? AND event_id = ?`,
			broadcastdomain.StatusDelivering,
			workerID,
			lockUntilMS,
			nowMS,
			record.AppCode,
			record.EventID,
		); err != nil {
			_ = tx.Rollback()
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	for i := range records {
		records[i].Status = broadcastdomain.StatusDelivering
		records[i].LockedBy = workerID
		records[i].LockedUntilMS = lockUntilMS
	}
	return records, nil
}

func queryBroadcastOutboxRecords(ctx context.Context, tx *sql.Tx, query string, args ...any) ([]broadcastdomain.OutboxRecord, error) {
	rows, err := tx.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	records := make([]broadcastdomain.OutboxRecord, 0)
	for rows.Next() {
		record, err := scanBroadcastOutbox(rows)
		if err != nil {
			return nil, err
		}
		records = append(records, record)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return records, nil
}

// MarkBroadcastOutboxDelivered 在腾讯 REST 接受消息后标记 delivered。
// delivered 只代表服务端投递成功，不代表所有客户端已在线收到或已展示。
func (r *Repository) MarkBroadcastOutboxDelivered(ctx context.Context, eventID string, nowMS int64) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE im_broadcast_outbox
		SET status = ?, locked_by = '', locked_until_ms = 0, last_error = '', updated_at_ms = ?
		WHERE app_code = ? AND event_id = ?`,
		broadcastdomain.StatusDelivered,
		nowMS,
		appcode.FromContext(ctx),
		eventID,
	)
	return err
}

// MarkBroadcastOutboxFailed 释放失败记录，未达上限时进入 retryable，达到上限时进入 failed。
// WHERE locked_by 限定当前 worker，防止锁过期后旧 worker 把新 worker 的处理结果覆盖掉。
func (r *Repository) MarkBroadcastOutboxFailed(ctx context.Context, eventID string, workerID string, nowMS int64, nextRetryAtMS int64, maxRetry int, lastError string) error {
	if maxRetry <= 0 {
		maxRetry = 8
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE im_broadcast_outbox
		SET status = CASE WHEN attempt_count + 1 >= ? THEN ? ELSE ? END,
		    attempt_count = attempt_count + 1,
		    next_retry_at_ms = CASE WHEN attempt_count + 1 >= ? THEN 0 ELSE ? END,
		    last_error = ?,
		    locked_by = '',
		    locked_until_ms = 0,
		    updated_at_ms = ?
		WHERE app_code = ? AND event_id = ? AND locked_by = ?`,
		maxRetry,
		broadcastdomain.StatusFailed,
		broadcastdomain.StatusRetryable,
		maxRetry,
		nextRetryAtMS,
		lastError,
		nowMS,
		appcode.FromContext(ctx),
		eventID,
		workerID,
	)
	return err
}

func scanBroadcastOutbox(scanner interface{ Scan(dest ...any) error }) (broadcastdomain.OutboxRecord, error) {
	var record broadcastdomain.OutboxRecord
	err := scanner.Scan(
		&record.AppCode,
		&record.EventID,
		&record.Scope,
		&record.GroupID,
		&record.BroadcastType,
		&record.PayloadJSON,
		&record.Status,
		&record.AttemptCount,
		&record.NextRetryAtMS,
		&record.LastError,
		&record.LockedBy,
		&record.LockedUntilMS,
		&record.CreatedAtMS,
		&record.UpdatedAtMS,
	)
	return record, err
}
