package storage

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"strings"

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

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

// QuestStore handles quest system CRUD operations against PostgreSQL.
type QuestStore struct {
	pool *pgxpool.Pool
}

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

// ListTowns returns all towns ordered by level_min ascending.
func (s *QuestStore) ListTowns(ctx context.Context) ([]model.Town, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, name, biome, world_x, world_y, radius, level_min, level_max
		FROM towns
		ORDER BY level_min ASC
	`)
	if err != nil {
		return nil, fmt.Errorf("list towns: %w", err)
	}
	defer rows.Close()

	var towns []model.Town
	for rows.Next() {
		var t model.Town
		if err := rows.Scan(&t.ID, &t.Name, &t.Biome, &t.WorldX, &t.WorldY, &t.Radius, &t.LevelMin, &t.LevelMax); err != nil {
			return nil, fmt.Errorf("scan town: %w", err)
		}
		towns = append(towns, t)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list towns rows: %w", err)
	}
	if towns == nil {
		towns = []model.Town{}
	}
	return towns, nil
}

// GetTown loads a single town by ID. Returns (nil, nil) if not found.
func (s *QuestStore) GetTown(ctx context.Context, townID int64) (*model.Town, error) {
	var t model.Town
	err := s.pool.QueryRow(ctx, `
		SELECT id, name, biome, world_x, world_y, radius, level_min, level_max
		FROM towns WHERE id = $1
	`, townID).Scan(&t.ID, &t.Name, &t.Biome, &t.WorldX, &t.WorldY, &t.Radius, &t.LevelMin, &t.LevelMax)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil
		}
		return nil, fmt.Errorf("get town: %w", err)
	}
	return &t, nil
}

// ListNPCsByTown returns all NPCs in the given town.
func (s *QuestStore) ListNPCsByTown(ctx context.Context, townID int64) ([]model.NPC, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, town_id, name, type, offset_x, offset_y, building_id
		FROM npcs
		WHERE town_id = $1
		ORDER BY id ASC
	`, townID)
	if err != nil {
		return nil, fmt.Errorf("list npcs by town: %w", err)
	}
	defer rows.Close()

	var npcs []model.NPC
	for rows.Next() {
		var n model.NPC
		if err := rows.Scan(&n.ID, &n.TownID, &n.Name, &n.Type, &n.OffsetX, &n.OffsetY, &n.BuildingID); err != nil {
			return nil, fmt.Errorf("scan npc: %w", err)
		}
		npcs = append(npcs, n)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list npcs rows: %w", err)
	}
	if npcs == nil {
		npcs = []model.NPC{}
	}
	return npcs, nil
}

// GetNPCByID loads a single NPC by primary key. Returns (nil, nil) if not found.
func (s *QuestStore) GetNPCByID(ctx context.Context, npcID int64) (*model.NPC, error) {
	var n model.NPC
	err := s.pool.QueryRow(ctx, `
		SELECT id, town_id, name, type, offset_x, offset_y, building_id
		FROM npcs WHERE id = $1
	`, npcID).Scan(&n.ID, &n.TownID, &n.Name, &n.Type, &n.OffsetX, &n.OffsetY, &n.BuildingID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil
		}
		return nil, fmt.Errorf("get npc by id: %w", err)
	}
	return &n, nil
}

// ListAllNPCs returns every NPC across all towns.
func (s *QuestStore) ListAllNPCs(ctx context.Context) ([]model.NPC, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, town_id, name, type, offset_x, offset_y, building_id
		FROM npcs
		ORDER BY town_id ASC, id ASC
	`)
	if err != nil {
		return nil, fmt.Errorf("list all npcs: %w", err)
	}
	defer rows.Close()

	var npcs []model.NPC
	for rows.Next() {
		var n model.NPC
		if err := rows.Scan(&n.ID, &n.TownID, &n.Name, &n.Type, &n.OffsetX, &n.OffsetY, &n.BuildingID); err != nil {
			return nil, fmt.Errorf("scan npc: %w", err)
		}
		npcs = append(npcs, n)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list all npcs rows: %w", err)
	}
	if npcs == nil {
		npcs = []model.NPC{}
	}
	return npcs, nil
}

// ListBuildingsByTown returns all buildings in the given town.
func (s *QuestStore) ListBuildingsByTown(ctx context.Context, townID int64) ([]model.TownBuilding, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, town_id, building_type, offset_x, offset_y, facing, footprint_w, footprint_h
		FROM town_buildings
		WHERE town_id = $1
		ORDER BY id ASC
	`, townID)
	if err != nil {
		return nil, fmt.Errorf("list buildings by town: %w", err)
	}
	defer rows.Close()

	var buildings []model.TownBuilding
	for rows.Next() {
		var b model.TownBuilding
		if err := rows.Scan(&b.ID, &b.TownID, &b.BuildingType, &b.OffsetX, &b.OffsetY, &b.Facing, &b.FootprintW, &b.FootprintH); err != nil {
			return nil, fmt.Errorf("scan building: %w", err)
		}
		buildings = append(buildings, b)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list buildings rows: %w", err)
	}
	if buildings == nil {
		buildings = []model.TownBuilding{}
	}
	return buildings, nil
}

