package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"sort"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/walletmq"
	"hyapp/pkg/xerr"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
)

const (
	badgeDisplayProjectionName      = "activity_badge_display"
	walletResourceGrantedEvent      = walletmq.EventTypeResourceGranted
	walletResourceGroupGrantedEvent = walletmq.EventTypeResourceGroupGranted
	grantSourceVIPPurchase          = "vip_purchase"
)

type badgeGrantProjectionCandidate struct {
	AppCode     string
	EventID     string
	CreatedAtMS int64
}

type badgeGrantOutboxPayload struct {
	GrantID string `json:"grant_id"`
	Source  string `json:"source"`
}

type badgeGrantProjectionPayload struct {
	GrantID     string                          `json:"grant_id"`
	GrantSource string                          `json:"grant_source"`
	CommandID   string                          `json:"command_id"`
	EventType   string                          `json:"event_type"`
	Items       []resourcedomain.BadgeGrantItem `json:"items"`
}

// ClaimPendingBadgeGrantEvents claims wallet resource-grant outbox events that should be relayed to activity badge slots.
func (r *Repository) ClaimPendingBadgeGrantEvents(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int) ([]resourcedomain.BadgeGrantOutbox, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	workerID = strings.TrimSpace(workerID)
	if workerID == "" {
		return nil, xerr.New(xerr.InvalidArgument, "worker_id is required")
	}
	if batchSize <= 0 {
		batchSize = 50
	}
	if batchSize > 100 {
		batchSize = 100
	}
	if lockTTL <= 0 {
		lockTTL = 30 * time.Second
	}
	candidates, err := r.listBadgeGrantProjectionCandidates(ctx, nowMS, batchSize)
	if err != nil {
		return nil, err
	}
	events := make([]resourcedomain.BadgeGrantOutbox, 0, len(candidates))
	for _, candidate := range candidates {
		event, claimed, err := r.claimBadgeGrantEvent(ctx, candidate, workerID, nowMS, lockTTL)
		if err != nil {
			return events, err
		}
		if claimed {
			events = append(events, event)
		}
	}
	return events, nil
}

