package codes

import (
	"context"
	"fmt"
	"time"
)

// BatchInfo summarises a code_batches row with aggregate code counts, for the
// admin batch-list view. It carries no plaintext — only hashes live in `codes`.
type BatchInfo struct {
	ID        int64
	Channel   BatchChannel
	CreatedBy string
	Note      string
	CreatedAt time.Time
	Total     int
	Redeemed  int
	Void      int
	Unused    int
}

// ListBatches returns code batches newest-first with aggregate counts, plus the
// total number of batches (for pagination).
func (s *Store) ListBatches(ctx context.Context, limit, offset int) ([]BatchInfo, int, error) {
	if limit <= 0 || limit > 500 {
		limit = 50
	}
	if offset < 0 {
		offset = 0
	}

	var total int
	if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM code_batches`).Scan(&total); err != nil {
		return nil, 0, fmt.Errorf("store.ListBatches count: %w", err)
	}

	rows, err := s.db.QueryContext(ctx,
		`SELECT b.id, b.channel, b.created_by, COALESCE(b.note, ''), b.created_at,
		        COUNT(c.id),
		        SUM(c.status = 'redeemed'),
		        SUM(c.status = 'void'),
		        SUM(c.status = 'unused')
		   FROM code_batches b
		   LEFT JOIN codes c ON c.batch_id = b.id
		  GROUP BY b.id, b.channel, b.created_by, b.note, b.created_at
		  ORDER BY b.id DESC
		  LIMIT ? OFFSET ?`, limit, offset)
	if err != nil {
		return nil, 0, fmt.Errorf("store.ListBatches: %w", err)
	}
	defer rows.Close()

	var out []BatchInfo
	for rows.Next() {
		var bi BatchInfo
		// SUM over a possibly-empty group yields NULL; scan into nullable ints.
		var redeemed, void, unused, totalCnt nullInt
		if err := rows.Scan(&bi.ID, &bi.Channel, &bi.CreatedBy, &bi.Note, &bi.CreatedAt,
			&totalCnt, &redeemed, &void, &unused); err != nil {
			return nil, 0, fmt.Errorf("store.ListBatches scan: %w", err)
		}
		bi.Total = totalCnt.val
		bi.Redeemed = redeemed.val
		bi.Void = void.val
		bi.Unused = unused.val
		out = append(out, bi)
	}
	return out, total, rows.Err()
}

// VoidBatch marks every still-unused code in a batch as void and returns the
// number of codes voided. Already-redeemed codes are left untouched.
func (s *Store) VoidBatch(ctx context.Context, batchID int64) (int64, error) {
	res, err := s.db.ExecContext(ctx,
		`UPDATE codes SET status = 'void' WHERE batch_id = ? AND status = 'unused'`, batchID)
	if err != nil {
		return 0, fmt.Errorf("store.VoidBatch: %w", err)
	}
	n, err := res.RowsAffected()
	if err != nil {
		return 0, fmt.Errorf("store.VoidBatch rows: %w", err)
	}
	return n, nil
}

// nullInt scans a possibly-NULL integer aggregate, defaulting to 0.
type nullInt struct{ val int }

func (n *nullInt) Scan(src any) error {
	switch v := src.(type) {
	case nil:
		n.val = 0
	case int64:
		n.val = int(v)
	case []byte:
		_, err := fmt.Sscanf(string(v), "%d", &n.val)
		return err
	default:
		return fmt.Errorf("nullInt: unsupported type %T", src)
	}
	return nil
}
