package mysql

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

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	messagedomain "hyapp/services/activity-service/internal/domain/message"
)

// SaveActionConfirmWithOutbox creates the confirm row and IM outbox in one transaction.
func (r *Repository) SaveActionConfirmWithOutbox(ctx context.Context, confirm messagedomain.ActionConfirm, outbox messagedomain.ActionConfirmOutbox) (bool, messagedomain.ActionConfirm, error) {
	if r == nil || r.db == nil {
		return false, messagedomain.ActionConfirm{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	app := appcode.FromContext(ctx)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return false, messagedomain.ActionConfirm{}, err
	}
	defer tx.Rollback()
	confirm.AppCode = app
	outbox.AppCode = app
	_, err = tx.ExecContext(ctx, `
		INSERT INTO message_action_confirms (
			app_code, confirm_id, source_name, source_event_id, business_type, business_subtype, business_id,
			sender_user_id, receiver_user_id, message_text, status, action, command_id, processed_at_ms,
			locked_by, lock_until_ms, error_message, metadata_json, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', 0, '', 0, '', CAST(? AS JSON), ?, ?)`,
		app, confirm.ConfirmID, confirm.SourceName, confirm.SourceEventID, confirm.BusinessType, confirm.BusinessSubtype, confirm.BusinessID,
		confirm.SenderUserID, confirm.ReceiverUserID, confirm.MessageText, confirm.Status, nullJSONText(confirm.MetadataJSON), confirm.CreatedAtMS, confirm.UpdatedAtMS,
	)
	if err != nil {
		if isMySQLDuplicate(err) {
			existing, existingErr := getActionConfirmBySource(ctx, tx, confirm.SourceName, confirm.SourceEventID)
			return false, existing, existingErr
		}
		return false, messagedomain.ActionConfirm{}, err
	}
	_, err = tx.ExecContext(ctx, `
		INSERT INTO message_action_outbox (
			app_code, event_id, confirm_id, event_type, status, worker_id, lock_until_ms,
			retry_count, next_retry_at_ms, last_error, payload_json, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, '', 0, 0, 0, '', CAST(? AS JSON), ?, ?)`,
		app, outbox.EventID, confirm.ConfirmID, outbox.EventType, outbox.Status, outbox.PayloadJSON, outbox.CreatedAtMS, outbox.UpdatedAtMS,
	)
	if err != nil {
		if isMySQLDuplicate(err) {
			existing, existingErr := getActionConfirmBySource(ctx, tx, confirm.SourceName, confirm.SourceEventID)
			return false, existing, existingErr
		}
		return false, messagedomain.ActionConfirm{}, err
	}
	if err := tx.Commit(); err != nil {
		return false, messagedomain.ActionConfirm{}, err
	}
	return true, confirm, nil
}

// ClaimActionConfirm moves a pending or expired-processing confirm into processing under row lock.
func (r *Repository) ClaimActionConfirm(ctx context.Context, confirmID string, actorUserID int64, action string, commandID string, workerID string, nowMS int64, lockUntilMS int64) (messagedomain.ActionConfirm, bool, error) {
	if r == nil || r.db == nil {
		return messagedomain.ActionConfirm{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return messagedomain.ActionConfirm{}, false, err
	}
	defer tx.Rollback()
	confirm, err := getActionConfirmByIDForUpdate(ctx, tx, confirmID)
	if err != nil {
		return messagedomain.ActionConfirm{}, false, err
	}
	if confirm.ReceiverUserID != actorUserID {
		return messagedomain.ActionConfirm{}, false, xerr.New(xerr.PermissionDenied, "action confirm is not visible to actor")
	}
	if isActionConfirmTerminal(confirm.Status) {
		if err := tx.Commit(); err != nil {
			return messagedomain.ActionConfirm{}, false, err
		}
		return confirm, false, nil
	}
	if confirm.Status == messagedomain.ActionConfirmStatusProcessing && confirm.LockUntilMS > nowMS {
		return messagedomain.ActionConfirm{}, false, xerr.New(xerr.Conflict, "action confirm is processing")
	}
	_, err = tx.ExecContext(ctx, `
		UPDATE message_action_confirms
		SET status = ?, action = '', command_id = ?, locked_by = ?, lock_until_ms = ?, error_message = '', updated_at_ms = ?
		WHERE app_code = ? AND confirm_id = ?`,
		messagedomain.ActionConfirmStatusProcessing, commandID, workerID, lockUntilMS, nowMS, appcode.FromContext(ctx), confirmID,
	)
	if err != nil {
		return messagedomain.ActionConfirm{}, false, err
	}
	if err := tx.Commit(); err != nil {
		return messagedomain.ActionConfirm{}, false, err
	}
	confirm.Status = messagedomain.ActionConfirmStatusProcessing
	confirm.CommandID = commandID
	confirm.LockedBy = workerID
	confirm.LockUntilMS = lockUntilMS
	confirm.UpdatedAtMS = nowMS
	return confirm, true, nil
}

// CompleteActionConfirm writes the terminal state after the owner service accepts the business command.
func (r *Repository) CompleteActionConfirm(ctx context.Context, confirmID string, commandID string, status string, action string, processedAtMS int64) (messagedomain.ActionConfirm, error) {
	result, err := r.db.ExecContext(ctx, `
		UPDATE message_action_confirms
		SET status = ?, action = ?, processed_at_ms = ?, locked_by = '', lock_until_ms = 0, error_message = '', updated_at_ms = ?
		WHERE app_code = ? AND confirm_id = ? AND command_id = ?`,
		status, action, processedAtMS, processedAtMS, appcode.FromContext(ctx), confirmID, commandID,
	)
	if err != nil {
		return messagedomain.ActionConfirm{}, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return messagedomain.ActionConfirm{}, err
	} else if affected == 0 {
		return messagedomain.ActionConfirm{}, xerr.New(xerr.Conflict, "action confirm processing lock is lost")
	}
	return r.getActionConfirmByID(ctx, confirmID)
}

// ReleaseActionConfirm returns a failed processing attempt to pending so the user can retry with a new command.
func (r *Repository) ReleaseActionConfirm(ctx context.Context, confirmID string, commandID string, errorMessage string, nowMS int64) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE message_action_confirms
		SET status = ?, locked_by = '', lock_until_ms = 0, error_message = ?, updated_at_ms = ?
		WHERE app_code = ? AND confirm_id = ? AND command_id = ? AND status = ?`,
		messagedomain.ActionConfirmStatusPending, truncateMessageActionError(errorMessage), nowMS, appcode.FromContext(ctx), confirmID, commandID, messagedomain.ActionConfirmStatusProcessing,
	)
	return err
}

// BatchGetActionConfirms returns current-user-visible confirm rows for a bounded ID list.
func (r *Repository) BatchGetActionConfirms(ctx context.Context, userID int64, confirmIDs []string) ([]messagedomain.ActionConfirm, error) {
	if len(confirmIDs) == 0 {
		return []messagedomain.ActionConfirm{}, nil
	}
	placeholders := strings.TrimSuffix(strings.Repeat("?,", len(confirmIDs)), ",")
	args := []any{appcode.FromContext(ctx), userID}
	for _, confirmID := range confirmIDs {
		args = append(args, confirmID)
	}
	rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
		SELECT %s
		FROM message_action_confirms
		WHERE app_code = ? AND receiver_user_id = ? AND confirm_id IN (`+placeholders+`)
		ORDER BY created_at_ms DESC, confirm_id DESC`, actionConfirmColumns), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanActionConfirmRows(rows)
}