// ListAllBuildings returns every building across all towns (for road_graph preload).
func (s *QuestStore) ListAllBuildings(ctx context.Context) ([]model.TownBuilding, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, town_id, building_type, offset_x, offset_y, facing, footprint_w, footprint_h
		FROM town_buildings
		ORDER BY town_id ASC, id ASC
	`)
	if err != nil {
		return nil, fmt.Errorf("list all buildings: %w", err)
	}
	defer rows.Close()

	var buildings []model.TownBuilding
	for rows.Next() {
		var b model.TownBuilding
		if err := rows.Scan(&b.ID, &b.TownID, &b.BuildingType, &b.OffsetX, &b.OffsetY, &b.Facing, &b.FootprintW, &b.FootprintH); err != nil {
			return nil, fmt.Errorf("scan building: %w", err)
		}
		buildings = append(buildings, b)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list all buildings rows: %w", err)
	}
	if buildings == nil {
		buildings = []model.TownBuilding{}
	}
	return buildings, nil
}

// ListQuestsByNPCForHeroLevel returns quests offered by an NPC that match the hero level range.
func (s *QuestStore) ListQuestsByNPCForHeroLevel(ctx context.Context, npcID int64, heroLevel int) ([]model.Quest, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, npc_id, title, description, type, target_count,
		       target_enemy_type, target_town_id, drop_chance,
		       min_level, max_level, reward_xp, reward_gold, reward_potions
		FROM quests
		WHERE npc_id = $1 AND $2 BETWEEN min_level AND max_level
		ORDER BY min_level ASC, id ASC
	`, npcID, heroLevel)
	if err != nil {
		return nil, fmt.Errorf("list quests by npc for level: %w", err)
	}
	defer rows.Close()

	var quests []model.Quest
	for rows.Next() {
		var q model.Quest
		if err := rows.Scan(
			&q.ID, &q.NPCID, &q.Title, &q.Description, &q.Type, &q.TargetCount,
			&q.TargetEnemyType, &q.TargetTownID, &q.DropChance,
			&q.MinLevel, &q.MaxLevel, &q.RewardXP, &q.RewardGold, &q.RewardPotions,
		); err != nil {
			return nil, fmt.Errorf("scan quest: %w", err)
		}
		quests = append(quests, q)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list quests by npc for level rows: %w", err)
	}
	if quests == nil {
		quests = []model.Quest{}
	}
	return quests, nil
}