// ClaimBadgeGrantMessage claims one wallet_outbox MQ fact for badge display projection.
// Runtime projection consumes MQ and does not scan wallet_outbox for candidates.
func (r *Repository) ClaimBadgeGrantMessage(ctx context.Context, workerID string, message walletmq.WalletOutboxMessage, nowMS int64, lockTTL time.Duration) (resourcedomain.BadgeGrantOutbox, bool, error) {
	if r == nil || r.db == nil {
		return resourcedomain.BadgeGrantOutbox{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if !isBadgeGrantWalletMessage(message) {
		return resourcedomain.BadgeGrantOutbox{}, false, nil
	}
	workerID = strings.TrimSpace(workerID)
	if workerID == "" {
		return resourcedomain.BadgeGrantOutbox{}, false, xerr.New(xerr.InvalidArgument, "worker_id is required")
	}
	if nowMS <= 0 {
		nowMS = time.Now().UnixMilli()
	}
	if lockTTL <= 0 {
		lockTTL = 30 * time.Second
	}
	event := resourcedomain.BadgeGrantOutbox{
		AppCode:     appcode.Normalize(message.AppCode),
		EventID:     strings.TrimSpace(message.EventID),
		EventType:   strings.TrimSpace(message.EventType),
		CommandID:   strings.TrimSpace(message.CommandID),
		UserID:      message.UserID,
		PayloadJSON: strings.TrimSpace(message.PayloadJSON),
		CreatedAtMS: message.OccurredAtMS,
		UpdatedAtMS: message.OccurredAtMS,
	}
	if event.PayloadJSON == "" {
		event.PayloadJSON = "{}"
	}
	ctx = appcode.WithContext(ctx, event.AppCode)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return resourcedomain.BadgeGrantOutbox{}, false, err
	}
	defer func() { _ = tx.Rollback() }()
	return r.claimBadgeGrantEventInTx(ctx, tx, event, workerID, nowMS, lockTTL)
}

func (r *Repository) listBadgeGrantProjectionCandidates(ctx context.Context, nowMS int64, limit int) ([]badgeGrantProjectionCandidate, error) {
	appCode := appcode.FromContext(ctx)
	rows, err := r.db.QueryContext(ctx, `
		SELECT wo.app_code, wo.event_id, wo.created_at_ms
		FROM wallet_outbox wo FORCE INDEX (idx_wallet_outbox_asset_event_created)
		LEFT JOIN wallet_projection_events pe
		  ON pe.projection_name = ? AND pe.app_code = wo.app_code AND pe.event_id = wo.event_id
		WHERE wo.app_code = ?
		  AND wo.asset_type = ?
		  AND wo.event_type IN (?, ?)
		  AND pe.event_id IS NULL
		ORDER BY wo.created_at_ms ASC, wo.event_id ASC
		LIMIT ?`,
		badgeDisplayProjectionName,
		appCode,
		resourceOutboxAsset,
		walletResourceGrantedEvent,
		walletResourceGroupGrantedEvent,
		limit,
	)
	if err != nil {
		return nil, err
	}
	candidates, err := scanBadgeGrantProjectionCandidates(rows, limit)
	if err != nil {
		return nil, err
	}
	rows, err = r.db.QueryContext(ctx, `
		SELECT wo.app_code, wo.event_id, wo.created_at_ms
		FROM wallet_projection_events pe
		JOIN wallet_outbox wo
		  ON wo.app_code = pe.app_code AND wo.event_id = pe.event_id
		WHERE pe.projection_name = ?
		  AND pe.app_code = ?
		  AND pe.status IN (?, ?)
		  AND pe.locked_until_ms <= ?
		  AND wo.asset_type = ?
		  AND wo.event_type IN (?, ?)
		ORDER BY wo.created_at_ms ASC, wo.event_id ASC
		LIMIT ?`,
		badgeDisplayProjectionName,
		appCode,
		projectionStatusFailed,
		projectionStatusProcessing,
		nowMS,
		resourceOutboxAsset,
		walletResourceGrantedEvent,
		walletResourceGroupGrantedEvent,
		limit,
	)
	if err != nil {
		return nil, err
	}
	retryCandidates, err := scanBadgeGrantProjectionCandidates(rows, limit)
	if err != nil {
		return nil, err
	}
	candidates = append(candidates, retryCandidates...)
	sort.SliceStable(candidates, func(i, j int) bool {
		if candidates[i].CreatedAtMS == candidates[j].CreatedAtMS {
			return candidates[i].EventID < candidates[j].EventID
		}
		return candidates[i].CreatedAtMS < candidates[j].CreatedAtMS
	})
	if len(candidates) > limit {
		candidates = candidates[:limit]
	}
	return candidates, nil
}

func isBadgeGrantWalletMessage(message walletmq.WalletOutboxMessage) bool {
	if strings.TrimSpace(message.AssetType) != resourceOutboxAsset {
		return false
	}
	switch message.EventType {
	case walletResourceGrantedEvent, walletResourceGroupGrantedEvent:
		return true
	default:
		return false
	}
}

func scanBadgeGrantProjectionCandidates(rows *sql.Rows, limit int) ([]badgeGrantProjectionCandidate, error) {
	defer rows.Close()
	candidates := make([]badgeGrantProjectionCandidate, 0, limit)
	for rows.Next() {
		var candidate badgeGrantProjectionCandidate
		if err := rows.Scan(&candidate.AppCode, &candidate.EventID, &candidate.CreatedAtMS); err != nil {
			return nil, err
		}
		candidates = append(candidates, candidate)
	}
	return candidates, rows.Err()
}

func (r *Repository) claimBadgeGrantEvent(ctx context.Context, candidate badgeGrantProjectionCandidate, workerID string, nowMS int64, lockTTL time.Duration) (resourcedomain.BadgeGrantOutbox, bool, error) {
	ctx = appcode.WithContext(ctx, candidate.AppCode)

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

	event, err := r.lockBadgeGrantOutboxEvent(ctx, tx, candidate)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return resourcedomain.BadgeGrantOutbox{}, false, nil
		}
		return resourcedomain.BadgeGrantOutbox{}, false, err
	}
	return r.claimBadgeGrantEventInTx(ctx, tx, event, workerID, nowMS, lockTTL)
}

