package channelserver

import (
	"database/sql"
	"errors"
	"time"

	"github.com/jmoiron/sqlx"
	"go.uber.org/zap"
)

// GachaRepository centralizes all database access for gacha-related tables
// (gacha_shop, gacha_entries, gacha_items, gacha_stepup, gacha_box).
type GachaRepository struct {
	db     *sqlx.DB
	logger *zap.Logger
}

// NewGachaRepository creates a new GachaRepository.
func NewGachaRepository(db *sqlx.DB, logger *zap.Logger) *GachaRepository {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &GachaRepository{db: db, logger: logger}
}

// GetEntryForTransaction reads the cost type/amount and roll count for a gacha transaction.
func (r *GachaRepository) GetEntryForTransaction(gachaID uint32, rollID uint8) (itemType uint8, itemNumber uint16, rolls int, err error) {
	err = r.db.QueryRowx(
		`SELECT item_type, item_number, rolls FROM gacha_entries WHERE gacha_id = $1 AND entry_type = $2`,
		gachaID, rollID,
	).Scan(&itemType, &itemNumber, &rolls)
	return
}

// GetRewardPool returns the entry_type=100 reward pool for a gacha, ordered by weight descending.
// Entries with no corresponding items in gacha_items are excluded.
func (r *GachaRepository) GetRewardPool(gachaID uint32) ([]GachaEntry, error) {
	var entries []GachaEntry
	rows, err := r.db.Queryx(
		`SELECT e.id, e.weight, e.rarity FROM gacha_entries e
		 WHERE e.gacha_id = $1 AND e.entry_type = 100
		   AND EXISTS (SELECT 1 FROM gacha_items gi WHERE gi.entry_id = e.id)
		 ORDER BY e.weight DESC`,
		gachaID,
	)
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()
	for rows.Next() {
		var entry GachaEntry
		if err := rows.StructScan(&entry); err == nil {
			entries = append(entries, entry)
		}
	}
	return entries, nil
}

// GetItemsForEntry returns the items associated with a gacha entry ID.
func (r *GachaRepository) GetItemsForEntry(entryID uint32) ([]GachaItem, error) {
	var items []GachaItem
	rows, err := r.db.Queryx(
		`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id = $1`,
		entryID,
	)
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()
	for rows.Next() {
		var item GachaItem
		if err := rows.StructScan(&item); err != nil {
			r.logger.Warn("Skipping gacha_items row that failed to scan — check item_type (≤255) and item_id/quantity (≤65535) for out-of-range values",
				zap.Uint32("entryID", entryID), zap.Error(err))
			continue
		}
		items = append(items, item)
	}
	return items, nil
}

// GetGuaranteedItems returns items for the entry matching a roll type and gacha ID.
func (r *GachaRepository) GetGuaranteedItems(rollType uint8, gachaID uint32) ([]GachaItem, error) {
	var items []GachaItem
	rows, err := r.db.Queryx(
		`SELECT item_type, item_id, quantity FROM gacha_items WHERE entry_id = (SELECT id FROM gacha_entries WHERE entry_type = $1 AND gacha_id = $2)`,
		rollType, gachaID,
	)
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()
	for rows.Next() {
		var item GachaItem
		if err := rows.StructScan(&item); err != nil {
			r.logger.Warn("Skipping guaranteed gacha_items row that failed to scan — check item_type (≤255) and item_id/quantity (≤65535) for out-of-range values",
				zap.Uint8("rollType", rollType), zap.Uint32("gachaID", gachaID), zap.Error(err))
			continue
		}
		items = append(items, item)
	}
	return items, nil
}

// Stepup methods

// GetStepupStep returns the current stepup step for a character on a gacha.
func (r *GachaRepository) GetStepupStep(gachaID uint32, charID uint32) (uint8, error) {
	var step uint8
	err := r.db.QueryRow(
		`SELECT step FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`,
		gachaID, charID,
	).Scan(&step)
	return step, err
}

// GetStepupWithTime returns the current step and creation time for a stepup entry.
// Returns sql.ErrNoRows if no entry exists.
func (r *GachaRepository) GetStepupWithTime(gachaID uint32, charID uint32) (uint8, time.Time, error) {
	var step uint8
	var createdAt time.Time
	err := r.db.QueryRow(
		`SELECT step, COALESCE(created_at, '2000-01-01'::timestamptz) FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`,
		gachaID, charID,
	).Scan(&step, &createdAt)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, time.Time{}, err
	}
	return step, createdAt, err
}

