package storage

import (
	"context"
	"errors"
	"fmt"

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

	"github.com/denisovdennis/autohero/internal/model"
)

// ErrInventoryFull is returned when the backpack already holds MaxInventorySlots items.
var ErrInventoryFull = errors.New("inventory full")

// GearStore handles all gear CRUD operations against PostgreSQL.
type GearStore struct {
	pool *pgxpool.Pool
}

// NewGearStore creates a new GearStore backed by the given connection pool.
func NewGearStore(pool *pgxpool.Pool) *GearStore {
	return &GearStore{pool: pool}
}

// CreateItem inserts a new gear item into the database and populates item.ID.
func (s *GearStore) CreateItem(ctx context.Context, item *model.GearItem) error {
	err := s.pool.QueryRow(ctx, `
		INSERT INTO gear (slot, form_id, name, subtype, rarity, ilvl, base_primary, primary_stat,
		                   stat_type, speed_modifier, crit_chance, agility_bonus, set_name, special_effect)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
		RETURNING id
	`,
		string(item.Slot), item.FormID, item.Name, item.Subtype,
		string(item.Rarity), item.Ilvl, item.BasePrimary, item.PrimaryStat,
		item.StatType, item.SpeedModifier, item.CritChance, item.AgilityBonus,
		item.SetName, item.SpecialEffect,
	).Scan(&item.ID)
	if err != nil {
		return fmt.Errorf("create gear item: %w", err)
	}
	return nil
}

// UpdateItem updates an existing row in `gear` by id (all columns except created_at).
func (s *GearStore) UpdateItem(ctx context.Context, item *model.GearItem) error {
	if item == nil || item.ID <= 0 {
		return fmt.Errorf("invalid gear id")
	}
	cmd, err := s.pool.Exec(ctx, `
		UPDATE gear SET
			slot = $2, form_id = $3, name = $4, subtype = $5, rarity = $6, ilvl = $7,
			base_primary = $8, primary_stat = $9, stat_type = $10,
			speed_modifier = $11, crit_chance = $12, agility_bonus = $13, set_name = $14, special_effect = $15
		WHERE id = $1
	`,
		item.ID, string(item.Slot), item.FormID, item.Name, item.Subtype,
		string(item.Rarity), item.Ilvl, item.BasePrimary, item.PrimaryStat,
		item.StatType, item.SpeedModifier, item.CritChance, item.AgilityBonus,
		item.SetName, item.SpecialEffect,
	)
	if err != nil {
		return fmt.Errorf("update gear: %w", err)
	}
	if cmd.RowsAffected() == 0 {
		return fmt.Errorf("gear not found: %d", item.ID)
	}
	return nil
}

// GetItem loads a single gear item by ID. Returns (nil, nil) if not found.
func (s *GearStore) GetItem(ctx context.Context, id int64) (*model.GearItem, error) {
	var item model.GearItem
	var slot, rarity string
	err := s.pool.QueryRow(ctx, `
		SELECT id, slot, form_id, name, subtype, rarity, ilvl,
		       base_primary, primary_stat, stat_type,
		       speed_modifier, crit_chance, agility_bonus,
		       set_name, special_effect
		FROM gear WHERE id = $1
	`, id).Scan(
		&item.ID, &slot, &item.FormID, &item.Name, &item.Subtype,
		&rarity, &item.Ilvl,
		&item.BasePrimary, &item.PrimaryStat, &item.StatType,
		&item.SpeedModifier, &item.CritChance, &item.AgilityBonus,
		&item.SetName, &item.SpecialEffect,
	)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil
		}
		return nil, fmt.Errorf("get gear item: %w", err)
	}
	item.Slot = model.EquipmentSlot(slot)
	item.Rarity = model.Rarity(rarity)
	return &item, nil
}

