package game

import (
	"errors"
	"math"
	"math/rand"
	"strconv"
	"time"

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

// TownTourOfflineAtNPC resolves a town NPC visit without UI (offline catch-up).
type TownTourOfflineAtNPC func(heroID int64, hm *HeroMovement, graph *RoadGraph, npc TownNPC, now time.Time, adventureLog AdventureLogWriter)

func scheduleTownTourWanderRetarget(hm *HeroMovement, now time.Time) {
	cfg := tuning.Get()
	minMs := cfg.TownTourWanderRetargetMinMs
	maxMs := cfg.TownTourWanderRetargetMaxMs
	if minMs <= 0 {
		minMs = tuning.DefaultValues().TownTourWanderRetargetMinMs
	}
	if maxMs <= 0 {
		maxMs = tuning.DefaultValues().TownTourWanderRetargetMaxMs
	}
	hm.Excursion.WanderNextAt = now.Add(randomDurationBetweenMs(minMs, maxMs))
}

// beginTownTourExcursion starts attractor-based wandering in the current town (StateInTown).
func beginTownTourExcursion(hm *HeroMovement, now time.Time, graph *RoadGraph) {
	if hm == nil || graph == nil {
		return
	}
	clearLegacyTownNPCState(hm)
	dur := randomRestDuration()
	hm.Excursion = model.ExcursionSession{
		Kind:           model.ExcursionKindTown,
		Phase:          model.ExcursionWild,
		StartedAt:      now,
		TownTourPhase:  string(model.TownTourPhaseWander),
		TownTourEndsAt: now.Add(dur),
	}
	scheduleTownTourWanderRetarget(hm, now)
	pickTownTourWanderAttractor(hm, graph, now)
}

func clearLegacyTownNPCState(hm *HeroMovement) {
	if hm == nil {
		return
	}
	hm.TownNPCQueue = nil
	hm.NextTownNPCRollAt = time.Time{}
	hm.TownLastNPCLingerUntil = time.Time{}
	hm.TownNPCWalkTargetID = 0
	hm.TownNPCWalkToX = 0
	hm.TownNPCWalkToY = 0
	hm.TownCenterWalkActive = false
	hm.TownCenterWalkToX = 0
	hm.TownCenterWalkToY = 0
	hm.TownPlazaHealActive = false
	hm.TownLeaveAt = time.Time{}
	hm.TownVisitNPCName = ""
	hm.TownVisitNPCKey = ""
	hm.TownVisitNPCType = ""
	hm.TownVisitStartedAt = time.Time{}
	hm.TownVisitLogsEmitted = 0
	hm.TownNPCUILock = false
}

func clearTownVisitLogFields(hm *HeroMovement) {
	if hm == nil {
		return
	}
	hm.TownVisitNPCName = ""
	hm.TownVisitNPCKey = ""
	hm.TownVisitNPCType = ""
	hm.TownVisitStartedAt = time.Time{}
	hm.TownVisitLogsEmitted = 0
}

func transitionTownTourToWander(hm *HeroMovement, graph *RoadGraph, now time.Time) {
	ex := &hm.Excursion
	ex.TownTourPhase = string(model.TownTourPhaseWander)
	ex.TownTourNpcID = 0
	ex.TownTourStandX = 0
	ex.TownTourStandY = 0
	ex.TownWelcomeUntil = time.Time{}
	ex.TownServiceUntil = time.Time{}
	ex.TownTourDialogOpen = false
	ex.TownTourInteractionOpen = false
	clearTownVisitLogFields(hm)
	scheduleTownTourWanderRetarget(hm, now)
	pickTownTourWanderAttractor(hm, graph, now)
}

// pickTownTourWanderAttractor chooses the next wander target: random point in town or stand near an NPC.
func pickTownTourWanderAttractor(hm *HeroMovement, graph *RoadGraph, now time.Time) {
	if hm == nil || graph == nil {
		return
	}
	ex := &hm.Excursion
	if ex.TownExitPending {
		return
	}
	town := graph.Towns[hm.CurrentTownID]
	if town == nil {
		return
	}
	cfg := tuning.Get()
	npcs := graph.TownNPCs[hm.CurrentTownID]
	pNpc := cfg.TownTourNpcAttractorChance
	if pNpc <= 0 {
		pNpc = tuning.DefaultValues().TownTourNpcAttractorChance
	}
	if pNpc > 1 {
		pNpc = 1
	}

	if len(npcs) > 0 && rand.Float64() < pNpc {
		npc := npcs[rand.Intn(len(npcs))]
		npcWX, npcWY, posOk := graph.NPCWorldPos(npc.ID, hm.CurrentTownID)
		if !posOk {
			npcWX = town.WorldX + npc.OffsetX
			npcWY = town.WorldY + npc.OffsetY
		}
		standoff := cfg.TownNPCStandoffWorld
		if standoff <= 0 {
			standoff = tuning.DefaultValues().TownNPCStandoffWorld
		}
		toX, toY := townNPCStandPoint(npcWX, npcWY, hm.CurrentX, hm.CurrentY, standoff)
		ex.TownTourPhase = string(model.TownTourPhaseNpcApproach)
		ex.TownTourNpcID = npc.ID
		ex.TownTourStandX = toX
		ex.TownTourStandY = toY
		ex.AttractorSet = false
		return
	}

	// Random point inside town circle (keep margin from edge).
	cx, cy := town.WorldX, town.WorldY
	radius := town.Radius
	if radius < 1 {
		radius = 8
	}
	margin := radius * 0.12
	maxR := radius - margin
	if maxR < margin {
		maxR = radius * 0.5
	}
	for attempt := 0; attempt < 24; attempt++ {
		theta := rand.Float64() * 2 * math.Pi
		rd := margin + rand.Float64()*math.Max(0.01, maxR-margin)
		px := cx + math.Cos(theta)*rd
		py := cy + math.Sin(theta)*rd
		if graph.HeroInTownAt(px, py) {
			ex.AttractorX = px
			ex.AttractorY = py
			ex.AttractorSet = true
			ex.TownTourPhase = string(model.TownTourPhaseWander)
			ex.TownTourNpcID = 0
			return
		}
	}
	ex.AttractorX = cx
	ex.AttractorY = cy
	ex.AttractorSet = true
	ex.TownTourPhase = string(model.TownTourPhaseWander)
	ex.TownTourNpcID = 0
}

// AdminTownTourApproachNPC forces npc_approach toward npcID in the hero's current town (admin only).
func (hm *HeroMovement) AdminTownTourApproachNPC(graph *RoadGraph, npcID int64, now time.Time) error {
	if hm == nil || graph == nil {
		return errors.New("nil movement or graph")
	}
	if hm.Excursion.Kind != model.ExcursionKindTown {
		return errors.New("hero is not on town tour excursion")
	}
	if hm.State != model.StateInTown {
		return errors.New("hero must be in town")
	}
	npc, ok := graph.NPCByID[npcID]
	if !ok {
		return errors.New("npc not found in world graph")
	}
	found := false
	for _, n := range graph.TownNPCs[hm.CurrentTownID] {
		if n.ID == npcID {
			found = true
			break
		}
	}
	if !found {
		return errors.New("npc is not in hero's current town")
	}
	town := graph.Towns[hm.CurrentTownID]
	if town == nil {
		return errors.New("town not found")
	}
	cfg := tuning.Get()
	npcWX, npcWY, posOk := graph.NPCWorldPos(npc.ID, hm.CurrentTownID)
	if !posOk {
		npcWX = town.WorldX + npc.OffsetX
		npcWY = town.WorldY + npc.OffsetY
	}
	standoff := cfg.TownNPCStandoffWorld
	if standoff <= 0 {
		standoff = tuning.DefaultValues().TownNPCStandoffWorld
	}
	ex := &hm.Excursion
	toX, toY := townNPCStandPoint(npcWX, npcWY, hm.CurrentX, hm.CurrentY, standoff)
	ex.TownTourPhase = string(model.TownTourPhaseNpcApproach)
	ex.TownTourNpcID = npc.ID
	ex.TownTourStandX = toX
	ex.TownTourStandY = toY
	ex.AttractorSet = false
	ex.TownWelcomeUntil = time.Time{}
	ex.TownServiceUntil = time.Time{}
	ex.TownTourDialogOpen = false
	ex.TownTourInteractionOpen = false
	hm.TownNPCUILock = false
	hm.sentTownTourWireSig = ""
	return nil
}

// NotifyTownTourClients pushes town_tour_phase, hero_state, and hero_move after an out-of-tick town tour change.
func NotifyTownTourClients(sender MessageSender, heroID int64, hm *HeroMovement, graph *RoadGraph, now time.Time) {
	if sender == nil || hm == nil || graph == nil || hm.Excursion.Kind != model.ExcursionKindTown {
		return
	}
	hm.sentTownTourWireSig = ""
	sendTownTourUpdate(sender, heroID, hm, graph)
	h := hm.Hero
	if h != nil {
		h.EnsureGearMap()
		h.RefreshDerivedCombatStats(now)
		sender.SendToHero(heroID, "hero_state", h)
	}
	sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
}

func townTourWireSig(hm *HeroMovement) string {
	if hm == nil {
		return ""
	}
	ex := hm.Excursion
	return ex.TownTourPhase + ":" + strconv.FormatInt(ex.TownTourNpcID, 10) + ":" + strconv.FormatBool(ex.TownExitPending)
}

func sendTownTourUpdate(sender MessageSender, heroID int64, hm *HeroMovement, graph *RoadGraph) {
	if sender == nil || hm == nil || graph == nil {
		return
	}
	ex := hm.Excursion
	town := graph.Towns[hm.CurrentTownID]
	var townKey string
	if town != nil {
		townKey = town.NameKey
	}
	var npcID int64
	var name, nameKey, npcType string
	var wx, wy float64
	if ex.TownTourNpcID != 0 {
		if npc, ok := graph.NPCByID[ex.TownTourNpcID]; ok {
			npcID = npc.ID
			name = npc.Name
			nameKey = npc.NameKey
			npcType = npc.Type
			if x, y, ok2 := graph.NPCWorldPos(npc.ID, hm.CurrentTownID); ok2 {
				wx, wy = x, y
			} else if town != nil {
				wx = town.WorldX + npc.OffsetX
				wy = town.WorldY + npc.OffsetY
			}
		}
	}
	payload := model.TownTourPhasePayload{
		Phase:       ex.TownTourPhase,
		TownID:      hm.CurrentTownID,
		TownNameKey: townKey,
		NpcID:       npcID,
		NpcName:     name,
		NpcNameKey:  nameKey,
		NpcType:     npcType,
		WorldX:      wx,
		WorldY:      wy,
		ExitPending: ex.TownExitPending,
	}
	sender.SendToHero(heroID, "town_tour_phase", payload)
}

func processTownTourMovement(
	heroID int64,
	hm *HeroMovement,
	graph *RoadGraph,
	now time.Time,
	sender MessageSender,
	adventureLog AdventureLogWriter,
	townTourOffline TownTourOfflineAtNPC,
) {
	if hm == nil || graph == nil {
		return
	}
	ex := &hm.Excursion
	cfg := tuning.Get()
	dt := now.Sub(hm.LastMoveTick).Seconds()
	if dt <= 0 {
		dt = movementTickRate().Seconds()
	}
	hm.LastMoveTick = now
	hm.refreshSpeed(now)

	if !now.Before(ex.TownTourEndsAt) {
		ex.TownExitPending = true
	}

	uiOpen := ex.TownTourDialogOpen || ex.TownTourInteractionOpen
	if uiOpen && dt > 0 {
		shift := time.Duration(dt * float64(time.Second))
		switch model.TownTourPhase(ex.TownTourPhase) {
		case model.TownTourPhaseNpcWelcome:
			if !ex.TownWelcomeUntil.IsZero() {
				ex.TownWelcomeUntil = ex.TownWelcomeUntil.Add(shift)
			}
		case model.TownTourPhaseNpcService:
			if !ex.TownServiceUntil.IsZero() {
				ex.TownServiceUntil = ex.TownServiceUntil.Add(shift)
			}
		case model.TownTourPhaseRest:
			if !ex.TownRestUntil.IsZero() {
				ex.TownRestUntil = ex.TownRestUntil.Add(shift)
			}
			ex.TownTourEndsAt = ex.TownTourEndsAt.Add(shift)
		}
	}
	hm.TownNPCUILock = uiOpen

	walkSpeed := cfg.TownNPCWalkSpeed
	if walkSpeed <= 0 {
		walkSpeed = tuning.DefaultValues().TownNPCWalkSpeed
	}

	switch model.TownTourPhase(ex.TownTourPhase) {
	case model.TownTourPhaseWander:
		if !ex.AttractorSet {
			// Defensive: pick a wander target.
			pickTownTourWanderAttractor(hm, graph, now)
		}
		arrived := hm.stepTowardAttractor(now, dt)
		if !arrived {
			break
		}
		// At wander attractor.
		if ex.TownExitPending {
			hm.LeaveTown(graph, now)
			hm.Excursion = model.ExcursionSession{}
			hm.sentTownTourWireSig = ""
			if sender != nil {
				sender.SendToHero(heroID, "town_exit", model.TownExitPayload{})
				if route := hm.RoutePayload(); route != nil {
					sender.SendToHero(heroID, "route_assigned", route)
				}
			}
			return
		}
		if now.Before(ex.WanderNextAt) {
			break
		}
		hpFrac := 1.0
		if hm.Hero != nil && hm.Hero.MaxHP > 0 {
			hpFrac = float64(hm.Hero.HP) / float64(hm.Hero.MaxHP)
		}
		th := cfg.TownRestHpThreshold
		if th <= 0 {
			th = tuning.DefaultValues().TownRestHpThreshold
		}
		rch := cfg.TownRestChance
		if rch <= 0 {
			rch = tuning.DefaultValues().TownRestChance
		}
		if rch > 1 {
			rch = 1
		}
		if hpFrac < th && rand.Float64() < rch && !ex.TownExitPending {
			minR := cfg.TownTourRestMinMs
			maxR := cfg.TownTourRestMaxMs
			if minR <= 0 {
				minR = tuning.DefaultValues().TownTourRestMinMs
			}
			if maxR <= 0 {
				maxR = tuning.DefaultValues().TownTourRestMaxMs
			}
			ex.TownTourPhase = string(model.TownTourPhaseRest)
			ex.TownRestUntil = now.Add(randomDurationBetweenMs(minR, maxR))
			ex.AttractorSet = false
			break
		}
		scheduleTownTourWanderRetarget(hm, now)
		pickTownTourWanderAttractor(hm, graph, now)

	case model.TownTourPhaseNpcApproach:
		arrived := hm.stepTowardWorldPoint(dt, ex.TownTourStandX, ex.TownTourStandY, walkSpeed)
		if !arrived {
			break
		}
		npc, ok := graph.NPCByID[ex.TownTourNpcID]
		if !ok {
			transitionTownTourToWander(hm, graph, now)
			break
		}
		if sender != nil {
			// Online: welcome + dialog.
			ex.TownTourPhase = string(model.TownTourPhaseNpcWelcome)
			welcomeMs := cfg.TownWelcomeDurationMs
			if welcomeMs <= 0 {
				welcomeMs = tuning.DefaultValues().TownWelcomeDurationMs
			}
			ex.TownWelcomeUntil = now.Add(time.Duration(welcomeMs) * time.Millisecond)
			hm.TownVisitNPCName = npc.Name
			hm.TownVisitNPCKey = npc.NameKey
			hm.TownVisitNPCType = npc.Type
			hm.TownVisitStartedAt = now
			hm.TownVisitLogsEmitted = 0
			townNameKey := ""
			if tt := graph.Towns[hm.CurrentTownID]; tt != nil {
				townNameKey = tt.NameKey
			}
			sender.SendToHero(heroID, "town_npc_visit", model.TownNPCVisitPayload{
				NPCID: npc.ID, Name: npc.Name, NameKey: npc.NameKey, Type: npc.Type, TownID: hm.CurrentTownID,
				TownNameKey: townNameKey,
				WorldX:      ex.TownTourStandX, WorldY: ex.TownTourStandY,
			})
			legacyMerchantSell := model.IsGearVendorType(npc.Type)
			if legacyMerchantSell {
				share := cfg.MerchantTownAutoSellShare
				if share <= 0 || share > 1 {
					share = tuning.DefaultValues().MerchantTownAutoSellShare
				}
				soldItems, soldGold := AutoSellRandomInventoryShare(hm.Hero, share, nil)
				if soldItems > 0 && adventureLog != nil {
					adventureLog(heroID, model.AdventureLogLine{
						Event: &model.AdventureLogEvent{
							Code: model.LogPhraseSoldItemsMerchant,
							Args: map[string]any{"count": soldItems, "npcKey": npc.NameKey, "gold": soldGold},
						},
					})
				}
			}
			emitTownNPCVisitLogs(heroID, hm, now, adventureLog)
		} else {
			if townTourOffline != nil {
				townTourOffline(heroID, hm, graph, npc, now, adventureLog)
			}
			transitionTownTourToWander(hm, graph, now)
		}

	case model.TownTourPhaseNpcWelcome:
		emitTownNPCVisitLogs(heroID, hm, now, adventureLog)
		if !ex.TownWelcomeUntil.IsZero() && !now.Before(ex.TownWelcomeUntil) && !ex.TownTourDialogOpen {
			transitionTownTourToWander(hm, graph, now)
			break
		}

	case model.TownTourPhaseNpcService:
		emitTownNPCVisitLogs(heroID, hm, now, adventureLog)
		svcMs := cfg.TownServiceMaxMs
		if svcMs <= 0 {
			svcMs = tuning.DefaultValues().TownServiceMaxMs
		}
		if !ex.TownServiceUntil.IsZero() && !now.Before(ex.TownServiceUntil) {
			if sender != nil {
				sender.SendToHero(heroID, "town_tour_service_end", model.TownTourServiceEndPayload{Reason: "timeout"})
			}
			transitionTownTourToWander(hm, graph, now)
			break
		}

	case model.TownTourPhaseRest:
		hm.applyTownRestHeal(dt)
		if !ex.TownRestUntil.IsZero() && now.After(ex.TownRestUntil) {
			ex.TownTourPhase = string(model.TownTourPhaseWander)
			ex.TownRestUntil = time.Time{}
			scheduleTownTourWanderRetarget(hm, now)
			pickTownTourWanderAttractor(hm, graph, now)
		}
	}

	sig := townTourWireSig(hm)
	if sender != nil && hm.Excursion.Kind == model.ExcursionKindTown {
		if sig != hm.sentTownTourWireSig {
			hm.sentTownTourWireSig = sig
			sendTownTourUpdate(sender, heroID, hm, graph)
		}
		sender.SendToHero(heroID, "hero_move", hm.MovePayload(now))
	}
	hm.SyncToHero()
}

// Town tour client command handlers (engine calls under lock).
func (hm *HeroMovement) townTourNPCDialogClosed(now time.Time, graph *RoadGraph) {
	if hm == nil || graph == nil {
		return
	}
	if hm.Excursion.Kind != model.ExcursionKindTown {
		return
	}
	ex := &hm.Excursion
	ex.TownTourDialogOpen = false
	hm.TownNPCUILock = ex.TownTourDialogOpen || ex.TownTourInteractionOpen
	switch model.TownTourPhase(ex.TownTourPhase) {
	case model.TownTourPhaseNpcWelcome:
		transitionTownTourToWander(hm, graph, now)
	case model.TownTourPhaseNpcService:
		if !ex.TownTourInteractionOpen {
			transitionTownTourToWander(hm, graph, now)
		}
	}
}

func (hm *HeroMovement) townTourNPCInteractionOpened(now time.Time, graph *RoadGraph) {
	if hm == nil || graph == nil {
		return
	}
	if hm.Excursion.Kind != model.ExcursionKindTown {
		return
	}
	ex := &hm.Excursion
	ex.TownTourInteractionOpen = true
	hm.TownNPCUILock = ex.TownTourDialogOpen || ex.TownTourInteractionOpen
	if model.TownTourPhase(ex.TownTourPhase) == model.TownTourPhaseNpcWelcome {
		ex.TownTourPhase = string(model.TownTourPhaseNpcService)
		svcMs := tuning.Get().TownServiceMaxMs
		if svcMs <= 0 {
			svcMs = tuning.DefaultValues().TownServiceMaxMs
		}
		ex.TownServiceUntil = now.Add(time.Duration(svcMs) * time.Millisecond)
	}
}

func (hm *HeroMovement) townTourNPCInteractionClosed(now time.Time, graph *RoadGraph) {
	if hm == nil || graph == nil {
		return
	}
	if hm.Excursion.Kind != model.ExcursionKindTown {
		return
	}
	ex := &hm.Excursion
	ex.TownTourInteractionOpen = false
	hm.TownNPCUILock = ex.TownTourDialogOpen || ex.TownTourInteractionOpen
	if model.TownTourPhase(ex.TownTourPhase) == model.TownTourPhaseNpcService {
		transitionTownTourToWander(hm, graph, now)
	}
}

func (hm *HeroMovement) townTourSetDialogOpen(open bool) {
	if hm == nil || hm.Excursion.Kind != model.ExcursionKindTown {
		return
	}
	hm.Excursion.TownTourDialogOpen = open
	hm.TownNPCUILock = hm.Excursion.TownTourDialogOpen || hm.Excursion.TownTourInteractionOpen
}