// HasEntryType returns whether a gacha has any entries of the given type.
func (r *GachaRepository) HasEntryType(gachaID uint32, entryType uint8) (bool, error) {
	var count int
	err := r.db.QueryRow(
		`SELECT COUNT(1) FROM gacha_entries WHERE gacha_id = $1 AND entry_type = $2`,
		gachaID, entryType,
	).Scan(&count)
	return count > 0, err
}

// DeleteStepup removes the stepup state for a character on a gacha.
func (r *GachaRepository) DeleteStepup(gachaID uint32, charID uint32) error {
	_, err := r.db.Exec(
		`DELETE FROM gacha_stepup WHERE gacha_id = $1 AND character_id = $2`,
		gachaID, charID,
	)
	return err
}

// InsertStepup records a new stepup step for a character on a gacha.
func (r *GachaRepository) InsertStepup(gachaID uint32, step uint8, charID uint32) error {
	_, err := r.db.Exec(
		`INSERT INTO gacha_stepup (gacha_id, step, character_id) VALUES ($1, $2, $3)`,
		gachaID, step, charID,
	)
	return err
}

// Box gacha methods

// GetBoxEntryIDs returns the entry IDs already drawn for a box gacha.
func (r *GachaRepository) GetBoxEntryIDs(gachaID uint32, charID uint32) ([]uint32, error) {
	var ids []uint32
	rows, err := r.db.Queryx(
		`SELECT entry_id FROM gacha_box WHERE gacha_id = $1 AND character_id = $2`,
		gachaID, charID,
	)
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()
	for rows.Next() {
		var id uint32
		if err := rows.Scan(&id); err == nil {
			ids = append(ids, id)
		}
	}
	return ids, nil
}

// InsertBoxEntry records a drawn entry in a box gacha.
func (r *GachaRepository) InsertBoxEntry(gachaID uint32, entryID uint32, charID uint32) error {
	_, err := r.db.Exec(
		`INSERT INTO gacha_box (gacha_id, entry_id, character_id) VALUES ($1, $2, $3)`,
		gachaID, entryID, charID,
	)
	return err
}

// DeleteBoxEntries resets all drawn entries for a box gacha.
func (r *GachaRepository) DeleteBoxEntries(gachaID uint32, charID uint32) error {
	_, err := r.db.Exec(
		`DELETE FROM gacha_box WHERE gacha_id = $1 AND character_id = $2`,
		gachaID, charID,
	)
	return err
}

// Shop listing methods

// ListShop returns all gacha shop definitions.
func (r *GachaRepository) ListShop() ([]Gacha, error) {
	var gachas []Gacha
	rows, err := r.db.Queryx(
		`SELECT id, min_gr, min_hr, name, url_banner, url_feature, url_thumbnail, wide, recommended, gacha_type, hidden FROM gacha_shop`,
	)
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()
	for rows.Next() {
		var g Gacha
		if err := rows.StructScan(&g); err == nil {
			gachas = append(gachas, g)
		}
	}
	return gachas, nil
}

// GetShopType returns the gacha_type for a gacha shop ID.
func (r *GachaRepository) GetShopType(shopID uint32) (int, error) {
	var gachaType int
	err := r.db.QueryRow(
		`SELECT gacha_type FROM gacha_shop WHERE id = $1`,
		shopID,
	).Scan(&gachaType)
	return gachaType, err
}

// GetAllEntries returns all entries for a gacha, ordered by weight descending.
func (r *GachaRepository) GetAllEntries(gachaID uint32) ([]GachaEntry, error) {
	var entries []GachaEntry
	rows, err := r.db.Queryx(
		`SELECT entry_type, id, item_type, item_number, item_quantity, weight, rarity, rolls, daily_limit, frontier_points, COALESCE(name, '') AS name FROM gacha_entries WHERE gacha_id = $1 ORDER BY weight DESC`,
		gachaID,
	)
	if err != nil {
		return nil, err
	}
	defer func() { _ = rows.Close() }()
	for rows.Next() {
		var entry GachaEntry
		if err := rows.StructScan(&entry); err == nil {
			entries = append(entries, entry)
		}
	}
	return entries, nil
}

// GetWeightDivisor returns the total weight / 100000 for probability display.
func (r *GachaRepository) GetWeightDivisor(gachaID uint32) (float64, error) {
	var divisor float64
	err := r.db.QueryRow(
		`SELECT COALESCE(SUM(weight) / 100000.0, 0) AS chance FROM gacha_entries WHERE gacha_id = $1`,
		gachaID,
	).Scan(&divisor)
	return divisor, err
}
