package handler

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"math"
	"math/rand"
	"net/http"
	"strconv"
	"time"

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

// NPCHandler serves NPC interaction API endpoints.
type NPCHandler struct {
	questStore *storage.QuestStore
	heroStore  *storage.HeroStore
	gearStore  *storage.GearStore
	logStore   *storage.LogStore
	logger     *slog.Logger
}

// NewNPCHandler creates a new NPCHandler.
func NewNPCHandler(questStore *storage.QuestStore, heroStore *storage.HeroStore, gearStore *storage.GearStore, logStore *storage.LogStore, logger *slog.Logger) *NPCHandler {
	return &NPCHandler{
		questStore: questStore,
		heroStore:  heroStore,
		gearStore:  gearStore,
		logStore:   logStore,
		logger:     logger,
	}
}

// addLog is a fire-and-forget helper that writes an adventure log entry.
func (h *NPCHandler) addLog(heroID int64, message string) {
	if h.logStore == nil {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	if err := h.logStore.Add(ctx, heroID, message); err != nil {
		h.logger.Warn("failed to write adventure log", "hero_id", heroID, "error", err)
	}
}

// dist2D calculates the Euclidean distance between two 2D points.
func dist2D(x1, y1, x2, y2 float64) float64 {
	dx := x1 - x2
	dy := y1 - y2
	return math.Sqrt(dx*dx + dy*dy)
}

// InteractNPC handles POST /api/v1/hero/npc-interact.
// The hero interacts with a specific NPC; checks proximity to the NPC's town.
func (h *NPCHandler) InteractNPC(w http.ResponseWriter, r *http.Request) {
	telegramID, ok := resolveTelegramID(r)
	if !ok {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "missing telegramId",
		})
		return
	}

	var req struct {
		NPCID     int64   `json:"npcId"`
		PositionX float64 `json:"positionX"`
		PositionY float64 `json:"positionY"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "invalid request body",
		})
		return
	}
	if req.NPCID == 0 {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "npcId is required",
		})
		return
	}

	hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
	if err != nil {
		h.logger.Error("failed to get hero for npc interact", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to load hero",
		})
		return
	}
	if hero == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{
			"error": "hero not found",
		})
		return
	}

	// Load NPC.
	npc, err := h.questStore.GetNPCByID(r.Context(), req.NPCID)
	if err != nil {
		h.logger.Error("failed to get npc", "npc_id", req.NPCID, "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to load npc",
		})
		return
	}
	if npc == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{
			"error": "npc not found",
		})
		return
	}

	// Load the NPC's town.
	town, err := h.questStore.GetTown(r.Context(), npc.TownID)
	if err != nil {
		h.logger.Error("failed to get town for npc", "town_id", npc.TownID, "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to load town",
		})
		return
	}
	if town == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{
			"error": "town not found",
		})
		return
	}

	// Check proximity: hero must be within the town's radius.
	d := dist2D(req.PositionX, req.PositionY, town.WorldX, town.WorldY)
	if d > town.Radius {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "hero is too far from the town",
		})
		return
	}

	// Build actions based on NPC type.
	var actions []model.NPCInteractAction

	switch npc.Type {
	case "quest_giver":
		quests, err := h.questStore.ListQuestsByNPCForHeroLevel(r.Context(), npc.ID, hero.Level)
		if err != nil {
			h.logger.Error("failed to list quests for npc interaction", "npc_id", npc.ID, "error", err)
			writeJSON(w, http.StatusInternalServerError, map[string]string{
				"error": "failed to load quests",
			})
			return
		}
		for _, q := range quests {
			actions = append(actions, model.NPCInteractAction{
				ActionType:  "quest",
				QuestID:     q.ID,
				QuestTitle:  q.Title,
				Description: q.Description,
			})
		}

	case "merchant":
		actions = append(actions, model.NPCInteractAction{
			ActionType:  "shop_item",
			ItemName:    "Healing Potion",
			ItemCost:    50,
			Description: "Restores health. Always handy in a pinch.",
		})

	case "healer":
		actions = append(actions, model.NPCInteractAction{
			ActionType:  "heal",
			ItemName:    "Full Heal",
			ItemCost:    100,
			Description: "Restore hero to full HP.",
		})
	}

	// Log the meeting.
	h.addLog(hero.ID, fmt.Sprintf("Met %s in %s", npc.Name, town.Name))

	resp := model.NPCInteractResponse{
		NPCName:  npc.Name,
		NPCType:  npc.Type,
		TownName: town.Name,
		Actions:  actions,
	}
	if resp.Actions == nil {
		resp.Actions = []model.NPCInteractAction{}
	}
	writeJSON(w, http.StatusOK, resp)
}

// NearbyNPCs handles GET /api/v1/hero/nearby-npcs.
// Returns NPCs within 3 world units of the given position.
func (h *NPCHandler) NearbyNPCs(w http.ResponseWriter, r *http.Request) {
	telegramID, ok := resolveTelegramID(r)
	if !ok {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "missing telegramId",
		})
		return
	}

	posXStr := r.URL.Query().Get("posX")
	posYStr := r.URL.Query().Get("posY")
	posX, errX := strconv.ParseFloat(posXStr, 64)
	posY, errY := strconv.ParseFloat(posYStr, 64)
	if errX != nil || errY != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "posX and posY are required numeric parameters",
		})
		return
	}

	// Verify hero exists.
	hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
	if err != nil {
		h.logger.Error("failed to get hero for nearby npcs", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to load hero",
		})
		return
	}
	if hero == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{
			"error": "hero not found",
		})
		return
	}

	// Load all towns and NPCs, then filter by distance.
	towns, err := h.questStore.ListTowns(r.Context())
	if err != nil {
		h.logger.Error("failed to list towns for nearby npcs", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to load towns",
		})
		return
	}

	const nearbyRadius = 3.0
	var result []model.NearbyNPCEntry

	for _, town := range towns {
		npcs, err := h.questStore.ListNPCsByTown(r.Context(), town.ID)
		if err != nil {
			h.logger.Warn("failed to list npcs for town", "town_id", town.ID, "error", err)
			continue
		}
		for _, npc := range npcs {
			npcWorldX := town.WorldX + npc.OffsetX
			npcWorldY := town.WorldY + npc.OffsetY
			d := dist2D(posX, posY, npcWorldX, npcWorldY)
			if d <= nearbyRadius {
				result = append(result, model.NearbyNPCEntry{
					ID:                   npc.ID,
					Name:                 npc.Name,
					Type:                 npc.Type,
					WorldX:               npcWorldX,
					WorldY:               npcWorldY,
					InteractionAvailable: true,
				})
			}
		}
	}

	if result == nil {
		result = []model.NearbyNPCEntry{}
	}
	writeJSON(w, http.StatusOK, result)
}

// NPCAlms handles POST /api/v1/hero/npc-alms.
// The hero gives alms to a wandering merchant in exchange for random equipment.
func (h *NPCHandler) NPCAlms(w http.ResponseWriter, r *http.Request) {
	telegramID, ok := resolveTelegramID(r)
	if !ok {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "missing telegramId",
		})
		return
	}

	var req struct {
		Accept bool `json:"accept"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		// Empty body used to be sent by the web client; treat as accept (mysterious item purchase).
		if errors.Is(err, io.EOF) {
			req.Accept = true
		} else {
			writeJSON(w, http.StatusBadRequest, map[string]string{
				"error": "invalid request body",
			})
			return
		}
	}

	hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
	if err != nil {
		h.logger.Error("failed to get hero for npc alms", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to load hero",
		})
		return
	}
	if hero == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{
			"error": "hero not found",
		})
		return
	}

	if !req.Accept {
		writeJSON(w, http.StatusOK, model.AlmsResponse{
			Accepted: false,
			Message:  "You declined the wandering merchant's offer.",
		})
		return
	}

	// Compute cost: 20 + level * 5.
	cost := int64(20 + hero.Level*5)
	if hero.Gold < cost {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": fmt.Sprintf("not enough gold (need %d, have %d)", cost, hero.Gold),
		})
		return
	}

	hero.Gold -= cost

	// Generate random equipment drop.
	slots := model.AllEquipmentSlots
	slot := slots[rand.Intn(len(slots))]
	family := model.PickGearFamily(slot)

	var drop *model.LootDrop

	if family != nil {
		rarity := model.RollRarity()
		ilvl := model.RollIlvl(hero.Level, false)
		item := model.NewGearItem(family, ilvl, rarity)

		if h.gearStore != nil {
			ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
			err := h.gearStore.CreateItem(ctx, item)
			cancel()
			if err != nil {
				h.logger.Warn("failed to create alms gear item", "slot", slot, "error", err)
			} else {
				drop = &model.LootDrop{
					ItemType: string(slot),
					ItemID:   item.ID,
					ItemName: item.Name,
					Rarity:   rarity,
				}
				h.addLog(hero.ID, fmt.Sprintf("Gave alms to wandering merchant, received %s", item.Name))
			}
		}
	}

	if drop == nil {
		// Fallback: gold refund if we couldn't generate equipment.
		hero.Gold += cost
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to generate reward",
		})
		return
	}

	if err := h.heroStore.Save(r.Context(), hero); err != nil {
		h.logger.Error("failed to save hero after alms", "hero_id", hero.ID, "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to save hero",
		})
		return
	}

	writeJSON(w, http.StatusOK, model.AlmsResponse{
		Accepted:  true,
		GoldSpent: cost,
		ItemDrop:  drop,
		Hero:      hero,
		Message:   fmt.Sprintf("You gave %d gold to the wandering merchant and received %s.", cost, drop.ItemName),
	})
}