// ListQuestsByNPC returns all quest templates offered by the given NPC.
func (s *QuestStore) ListQuestsByNPC(ctx context.Context, npcID int64) ([]model.Quest, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, npc_id, title, description, type, target_count,
		       target_enemy_type, target_town_id, drop_chance,
		       min_level, max_level, reward_xp, reward_gold, reward_potions
		FROM quests
		WHERE npc_id = $1
		ORDER BY min_level ASC, id ASC
	`, npcID)
	if err != nil {
		return nil, fmt.Errorf("list quests by npc: %w", err)
	}
	defer rows.Close()

	var quests []model.Quest
	for rows.Next() {
		var q model.Quest
		if err := rows.Scan(
			&q.ID, &q.NPCID, &q.Title, &q.Description, &q.Type, &q.TargetCount,
			&q.TargetEnemyType, &q.TargetTownID, &q.DropChance,
			&q.MinLevel, &q.MaxLevel, &q.RewardXP, &q.RewardGold, &q.RewardPotions,
		); err != nil {
			return nil, fmt.Errorf("scan quest: %w", err)
		}
		quests = append(quests, q)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list quests rows: %w", err)
	}
	if quests == nil {
		quests = []model.Quest{}
	}
	return quests, nil
}

// ListAllQuestTemplates returns every quest template row (content catalog).
func (s *QuestStore) ListAllQuestTemplates(ctx context.Context) ([]model.Quest, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT id, npc_id, title, description, type, target_count,
		       target_enemy_type, target_town_id, drop_chance,
		       min_level, max_level, reward_xp, reward_gold, reward_potions
		FROM quests
		ORDER BY id ASC
	`)
	if err != nil {
		return nil, fmt.Errorf("list all quest templates: %w", err)
	}
	defer rows.Close()

	var quests []model.Quest
	for rows.Next() {
		var q model.Quest
		if err := rows.Scan(
			&q.ID, &q.NPCID, &q.Title, &q.Description, &q.Type, &q.TargetCount,
			&q.TargetEnemyType, &q.TargetTownID, &q.DropChance,
			&q.MinLevel, &q.MaxLevel, &q.RewardXP, &q.RewardGold, &q.RewardPotions,
		); err != nil {
			return nil, fmt.Errorf("scan quest: %w", err)
		}
		quests = append(quests, q)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list all quest templates rows: %w", err)
	}
	if quests == nil {
		quests = []model.Quest{}
	}
	return quests, nil
}

// UpdateQuestTemplate updates a quest definition row by id.
func (s *QuestStore) UpdateQuestTemplate(ctx context.Context, q *model.Quest) error {
	if q == nil || q.ID <= 0 {
		return fmt.Errorf("invalid quest id")
	}
	cmd, err := s.pool.Exec(ctx, `
		UPDATE quests SET
			npc_id = $2, title = $3, description = $4, type = $5, target_count = $6,
			target_enemy_type = $7, target_town_id = $8, drop_chance = $9,
			min_level = $10, max_level = $11, reward_xp = $12, reward_gold = $13, reward_potions = $14
		WHERE id = $1
	`,
		q.ID, q.NPCID, q.Title, q.Description, q.Type, q.TargetCount,
		q.TargetEnemyType, q.TargetTownID, q.DropChance,
		q.MinLevel, q.MaxLevel, q.RewardXP, q.RewardGold, q.RewardPotions,
	)
	if err != nil {
		return fmt.Errorf("update quest: %w", err)
	}
	if cmd.RowsAffected() == 0 {
		return fmt.Errorf("quest not found: %d", q.ID)
	}
	return nil
}

// CreateQuestTemplate inserts a new quest row and sets q.ID.
func (s *QuestStore) CreateQuestTemplate(ctx context.Context, q *model.Quest) error {
	if q == nil {
		return fmt.Errorf("nil quest")
	}
	if q.NPCID <= 0 || strings.TrimSpace(q.Title) == "" || q.Type == "" {
		return fmt.Errorf("npcId, title and type are required")
	}
	err := s.pool.QueryRow(ctx, `
		INSERT INTO quests (npc_id, title, description, type, target_count,
			target_enemy_type, target_town_id, drop_chance,
			min_level, max_level, reward_xp, reward_gold, reward_potions)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
		RETURNING id
	`,
		q.NPCID, q.Title, q.Description, q.Type, q.TargetCount,
		q.TargetEnemyType, q.TargetTownID, q.DropChance,
		q.MinLevel, q.MaxLevel, q.RewardXP, q.RewardGold, q.RewardPotions,
	).Scan(&q.ID)
	if err != nil {
		return fmt.Errorf("create quest: %w", err)
	}
	return nil
}

// AcceptQuest creates a hero_quests row for the given hero and quest.
// Returns an error if the quest is already accepted/active.
func (s *QuestStore) AcceptQuest(ctx context.Context, heroID int64, questID int64) error {
	_, err := s.pool.Exec(ctx, `
		INSERT INTO hero_quests (hero_id, quest_id, status, progress, accepted_at)
		VALUES ($1, $2, 'accepted', 0, now())
		ON CONFLICT (hero_id, quest_id) DO NOTHING
	`, heroID, questID)
	if err != nil {
		return fmt.Errorf("accept quest: %w", err)
	}
	return nil
}