// ListConversationActionConfirms reads one cursor page for confirms sent by a specific peer.
func (r *Repository) ListConversationActionConfirms(ctx context.Context, userID int64, peerUserID int64, limit int, cursorCreatedAtMS int64, cursorConfirmID string) ([]messagedomain.ActionConfirm, error) {
	args := []any{appcode.FromContext(ctx), userID, peerUserID}
	cursorSQL := ""
	if cursorCreatedAtMS > 0 || cursorConfirmID != "" {
		cursorSQL = " AND (created_at_ms < ? OR (created_at_ms = ? AND confirm_id < ?))"
		args = append(args, cursorCreatedAtMS, cursorCreatedAtMS, cursorConfirmID)
	}
	args = append(args, limit)
	rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
		SELECT %s
		FROM message_action_confirms
		WHERE app_code = ? AND receiver_user_id = ? AND sender_user_id = ?`+cursorSQL+`
		ORDER BY created_at_ms DESC, confirm_id DESC
		LIMIT ?`, actionConfirmColumns), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanActionConfirmRows(rows)
}

// ClaimPendingActionConfirmOutbox locks pending/retryable confirm IM facts for RocketMQ publishing.
func (r *Repository) ClaimPendingActionConfirmOutbox(ctx context.Context, workerID string, nowMS int64, lockUntilMS int64, batchSize int) ([]messagedomain.ActionConfirmOutbox, error) {
	if batchSize <= 0 {
		batchSize = 100
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	records := make([]messagedomain.ActionConfirmOutbox, 0, batchSize)
	branches := []struct {
		query string
		args  []any
	}{
		{query: actionOutboxClaimQuery("status = 'pending' AND next_retry_at_ms <= ?"), args: []any{appcode.FromContext(ctx), nowMS}},
		{query: actionOutboxClaimQuery("status = 'retryable' AND next_retry_at_ms <= ?"), args: []any{appcode.FromContext(ctx), nowMS}},
		{query: actionOutboxClaimQuery("status = 'running' AND lock_until_ms <= ?"), args: []any{appcode.FromContext(ctx), nowMS}},
	}
	for _, branch := range branches {
		remaining := batchSize - len(records)
		if remaining <= 0 {
			break
		}
		args := append(append([]any{}, branch.args...), remaining)
		items, err := queryActionConfirmOutboxRows(ctx, tx, branch.query, args...)
		if err != nil {
			return nil, err
		}
		records = append(records, items...)
	}
	for _, record := range records {
		if _, err := tx.ExecContext(ctx, `
			UPDATE message_action_outbox
			SET status = 'running', worker_id = ?, lock_until_ms = ?, updated_at_ms = ?
			WHERE app_code = ? AND event_id = ?`,
			workerID, lockUntilMS, nowMS, record.AppCode, record.EventID,
		); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	for index := range records {
		records[index].Status = "running"
		records[index].WorkerID = workerID
		records[index].LockUntilMS = lockUntilMS
	}
	return records, nil
}

// MarkActionConfirmOutboxDelivered records that RocketMQ accepted the confirm IM fact.
func (r *Repository) MarkActionConfirmOutboxDelivered(ctx context.Context, eventID string, nowMS int64) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE message_action_outbox
		SET status = 'delivered', worker_id = '', lock_until_ms = 0, last_error = '', updated_at_ms = ?
		WHERE app_code = ? AND event_id = ?`,
		nowMS, appcode.FromContext(ctx), eventID,
	)
	return err
}