// GetHeroGear returns all equipped gear for a hero, keyed by slot.
func (s *GearStore) GetHeroGear(ctx context.Context, heroID int64) (map[model.EquipmentSlot]*model.GearItem, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT g.id, g.slot, g.form_id, g.name, g.subtype, g.rarity, g.ilvl,
		       g.base_primary, g.primary_stat, g.stat_type,
		       g.speed_modifier, g.crit_chance, g.agility_bonus,
		       g.set_name, g.special_effect
		FROM hero_gear hg
		JOIN gear g ON hg.gear_id = g.id
		WHERE hg.hero_id = $1
	`, heroID)
	if err != nil {
		return nil, fmt.Errorf("get hero gear: %w", err)
	}
	defer rows.Close()

	gear := make(map[model.EquipmentSlot]*model.GearItem)
	for rows.Next() {
		var item model.GearItem
		var slot, rarity string
		if err := rows.Scan(
			&item.ID, &slot, &item.FormID, &item.Name, &item.Subtype,
			&rarity, &item.Ilvl,
			&item.BasePrimary, &item.PrimaryStat, &item.StatType,
			&item.SpeedModifier, &item.CritChance, &item.AgilityBonus,
			&item.SetName, &item.SpecialEffect,
		); err != nil {
			return nil, fmt.Errorf("scan gear item: %w", err)
		}
		item.Slot = model.EquipmentSlot(slot)
		item.Rarity = model.Rarity(rarity)
		gear[item.Slot] = &item
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("hero gear rows: %w", err)
	}
	return gear, nil
}

// EquipItem equips a gear item into the given slot for a hero (upsert).
// Any previously equipped item in that slot is moved to the backpack (unless it is the same gear_id).
// If the new item was in the backpack, it is removed and remaining slots are reindexed (0..n-1).
// Returns ErrInventoryFull if the previous item cannot be stashed (transaction rolled back).
func (s *GearStore) EquipItem(ctx context.Context, heroID int64, slot model.EquipmentSlot, gearID int64) error {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return fmt.Errorf("equip gear item begin: %w", err)
	}
	defer tx.Rollback(ctx)

	var prevGearID int64
	err = tx.QueryRow(ctx, `
		SELECT gear_id FROM hero_gear WHERE hero_id = $1 AND slot = $2
	`, heroID, string(slot)).Scan(&prevGearID)
	hasPrev := true
	if errors.Is(err, pgx.ErrNoRows) {
		hasPrev = false
		err = nil
	} else if err != nil {
		return fmt.Errorf("equip read previous: %w", err)
	}

	if _, err := tx.Exec(ctx, `
		INSERT INTO hero_gear (hero_id, slot, gear_id)
		VALUES ($1, $2, $3)
		ON CONFLICT (hero_id, slot) DO UPDATE SET gear_id = EXCLUDED.gear_id
	`, heroID, string(slot), gearID); err != nil {
		return fmt.Errorf("equip gear item: %w", err)
	}
	if err := compactInventoryAfterRemovingGear(ctx, tx, heroID, gearID); err != nil {
		return err
	}
	if hasPrev && prevGearID != gearID {
		if err := addToInventoryTx(ctx, tx, heroID, prevGearID); err != nil {
			return err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return fmt.Errorf("equip gear item commit: %w", err)
	}
	return nil
}

func addToInventoryTx(ctx context.Context, tx pgx.Tx, heroID, gearID int64) error {
	var n int
	if err := tx.QueryRow(ctx, `
		SELECT COUNT(*) FROM hero_inventory WHERE hero_id = $1
	`, heroID).Scan(&n); err != nil {
		return fmt.Errorf("inventory count: %w", err)
	}
	if n >= model.MaxInventorySlots {
		return ErrInventoryFull
	}
	if _, err := tx.Exec(ctx, `
		INSERT INTO hero_inventory (hero_id, slot_index, gear_id)
		VALUES ($1, $2, $3)
	`, heroID, n, gearID); err != nil {
		return fmt.Errorf("add to inventory: %w", err)
	}
	return nil
}

// compactInventoryAfterRemovingGear deletes one backpack row if present; if a row
// was removed, rewrites hero_inventory with contiguous slot_index.
func compactInventoryAfterRemovingGear(ctx context.Context, tx pgx.Tx, heroID, gearID int64) error {
	cmd, err := tx.Exec(ctx, `
		DELETE FROM hero_inventory WHERE hero_id = $1 AND gear_id = $2
	`, heroID, gearID)
	if err != nil {
		return fmt.Errorf("remove equipped gear from inventory: %w", err)
	}
	if cmd.RowsAffected() == 0 {
		return nil
	}
	rows, err := tx.Query(ctx, `
		SELECT gear_id FROM hero_inventory WHERE hero_id = $1 ORDER BY slot_index ASC
	`, heroID)
	if err != nil {
		return fmt.Errorf("list inventory after remove: %w", err)
	}
	defer rows.Close()
	var ids []int64
	for rows.Next() {
		var id int64
		if err := rows.Scan(&id); err != nil {
			return fmt.Errorf("scan inventory gear_id: %w", err)
		}
		ids = append(ids, id)
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("inventory rows: %w", err)
	}

	if _, err := tx.Exec(ctx, `DELETE FROM hero_inventory WHERE hero_id = $1`, heroID); err != nil {
		return fmt.Errorf("clear inventory for compact: %w", err)
	}
	for i, gid := range ids {
		if _, err := tx.Exec(ctx, `
			INSERT INTO hero_inventory (hero_id, slot_index, gear_id)
			VALUES ($1, $2, $3)
		`, heroID, i, gid); err != nil {
			return fmt.Errorf("reinsert inventory slot %d: %w", i, err)
		}
	}
	return nil
}

// DeleteGearItem removes a gear row by id (e.g. discarded drop not sold). Fails if still equipped.
func (s *GearStore) DeleteGearItem(ctx context.Context, id int64) error {
	cmd, err := s.pool.Exec(ctx, `DELETE FROM gear WHERE id = $1`, id)
	if err != nil {
		return fmt.Errorf("delete gear item: %w", err)
	}
	if cmd.RowsAffected() == 0 {
		return fmt.Errorf("delete gear item: no row for id %d", id)
	}
	return nil
}

// UnequipSlot moves equipped gear from the given slot into the hero's backpack.
// Returns ErrInventoryFull if there is no free slot (equipped row is left unchanged).
// If the slot is empty, returns nil (idempotent).
func (s *GearStore) UnequipSlot(ctx context.Context, heroID int64, slot model.EquipmentSlot) error {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return fmt.Errorf("unequip begin: %w", err)
	}
	defer tx.Rollback(ctx)

	var gearID int64
	err = tx.QueryRow(ctx, `
		SELECT gear_id FROM hero_gear WHERE hero_id = $1 AND slot = $2
	`, heroID, string(slot)).Scan(&gearID)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("unequip read slot: %w", err)
	}

	if err := addToInventoryTx(ctx, tx, heroID, gearID); err != nil {
		return err
	}

	if _, err := tx.Exec(ctx, `
		DELETE FROM hero_gear WHERE hero_id = $1 AND slot = $2
	`, heroID, string(slot)); err != nil {
		return fmt.Errorf("unequip delete hero_gear: %w", err)
	}

	if err := tx.Commit(ctx); err != nil {
		return fmt.Errorf("unequip commit: %w", err)
	}
	return nil
}

// GetHeroInventory loads unequipped gear ordered by slot_index.
func (s *GearStore) GetHeroInventory(ctx context.Context, heroID int64) ([]*model.GearItem, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT g.id, g.slot, g.form_id, g.name, g.subtype, g.rarity, g.ilvl,
		       g.base_primary, g.primary_stat, g.stat_type,
		       g.speed_modifier, g.crit_chance, g.agility_bonus,
		       g.set_name, g.special_effect
		FROM hero_inventory hi
		JOIN gear g ON hi.gear_id = g.id
		WHERE hi.hero_id = $1
		ORDER BY hi.slot_index ASC
	`, heroID)
	if err != nil {
		return nil, fmt.Errorf("get hero inventory: %w", err)
	}
	defer rows.Close()

	var out []*model.GearItem
	for rows.Next() {
		var item model.GearItem
		var slot, rarity string
		if err := rows.Scan(
			&item.ID, &slot, &item.FormID, &item.Name, &item.Subtype,
			&rarity, &item.Ilvl,
			&item.BasePrimary, &item.PrimaryStat, &item.StatType,
			&item.SpeedModifier, &item.CritChance, &item.AgilityBonus,
			&item.SetName, &item.SpecialEffect,
		); err != nil {
			return nil, fmt.Errorf("scan inventory gear: %w", err)
		}
		item.Slot = model.EquipmentSlot(slot)
		item.Rarity = model.Rarity(rarity)
		out = append(out, &item)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("hero inventory rows: %w", err)
	}
	return out, nil
}

