package waitlist

import (
	"context"
	"encoding/json"
	"errors"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgxpool"
)

var errNotFound = errors.New("not found")
var errAlreadyExists = errors.New("already exists")

type repository interface {
	create(ctx context.Context, email string, metadata map[string]string) (*WaitlistEntry, error)
	getAll(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error)
	getByEmail(ctx context.Context, email string) (*WaitlistEntry, error)
	markInvited(ctx context.Context, email string) error
}

type repositoryImpl struct {
	pool *pgxpool.Pool
}

func newRepository(pool *pgxpool.Pool) repository {
	return &repositoryImpl{pool: pool}
}

func (r *repositoryImpl) create(ctx context.Context, email string, metadata map[string]string) (*WaitlistEntry, error) {
	metadataJSON, err := json.Marshal(metadata)
	if err != nil {
		return nil, err
	}

	var entry WaitlistEntry
	var metadataBytes []byte
	err = r.pool.QueryRow(ctx,
		`INSERT INTO waitlist_entries (email, metadata)
		 VALUES ($1, $2)
		 RETURNING id, email, metadata, created_at, invited_at`,
		email, metadataJSON,
	).Scan(&entry.Id, &entry.Email, &metadataBytes, &entry.CreatedAt, &entry.InvitedAt)
	if err != nil {
		var pgErr *pgconn.PgError
		if errors.As(err, &pgErr) && pgErr.Code == "23505" {
			return nil, errAlreadyExists
		}
		return nil, err
	}

	if err := json.Unmarshal(metadataBytes, &entry.Metadata); err != nil {
		return nil, err
	}

	return &entry, nil
}

func (r *repositoryImpl) getAll(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error) {
	query := `SELECT id, email, metadata, created_at, invited_at FROM waitlist_entries`
	switch filter {
	case GetWaitlistFilterInvitedOnly:
		query += ` WHERE invited_at IS NOT NULL`
	case GetWaitlistFilterUninvitedOnly:
		query += ` WHERE invited_at IS NULL`
	}
	query += ` ORDER BY created_at DESC`

	rows, err := r.pool.Query(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var entries []*WaitlistEntry
	for rows.Next() {
		var entry WaitlistEntry
		var metadataBytes []byte
		if err := rows.Scan(&entry.Id, &entry.Email, &metadataBytes, &entry.CreatedAt, &entry.InvitedAt); err != nil {
			return nil, err
		}
		if err := json.Unmarshal(metadataBytes, &entry.Metadata); err != nil {
			return nil, err
		}
		entries = append(entries, &entry)
	}

	return entries, rows.Err()
}

func (r *repositoryImpl) getByEmail(ctx context.Context, email string) (*WaitlistEntry, error) {
	var entry WaitlistEntry
	var metadataBytes []byte
	err := r.pool.QueryRow(ctx,
		`SELECT id, email, metadata, created_at, invited_at FROM waitlist_entries WHERE email = $1`,
		email,
	).Scan(&entry.Id, &entry.Email, &metadataBytes, &entry.CreatedAt, &entry.InvitedAt)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, errNotFound
		}
		return nil, err
	}

	if err := json.Unmarshal(metadataBytes, &entry.Metadata); err != nil {
		return nil, err
	}

	return &entry, nil
}

func (r *repositoryImpl) markInvited(ctx context.Context, email string) error {
	result, err := r.pool.Exec(ctx,
		`UPDATE waitlist_entries SET invited_at = NOW() WHERE email = $1`,
		email,
	)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return errNotFound
	}
	return nil
}