// ListHeroQuests returns all quests for the hero with their quest template joined.
func (s *QuestStore) ListHeroQuests(ctx context.Context, heroID int64) ([]model.HeroQuest, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT hq.id, hq.hero_id, hq.quest_id, hq.status, hq.progress,
		       hq.accepted_at, hq.completed_at, hq.claimed_at,
		       q.id, q.npc_id, q.title, q.description, q.type, q.target_count,
		       q.target_enemy_type, q.target_town_id, q.drop_chance,
		       q.min_level, q.max_level, q.reward_xp, q.reward_gold, q.reward_potions
		FROM hero_quests hq
		JOIN quests q ON hq.quest_id = q.id
		WHERE hq.hero_id = $1
		ORDER BY hq.accepted_at DESC
	`, heroID)
	if err != nil {
		return nil, fmt.Errorf("list hero quests: %w", err)
	}
	defer rows.Close()

	var hqs []model.HeroQuest
	for rows.Next() {
		var hq model.HeroQuest
		var q model.Quest
		if err := rows.Scan(
			&hq.ID, &hq.HeroID, &hq.QuestID, &hq.Status, &hq.Progress,
			&hq.AcceptedAt, &hq.CompletedAt, &hq.ClaimedAt,
			&q.ID, &q.NPCID, &q.Title, &q.Description, &q.Type, &q.TargetCount,
			&q.TargetEnemyType, &q.TargetTownID, &q.DropChance,
			&q.MinLevel, &q.MaxLevel, &q.RewardXP, &q.RewardGold, &q.RewardPotions,
		); err != nil {
			return nil, fmt.Errorf("scan hero quest: %w", err)
		}
		hq.Quest = &q
		hqs = append(hqs, hq)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("list hero quests rows: %w", err)
	}
	if hqs == nil {
		hqs = []model.HeroQuest{}
	}
	return hqs, nil
}

// IncrementQuestProgress increments progress for all matching accepted quests.
// For kill_count: objectiveType="kill_count", targetValue=enemy type (or "" for any).
// For collect_item: objectiveType="collect_item", delta from drop chance roll.
// Quests that reach target_count are automatically marked as completed.
func (s *QuestStore) IncrementQuestProgress(ctx context.Context, heroID int64, objectiveType string, targetValue string, delta int) error {
	if delta <= 0 {
		return nil
	}

	// Update progress for matching quests. A quest matches if:
	// - It belongs to this hero and is in 'accepted' status
	// - Its type matches objectiveType
	// - Its target_enemy_type matches targetValue (or target_enemy_type IS NULL for "any")
	var query string
	var args []any

	if targetValue != "" {
		query = `
			UPDATE hero_quests hq
			SET progress = LEAST(progress + $3, q.target_count),
			    status = CASE WHEN progress + $3 >= q.target_count THEN 'completed' ELSE status END,
			    completed_at = CASE WHEN progress + $3 >= q.target_count AND completed_at IS NULL THEN now() ELSE completed_at END
			FROM quests q
			WHERE hq.quest_id = q.id
			  AND hq.hero_id = $1
			  AND hq.status = 'accepted'
			  AND q.type = $2
			  AND (q.target_enemy_type = $4 OR q.target_enemy_type IS NULL)
		`
		args = []any{heroID, objectiveType, delta, targetValue}
	} else {
		query = `
			UPDATE hero_quests hq
			SET progress = LEAST(progress + $3, q.target_count),
			    status = CASE WHEN progress + $3 >= q.target_count THEN 'completed' ELSE status END,
			    completed_at = CASE WHEN progress + $3 >= q.target_count AND completed_at IS NULL THEN now() ELSE completed_at END
			FROM quests q
			WHERE hq.quest_id = q.id
			  AND hq.hero_id = $1
			  AND hq.status = 'accepted'
			  AND q.type = $2
		`
		args = []any{heroID, objectiveType, delta}
	}

	_, err := s.pool.Exec(ctx, query, args...)
	if err != nil {
		return fmt.Errorf("increment quest progress: %w", err)
	}
	return nil
}

// ClaimQuestReward marks a completed quest as claimed and returns the rewards.
// Returns an error if the quest is not in 'completed' status.
func (s *QuestStore) ClaimQuestReward(ctx context.Context, heroID int64, questID int64) (*model.QuestReward, error) {
	var reward model.QuestReward

	err := s.pool.QueryRow(ctx, `
		UPDATE hero_quests hq
		SET status = 'claimed', claimed_at = now()
		FROM quests q
		WHERE hq.quest_id = q.id
		  AND hq.hero_id = $1
		  AND hq.id = $2
		  AND hq.status = 'completed'
		RETURNING q.reward_xp, q.reward_gold, q.reward_potions
	`, heroID, questID).Scan(&reward.XP, &reward.Gold, &reward.Potions)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, fmt.Errorf("quest not found or not in completed status")
		}
		return nil, fmt.Errorf("claim quest reward: %w", err)
	}

	return &reward, nil
}

// AbandonQuest removes a hero's quest entry. Only accepted/completed quests
// can be abandoned (not already claimed).
func (s *QuestStore) AbandonQuest(ctx context.Context, heroID int64, questID int64) error {
	tag, err := s.pool.Exec(ctx, `
		DELETE FROM hero_quests
		WHERE hero_id = $1 AND quest_id = $2 AND status != 'claimed'
	`, heroID, questID)
	if err != nil {
		return fmt.Errorf("abandon quest: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("quest not found or already claimed")
	}
	return nil
}

// IncrementVisitTownProgress updates visit_town quests when a hero enters a town.
func (s *QuestStore) IncrementVisitTownProgress(ctx context.Context, heroID int64, townID int64) error {
	_, err := s.pool.Exec(ctx, `
		UPDATE hero_quests hq
		SET progress = LEAST(progress + 1, q.target_count),
		    status = CASE WHEN progress + 1 >= q.target_count THEN 'completed' ELSE status END,
		    completed_at = CASE WHEN progress + 1 >= q.target_count AND completed_at IS NULL THEN now() ELSE completed_at END
		FROM quests q
		WHERE hq.quest_id = q.id
		  AND hq.hero_id = $1
		  AND hq.status = 'accepted'
		  AND q.type = 'visit_town'
		  AND q.target_town_id = $2
	`, heroID, townID)
	if err != nil {
		return fmt.Errorf("increment visit town progress: %w", err)
	}
	return nil
}

// IncrementCollectItemProgress increments collect_item quests by rolling the drop_chance.
// Called after a kill; each matching quest gets a roll for each delta kill.
func (s *QuestStore) IncrementCollectItemProgress(ctx context.Context, heroID int64, enemyType string) error {
	// Fetch active collect_item quests for this hero
	rows, err := s.pool.Query(ctx, `
		SELECT hq.id, q.target_count, hq.progress, q.drop_chance, q.target_enemy_type
		FROM hero_quests hq
		JOIN quests q ON hq.quest_id = q.id
		WHERE hq.hero_id = $1
		  AND hq.status = 'accepted'
		  AND q.type = 'collect_item'
	`, heroID)
	if err != nil {
		return fmt.Errorf("list collect item quests: %w", err)
	}
	defer rows.Close()

	type collectQuest struct {
		hqID            int64
		targetCount     int
		progress        int
		dropChance      float64
		targetEnemyType *string
	}
	var cqs []collectQuest
	for rows.Next() {
		var cq collectQuest
		if err := rows.Scan(&cq.hqID, &cq.targetCount, &cq.progress, &cq.dropChance, &cq.targetEnemyType); err != nil {
			return fmt.Errorf("scan collect quest: %w", err)
		}
		cqs = append(cqs, cq)
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("collect item quests rows: %w", err)
	}

	for _, cq := range cqs {
		// Check if the enemy type matches (nil = any enemy)
		if cq.targetEnemyType != nil && *cq.targetEnemyType != enemyType {
			continue
		}
		if cq.progress >= cq.targetCount {
			continue
		}
		// Roll the drop chance
		roll := randFloat64()
		if roll >= cq.dropChance {
			continue
		}
		// Increment progress by 1
		_, err := s.pool.Exec(ctx, `
			UPDATE hero_quests
			SET progress = LEAST(progress + 1, $2),
			    status = CASE WHEN progress + 1 >= $2 THEN 'completed' ELSE status END,
			    completed_at = CASE WHEN progress + 1 >= $2 AND completed_at IS NULL THEN now() ELSE completed_at END
			WHERE id = $1
		`, cq.hqID, cq.targetCount)
		if err != nil {
			return fmt.Errorf("update collect item progress: %w", err)
		}
	}

	return nil
}

// randFloat64 wraps rand.Float64 for testability.
var randFloat64 = rand.Float64