// MarkActionConfirmOutboxRetryable releases a failed publish with a bounded retry delay.
func (r *Repository) MarkActionConfirmOutboxRetryable(ctx context.Context, eventID string, lastErr string, nextRetryAtMS int64, nowMS int64) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE message_action_outbox
		SET status = 'retryable', worker_id = '', lock_until_ms = 0, retry_count = retry_count + 1,
		    next_retry_at_ms = ?, last_error = ?, updated_at_ms = ?
		WHERE app_code = ? AND event_id = ? AND status = 'running'`,
		nextRetryAtMS, truncateMessageActionError(lastErr), nowMS, appcode.FromContext(ctx), eventID,
	)
	return err
}

const actionConfirmColumns = `
	app_code, confirm_id, source_name, source_event_id, business_type, business_subtype, business_id,
	sender_user_id, receiver_user_id, message_text, status, action, command_id, processed_at_ms,
	locked_by, lock_until_ms, error_message, COALESCE(CAST(metadata_json AS CHAR), ''), created_at_ms, updated_at_ms`

func (r *Repository) getActionConfirmByID(ctx context.Context, confirmID string) (messagedomain.ActionConfirm, error) {
	return scanActionConfirm(r.db.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT %s FROM message_action_confirms WHERE app_code = ? AND confirm_id = ?`,
		actionConfirmColumns), appcode.FromContext(ctx), confirmID))
}