func (r *Repository) claimBadgeGrantEventInTx(ctx context.Context, tx *sql.Tx, event resourcedomain.BadgeGrantOutbox, workerID string, nowMS int64, lockTTL time.Duration) (resourcedomain.BadgeGrantOutbox, bool, error) {
	lockUntilMS := time.UnixMilli(nowMS).Add(lockTTL).UnixMilli()

	claimed, err := r.claimBadgeProjectionEvent(ctx, tx, event, workerID, lockUntilMS, nowMS)
	if err != nil || !claimed {
		return resourcedomain.BadgeGrantOutbox{}, false, err
	}

	var outboxPayload badgeGrantOutboxPayload
	if err := json.Unmarshal([]byte(event.PayloadJSON), &outboxPayload); err != nil || strings.TrimSpace(outboxPayload.GrantID) == "" {
		reason := "invalid resource grant outbox payload"
		if err != nil {
			reason += ": " + err.Error()
		}
		if markErr := r.markBadgeProjectionFailedInTx(ctx, tx, event, reason, nowMS); markErr != nil {
			return resourcedomain.BadgeGrantOutbox{}, false, markErr
		}
		return resourcedomain.BadgeGrantOutbox{}, false, tx.Commit()
	}
	grant, err := r.getResourceGrantTx(ctx, tx, strings.TrimSpace(outboxPayload.GrantID))
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			if markErr := r.markBadgeProjectionFailedInTx(ctx, tx, event, "resource grant not found", nowMS); markErr != nil {
				return resourcedomain.BadgeGrantOutbox{}, false, markErr
			}
			return resourcedomain.BadgeGrantOutbox{}, false, tx.Commit()
		}
		return resourcedomain.BadgeGrantOutbox{}, false, err
	}
	event.GrantID = grant.GrantID
	event.GrantSource = grant.GrantSource
	event.UserID = grant.TargetUserID
	if shouldSkipBadgeGrantSource(grant.GrantSource) {
		if err := r.markBadgeProjectionDoneInTx(ctx, tx, event, nowMS); err != nil {
			return resourcedomain.BadgeGrantOutbox{}, false, err
		}
		return resourcedomain.BadgeGrantOutbox{}, false, tx.Commit()
	}
	items, err := badgeGrantItemsFromResourceGrant(grant)
	if err != nil {
		if markErr := r.markBadgeProjectionFailedInTx(ctx, tx, event, err.Error(), nowMS); markErr != nil {
			return resourcedomain.BadgeGrantOutbox{}, false, markErr
		}
		return resourcedomain.BadgeGrantOutbox{}, false, tx.Commit()
	}
	if len(items) == 0 {
		if err := r.markBadgeProjectionDoneInTx(ctx, tx, event, nowMS); err != nil {
			return resourcedomain.BadgeGrantOutbox{}, false, err
		}
		return resourcedomain.BadgeGrantOutbox{}, false, tx.Commit()
	}
	payloadJSON, err := json.Marshal(badgeGrantProjectionPayload{
		GrantID:     grant.GrantID,
		GrantSource: grant.GrantSource,
		CommandID:   event.CommandID,
		EventType:   event.EventType,
		Items:       items,
	})
	if err != nil {
		return resourcedomain.BadgeGrantOutbox{}, false, err
	}
	event.Items = items
	event.PayloadJSON = string(payloadJSON)
	if err := tx.Commit(); err != nil {
		return resourcedomain.BadgeGrantOutbox{}, false, err
	}
	return event, true, nil
}

func (r *Repository) lockBadgeGrantOutboxEvent(ctx context.Context, tx *sql.Tx, candidate badgeGrantProjectionCandidate) (resourcedomain.BadgeGrantOutbox, error) {
	var event resourcedomain.BadgeGrantOutbox
	err := tx.QueryRowContext(ctx, `
		SELECT app_code, event_id, event_type, command_id, user_id, COALESCE(CAST(payload AS CHAR), '{}'), created_at_ms, updated_at_ms
		FROM wallet_outbox
		WHERE app_code = ? AND event_id = ? AND asset_type = ? AND event_type IN (?, ?)
		FOR UPDATE`,
		appcode.Normalize(candidate.AppCode),
		candidate.EventID,
		resourceOutboxAsset,
		walletResourceGrantedEvent,
		walletResourceGroupGrantedEvent,
	).Scan(&event.AppCode, &event.EventID, &event.EventType, &event.CommandID, &event.UserID, &event.PayloadJSON, &event.CreatedAtMS, &event.UpdatedAtMS)
	return event, err
}

func (r *Repository) claimBadgeProjectionEvent(ctx context.Context, tx *sql.Tx, event resourcedomain.BadgeGrantOutbox, workerID string, lockUntilMS int64, nowMS int64) (bool, error) {
	_, err := tx.ExecContext(ctx, `
		INSERT INTO wallet_projection_events (
		  projection_name, app_code, event_id, status, retry_count, locked_by,
		  locked_until_ms, last_error, processed_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, 0, ?, ?, '', 0, ?, ?)`,
		badgeDisplayProjectionName,
		event.AppCode,
		event.EventID,
		projectionStatusProcessing,
		workerID,
		lockUntilMS,
		nowMS,
		nowMS,
	)
	if err == nil {
		return true, nil
	}
	if !isMySQLDuplicateError(err) {
		return false, err
	}
	result, err := tx.ExecContext(ctx, `
		UPDATE wallet_projection_events
		SET status = ?, locked_by = ?, locked_until_ms = ?, updated_at_ms = ?
		WHERE projection_name = ? AND app_code = ? AND event_id = ?
		  AND status IN (?, ?) AND locked_until_ms <= ?`,
		projectionStatusProcessing,
		workerID,
		lockUntilMS,
		nowMS,
		badgeDisplayProjectionName,
		event.AppCode,
		event.EventID,
		projectionStatusFailed,
		projectionStatusProcessing,
		nowMS,
	)
	if err != nil {
		return false, err
	}
	affected, err := result.RowsAffected()
	return affected > 0, err
}