// AddToInventory inserts gear into the next free backpack slot. Fails if inventory is full.
func (s *GearStore) AddToInventory(ctx context.Context, heroID, gearID int64) error {
	var n int
	if err := s.pool.QueryRow(ctx, `
		SELECT COUNT(*) FROM hero_inventory WHERE hero_id = $1
	`, heroID).Scan(&n); err != nil {
		return fmt.Errorf("inventory count: %w", err)
	}
	if n >= model.MaxInventorySlots {
		return ErrInventoryFull
	}
	_, err := s.pool.Exec(ctx, `
		INSERT INTO hero_inventory (hero_id, slot_index, gear_id)
		VALUES ($1, $2, $3)
	`, heroID, n, gearID)
	if err != nil {
		return fmt.Errorf("add to inventory: %w", err)
	}
	return nil
}

// ReplaceHeroInventory deletes all backpack rows for the hero and re-inserts from the slice.
// Used when persisting offline-simulated heroes with in-memory-only gear rows (id may be 0).
func (s *GearStore) ReplaceHeroInventory(ctx context.Context, heroID int64, items []*model.GearItem) error {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return fmt.Errorf("replace inventory begin: %w", err)
	}
	defer tx.Rollback(ctx)

	if _, err := tx.Exec(ctx, `DELETE FROM hero_inventory WHERE hero_id = $1`, heroID); err != nil {
		return fmt.Errorf("replace inventory delete: %w", err)
	}
	for i, item := range items {
		if item == nil || i >= model.MaxInventorySlots {
			continue
		}
		gid := item.ID
		if gid == 0 {
			err := tx.QueryRow(ctx, `
				INSERT INTO gear (slot, form_id, name, subtype, rarity, ilvl, base_primary, primary_stat,
				                  stat_type, speed_modifier, crit_chance, agility_bonus, set_name, special_effect)
				VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
				RETURNING id
			`,
				string(item.Slot), item.FormID, item.Name, item.Subtype,
				string(item.Rarity), item.Ilvl, item.BasePrimary, item.PrimaryStat,
				item.StatType, item.SpeedModifier, item.CritChance, item.AgilityBonus,
				item.SetName, item.SpecialEffect,
			).Scan(&gid)
			if err != nil {
				return fmt.Errorf("replace inventory create gear: %w", err)
			}
			item.ID = gid
		}
		if _, err := tx.Exec(ctx, `
			INSERT INTO hero_inventory (hero_id, slot_index, gear_id)
			VALUES ($1, $2, $3)
		`, heroID, i, gid); err != nil {
			return fmt.Errorf("replace inventory insert: %w", err)
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return fmt.Errorf("replace inventory commit: %w", err)
	}
	return nil
}