func getActionConfirmByIDForUpdate(ctx context.Context, tx *sql.Tx, confirmID string) (messagedomain.ActionConfirm, error) {
	return scanActionConfirm(tx.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT %s FROM message_action_confirms WHERE app_code = ? AND confirm_id = ? FOR UPDATE`,
		actionConfirmColumns), appcode.FromContext(ctx), confirmID))
}

func getActionConfirmBySource(ctx context.Context, tx *sql.Tx, sourceName string, sourceEventID string) (messagedomain.ActionConfirm, error) {
	return scanActionConfirm(tx.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT %s FROM message_action_confirms WHERE app_code = ? AND source_name = ? AND source_event_id = ?`,
		actionConfirmColumns), appcode.FromContext(ctx), sourceName, sourceEventID))
}

func scanActionConfirm(scanner inboxScanner) (messagedomain.ActionConfirm, error) {
	var confirm messagedomain.ActionConfirm
	err := scanner.Scan(
		&confirm.AppCode,
		&confirm.ConfirmID,
		&confirm.SourceName,
		&confirm.SourceEventID,
		&confirm.BusinessType,
		&confirm.BusinessSubtype,
		&confirm.BusinessID,
		&confirm.SenderUserID,
		&confirm.ReceiverUserID,
		&confirm.MessageText,
		&confirm.Status,
		&confirm.Action,
		&confirm.CommandID,
		&confirm.ProcessedAtMS,
		&confirm.LockedBy,
		&confirm.LockUntilMS,
		&confirm.ErrorMessage,
		&confirm.MetadataJSON,
		&confirm.CreatedAtMS,
		&confirm.UpdatedAtMS,
	)
	if errors.Is(err, sql.ErrNoRows) {
		return messagedomain.ActionConfirm{}, xerr.New(xerr.NotFound, "action confirm not found")
	}
	return confirm, err
}

func scanActionConfirmRows(rows *sql.Rows) ([]messagedomain.ActionConfirm, error) {
	items := make([]messagedomain.ActionConfirm, 0)
	for rows.Next() {
		item, err := scanActionConfirm(rows)
		if err != nil {
			return nil, err
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

func actionOutboxClaimQuery(condition string) string {
	return `
		SELECT app_code, event_id, confirm_id, event_type, status, worker_id, lock_until_ms,
		       retry_count, next_retry_at_ms, last_error, CAST(payload_json AS CHAR), created_at_ms, updated_at_ms
		FROM message_action_outbox
		WHERE app_code = ? AND ` + condition + `
		ORDER BY created_at_ms ASC, event_id ASC
		LIMIT ? FOR UPDATE SKIP LOCKED`
}

func queryActionConfirmOutboxRows(ctx context.Context, tx *sql.Tx, query string, args ...any) ([]messagedomain.ActionConfirmOutbox, error) {
	rows, err := tx.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	records := make([]messagedomain.ActionConfirmOutbox, 0)
	for rows.Next() {
		var record messagedomain.ActionConfirmOutbox
		if err := rows.Scan(
			&record.AppCode,
			&record.EventID,
			&record.ConfirmID,
			&record.EventType,
			&record.Status,
			&record.WorkerID,
			&record.LockUntilMS,
			&record.RetryCount,
			&record.NextRetryAtMS,
			&record.LastError,
			&record.PayloadJSON,
			&record.CreatedAtMS,
			&record.UpdatedAtMS,
		); err != nil {
			return nil, err
		}
		records = append(records, record)
	}
	return records, rows.Err()
}

func isActionConfirmTerminal(status string) bool {
	switch status {
	case messagedomain.ActionConfirmStatusAccepted, messagedomain.ActionConfirmStatusRejected, messagedomain.ActionConfirmStatusExpired, messagedomain.ActionConfirmStatusCanceled, messagedomain.ActionConfirmStatusFailed:
		return true
	default:
		return false
	}
}

func nullJSONText(value string) any {
	if strings.TrimSpace(value) == "" {
		return "{}"
	}
	return value
}

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