// HealHero handles POST /api/v1/hero/npc-heal.
// A healer NPC restores the hero to full HP for 100 gold.
func (h *NPCHandler) HealHero(w http.ResponseWriter, r *http.Request) {
	telegramID, ok := resolveTelegramID(r)
	if !ok {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "missing telegramId",
		})
		return
	}

	var req struct {
		NPCID int64 `json:"npcId"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "invalid request body",
		})
		return
	}

	hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
	if err != nil {
		h.logger.Error("failed to get hero for heal", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to load hero",
		})
		return
	}
	if hero == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{
			"error": "hero not found",
		})
		return
	}

	// Verify NPC is a healer.
	if req.NPCID != 0 {
		npc, err := h.questStore.GetNPCByID(r.Context(), req.NPCID)
		if err != nil {
			h.logger.Error("failed to get npc for heal", "npc_id", req.NPCID, "error", err)
			writeJSON(w, http.StatusInternalServerError, map[string]string{
				"error": "failed to load npc",
			})
			return
		}
		if npc == nil || npc.Type != "healer" {
			writeJSON(w, http.StatusBadRequest, map[string]string{
				"error": "npc is not a healer",
			})
			return
		}
	}

	const healCost int64 = 100
	if hero.Gold < healCost {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": fmt.Sprintf("not enough gold (need %d, have %d)", healCost, hero.Gold),
		})
		return
	}

	hero.Gold -= healCost
	hero.HP = hero.MaxHP

	if err := h.heroStore.Save(r.Context(), hero); err != nil {
		h.logger.Error("failed to save hero after heal", "hero_id", hero.ID, "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to save hero",
		})
		return
	}

	h.addLog(hero.ID, "Healed to full HP by a town healer")
	writeJSON(w, http.StatusOK, map[string]any{
		"hero":    hero,
		"healed":  true,
		"message": "You have been healed to full HP.",
	})
}

// BuyPotion handles POST /api/v1/hero/npc-buy-potion.
// A merchant NPC sells a healing potion for 50 gold.
func (h *NPCHandler) BuyPotion(w http.ResponseWriter, r *http.Request) {
	telegramID, ok := resolveTelegramID(r)
	if !ok {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "missing telegramId",
		})
		return
	}

	hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
	if err != nil {
		h.logger.Error("failed to get hero for buy potion", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to load hero",
		})
		return
	}
	if hero == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{
			"error": "hero not found",
		})
		return
	}

	const potionCost int64 = 50
	if hero.Gold < potionCost {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": fmt.Sprintf("not enough gold (need %d, have %d)", potionCost, hero.Gold),
		})
		return
	}

	hero.Gold -= potionCost
	hero.Potions++

	if err := h.heroStore.Save(r.Context(), hero); err != nil {
		h.logger.Error("failed to save hero after buy potion", "hero_id", hero.ID, "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "failed to save hero",
		})
		return
	}

	h.addLog(hero.ID, "Purchased a Healing Potion from a merchant")
	writeJSON(w, http.StatusOK, map[string]any{
		"hero":    hero,
		"message": "You purchased a Healing Potion for 50 gold.",
	})
}
