package postgres

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

	"github.com/google/uuid"
	"github.com/lib/pq"
	"github.com/rwadurian/mpc-system/services/session-coordinator/domain/entities"
	"github.com/rwadurian/mpc-system/services/session-coordinator/domain/repositories"
	"github.com/rwadurian/mpc-system/services/session-coordinator/domain/value_objects"
)

// MessagePostgresRepo implements MessageRepository for PostgreSQL
type MessagePostgresRepo struct {
	db *sql.DB
}

// NewMessagePostgresRepo creates a new PostgreSQL message repository
func NewMessagePostgresRepo(db *sql.DB) *MessagePostgresRepo {
	return &MessagePostgresRepo{db: db}
}

// SaveMessage persists a new message
func (r *MessagePostgresRepo) SaveMessage(ctx context.Context, msg *entities.SessionMessage) error {
	toParties := msg.GetToPartyStrings()

	_, err := r.db.ExecContext(ctx, `
		INSERT INTO mpc_messages (
			id, session_id, from_party, to_parties, round_number, message_type, payload, created_at
		) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
	`,
		msg.ID,
		msg.SessionID.UUID(),
		msg.FromParty.String(),
		pq.Array(toParties),
		msg.RoundNumber,
		msg.MessageType,
		msg.Payload,
		msg.CreatedAt,
	)
	return err
}

// GetByID retrieves a message by ID
func (r *MessagePostgresRepo) GetByID(ctx context.Context, id uuid.UUID) (*entities.SessionMessage, error) {
	var row messageRow
	var toParties []string

	err := r.db.QueryRowContext(ctx, `
		SELECT id, session_id, from_party, to_parties, round_number, message_type, payload, created_at, delivered_at
		FROM mpc_messages WHERE id = $1
	`, id).Scan(
		&row.ID,
		&row.SessionID,
		&row.FromParty,
		pq.Array(&toParties),
		&row.RoundNumber,
		&row.MessageType,
		&row.Payload,
		&row.CreatedAt,
		&row.DeliveredAt,
	)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, nil
		}
		return nil, err
	}

	return r.rowToMessage(row, toParties)
}

// GetMessages retrieves messages for a session and party after a specific time
func (r *MessagePostgresRepo) GetMessages(
	ctx context.Context,
	sessionID value_objects.SessionID,
	partyID value_objects.PartyID,
	afterTime time.Time,
) ([]*entities.SessionMessage, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, session_id, from_party, to_parties, round_number, message_type, payload, created_at, delivered_at
		FROM mpc_messages
		WHERE session_id = $1
		  AND created_at > $2
		  AND (to_parties IS NULL OR $3 = ANY(to_parties))
		  AND from_party != $3
		ORDER BY created_at ASC
	`, sessionID.UUID(), afterTime, partyID.String())
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	return r.scanMessages(rows)
}

// GetUndeliveredMessages retrieves undelivered messages for a party
func (r *MessagePostgresRepo) GetUndeliveredMessages(
	ctx context.Context,
	sessionID value_objects.SessionID,
	partyID value_objects.PartyID,
) ([]*entities.SessionMessage, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, session_id, from_party, to_parties, round_number, message_type, payload, created_at, delivered_at
		FROM mpc_messages
		WHERE session_id = $1
		  AND delivered_at IS NULL
		  AND (to_parties IS NULL OR $2 = ANY(to_parties))
		  AND from_party != $2
		ORDER BY created_at ASC
	`, sessionID.UUID(), partyID.String())
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	return r.scanMessages(rows)
}

// GetMessagesByRound retrieves messages for a specific round
func (r *MessagePostgresRepo) GetMessagesByRound(
	ctx context.Context,
	sessionID value_objects.SessionID,
	roundNumber int,
) ([]*entities.SessionMessage, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, session_id, from_party, to_parties, round_number, message_type, payload, created_at, delivered_at
		FROM mpc_messages
		WHERE session_id = $1 AND round_number = $2
		ORDER BY created_at ASC
	`, sessionID.UUID(), roundNumber)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	return r.scanMessages(rows)
}

// MarkDelivered marks a message as delivered
func (r *MessagePostgresRepo) MarkDelivered(ctx context.Context, messageID uuid.UUID) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE mpc_messages SET delivered_at = NOW() WHERE id = $1
	`, messageID)
	return err
}