// MarkBadgeGrantEventDelivered marks a resource-grant event as successfully relayed to activity-service.
func (r *Repository) MarkBadgeGrantEventDelivered(ctx context.Context, event resourcedomain.BadgeGrantOutbox, nowMS int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE wallet_projection_events
		SET status = ?, locked_by = '', locked_until_ms = 0, last_error = '', processed_at_ms = ?, updated_at_ms = ?
		WHERE projection_name = ? AND app_code = ? AND event_id = ?`,
		projectionStatusDone,
		nowMS,
		nowMS,
		badgeDisplayProjectionName,
		event.AppCode,
		event.EventID,
	)
	return err
}

// MarkBadgeGrantEventFailed releases the projection lock and records a retryable activity relay failure.
func (r *Repository) MarkBadgeGrantEventFailed(ctx context.Context, event resourcedomain.BadgeGrantOutbox, failureReason string, nowMS int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	reason := trimProjectionError(failureReason)
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO wallet_projection_events (
		  projection_name, app_code, event_id, status, retry_count, locked_by,
		  locked_until_ms, last_error, processed_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, 1, '', 0, ?, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
		  status = IF(retry_count + 1 >= ?, ?, ?),
		  retry_count = retry_count + 1,
		  locked_by = '',
		  locked_until_ms = 0,
		  last_error = VALUES(last_error),
		  updated_at_ms = VALUES(updated_at_ms)`,
		badgeDisplayProjectionName,
		event.AppCode,
		event.EventID,
		projectionStatusFailed,
		reason,
		nowMS,
		nowMS,
		projectionMaxRetries,
		projectionStatusDead,
		projectionStatusFailed,
	)
	return err
}

func (r *Repository) markBadgeProjectionDoneInTx(ctx context.Context, tx *sql.Tx, event resourcedomain.BadgeGrantOutbox, nowMS int64) error {
	_, err := tx.ExecContext(ctx, `
		UPDATE wallet_projection_events
		SET status = ?, locked_by = '', locked_until_ms = 0, last_error = '', processed_at_ms = ?, updated_at_ms = ?
		WHERE projection_name = ? AND app_code = ? AND event_id = ?`,
		projectionStatusDone,
		nowMS,
		nowMS,
		badgeDisplayProjectionName,
		event.AppCode,
		event.EventID,
	)
	return err
}

func (r *Repository) markBadgeProjectionFailedInTx(ctx context.Context, tx *sql.Tx, event resourcedomain.BadgeGrantOutbox, reason string, nowMS int64) error {
	_, err := tx.ExecContext(ctx, `
		UPDATE wallet_projection_events
		SET status = IF(retry_count + 1 >= ?, ?, ?),
		    retry_count = retry_count + 1,
		    locked_by = '',
		    locked_until_ms = 0,
		    last_error = ?,
		    updated_at_ms = ?
		WHERE projection_name = ? AND app_code = ? AND event_id = ?`,
		projectionMaxRetries,
		projectionStatusDead,
		projectionStatusFailed,
		trimProjectionError(reason),
		nowMS,
		badgeDisplayProjectionName,
		event.AppCode,
		event.EventID,
	)
	return err
}

func badgeGrantItemsFromResourceGrant(grant resourcedomain.ResourceGrant) ([]resourcedomain.BadgeGrantItem, error) {
	items := make([]resourcedomain.BadgeGrantItem, 0, len(grant.Items))
	for _, grantItem := range grant.Items {
		if grantItem.ResultType != resourcedomain.ResultEntitlement {
			continue
		}
		var resource resourcedomain.Resource
		if err := json.Unmarshal([]byte(grantItem.ResourceSnapshotJSON), &resource); err != nil {
			return nil, xerr.New(xerr.InvalidArgument, "invalid badge resource snapshot: "+err.Error())
		}
		if resourcedomain.NormalizeResourceType(resource.ResourceType) != resourcedomain.TypeBadge {
			continue
		}
		items = append(items, resourcedomain.BadgeGrantItem{
			ResourceID:           resource.ResourceID,
			ResourceCode:         resource.ResourceCode,
			ResourceType:         resource.ResourceType,
			Name:                 resource.Name,
			EntitlementID:        grantItem.EntitlementID,
			ResourceSnapshotJSON: grantItem.ResourceSnapshotJSON,
			MetadataJSON:         resource.MetadataJSON,
		})
	}
	return items, nil
}

func shouldSkipBadgeGrantSource(source string) bool {
	switch strings.ToLower(strings.TrimSpace(source)) {
	case resourcedomain.GrantSourceAchievement, resourcedomain.GrantSourceGrowthLevel, grantSourceVIPPurchase:
		return true
	default:
		return false
	}
}