// MarkAllDelivered marks all messages for a party as delivered
func (r *MessagePostgresRepo) MarkAllDelivered(
	ctx context.Context,
	sessionID value_objects.SessionID,
	partyID value_objects.PartyID,
) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE mpc_messages SET delivered_at = NOW()
		WHERE session_id = $1
		  AND delivered_at IS NULL
		  AND (to_parties IS NULL OR $2 = ANY(to_parties))
	`, sessionID.UUID(), partyID.String())
	return err
}

// DeleteBySession deletes all messages for a session
func (r *MessagePostgresRepo) DeleteBySession(ctx context.Context, sessionID value_objects.SessionID) error {
	_, err := r.db.ExecContext(ctx, `DELETE FROM mpc_messages WHERE session_id = $1`, sessionID.UUID())
	return err
}

// DeleteOlderThan deletes messages older than a specific time
func (r *MessagePostgresRepo) DeleteOlderThan(ctx context.Context, before time.Time) (int64, error) {
	result, err := r.db.ExecContext(ctx, `DELETE FROM mpc_messages WHERE created_at < $1`, before)
	if err != nil {
		return 0, err
	}
	return result.RowsAffected()
}

// Count returns the total number of messages for a session
func (r *MessagePostgresRepo) Count(ctx context.Context, sessionID value_objects.SessionID) (int64, error) {
	var count int64
	err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM mpc_messages WHERE session_id = $1`, sessionID.UUID()).Scan(&count)
	return count, err
}

// CountUndelivered returns the number of undelivered messages for a party
func (r *MessagePostgresRepo) CountUndelivered(
	ctx context.Context,
	sessionID value_objects.SessionID,
	partyID value_objects.PartyID,
) (int64, error) {
	var count int64
	err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*) FROM mpc_messages
		WHERE session_id = $1
		  AND delivered_at IS NULL
		  AND (to_parties IS NULL OR $2 = ANY(to_parties))
	`, sessionID.UUID(), partyID.String()).Scan(&count)
	return count, err
}

// Helper methods

func (r *MessagePostgresRepo) scanMessages(rows *sql.Rows) ([]*entities.SessionMessage, error) {
	var messages []*entities.SessionMessage
	for rows.Next() {
		var row messageRow
		var toParties []string

		err := rows.Scan(
			&row.ID,
			&row.SessionID,
			&row.FromParty,
			pq.Array(&toParties),
			&row.RoundNumber,
			&row.MessageType,
			&row.Payload,
			&row.CreatedAt,
			&row.DeliveredAt,
		)
		if err != nil {
			return nil, err
		}

		msg, err := r.rowToMessage(row, toParties)
		if err != nil {
			return nil, err
		}
		messages = append(messages, msg)
	}

	return messages, rows.Err()
}

func (r *MessagePostgresRepo) rowToMessage(row messageRow, toParties []string) (*entities.SessionMessage, error) {
	fromParty, err := value_objects.NewPartyID(row.FromParty)
	if err != nil {
		return nil, err
	}

	var toPartiesVO []value_objects.PartyID
	for _, p := range toParties {
		partyID, err := value_objects.NewPartyID(p)
		if err != nil {
			return nil, err
		}
		toPartiesVO = append(toPartiesVO, partyID)
	}

	return &entities.SessionMessage{
		ID:          row.ID,
		SessionID:   value_objects.SessionIDFromUUID(row.SessionID),
		FromParty:   fromParty,
		ToParties:   toPartiesVO,
		RoundNumber: row.RoundNumber,
		MessageType: row.MessageType,
		Payload:     row.Payload,
		CreatedAt:   row.CreatedAt,
		DeliveredAt: row.DeliveredAt,
	}, nil
}

type messageRow struct {
	ID          uuid.UUID
	SessionID   uuid.UUID
	FromParty   string
	RoundNumber int
	MessageType string
	Payload     []byte
	CreatedAt   time.Time
	DeliveredAt *time.Time
}

// Ensure interface compliance
var _ repositories.MessageRepository = (*MessagePostgresRepo)(nil)
