package game

import (
	"encoding/json"
	"math"
	"math/rand"
	"os"
	"path/filepath"
	"time"

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

// #region agent log
func agentDebugLog(hypothesisID, location, message string, data map[string]any) {
	wd, err := os.Getwd()
	if err != nil {
		return
	}
	logPath := filepath.Join(wd, "debug-cbb64d.log")
	if filepath.Base(wd) == "backend" {
		logPath = filepath.Join(wd, "..", "debug-cbb64d.log")
	}
	payload := map[string]any{
		"sessionId":    "cbb64d",
		"hypothesisId": hypothesisID,
		"location":     location,
		"message":      message,
		"data":         data,
		"timestamp":    time.Now().UnixMilli(),
	}
	b, err := json.Marshal(payload)
	if err != nil {
		return
	}
	f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	_, _ = f.Write(append(b, '\n'))
	_ = f.Close()
}

// #endregion

const (
	// BaseMoveSpeed is the hero's base movement speed in world-units per second.
	BaseMoveSpeed = 2.0

	// MovementTickRate is how often the movement system updates (2 Hz).
	MovementTickRate = 500 * time.Millisecond

	// PositionSyncRate is how often the server sends a full position_sync (drift correction).
	PositionSyncRate = 10 * time.Second

	// EncounterCooldownBase is the minimum gap between random encounters.
	EncounterCooldownBase = 15 * time.Second

	// EncounterChancePerTick is the probability of an encounter on each movement tick.
	EncounterChancePerTick = 0.04

	// TownRestMin is the minimum rest duration when arriving at a town.
	TownRestMin = 5 * 60 * time.Second

	// TownRestMax is the maximum rest duration when arriving at a town.
	TownRestMax = 20 * 60 * time.Second

	// TownArrivalRadius is how close the hero must be to the final waypoint
	// to be considered "arrived" at the town.
	TownArrivalRadius = 0.5

	// Town NPC visits: high chance each attempt to approach the next NPC; queue clears on LeaveTown.
	townNPCVisitChance    = 0.78
	townNPCRollMin        = 800 * time.Millisecond
	townNPCRollMax        = 2600 * time.Millisecond
	townNPCRetryAfterMiss = 450 * time.Millisecond
)

// HeroMovement holds the live movement state for a single online hero.
type HeroMovement struct {
	HeroID            int64
	Hero              *model.Hero // live reference, owned by the engine
	CurrentX          float64
	CurrentY          float64
	Speed             float64 // effective world-units/sec
	State             model.GameState
	DestinationTownID int64
	CurrentTownID     int64
	Road              *Road
	WaypointIndex     int     // index of the waypoint we are heading toward
	WaypointFraction  float64 // 0..1 within the current segment
	LastEncounterAt   time.Time
	RestUntil         time.Time
	LastMoveTick      time.Time
	Direction         int // +1 forward along TownOrder, -1 backward

	// TownNPCQueue: NPC ids still to visit this stay (nil = not on NPC tour). Cleared in LeaveTown.
	TownNPCQueue      []int64
	NextTownNPCRollAt time.Time
}

// NewHeroMovement creates a HeroMovement for a hero that just connected.
// It initializes position, state, and picks the first destination if needed.
func NewHeroMovement(hero *model.Hero, graph *RoadGraph, now time.Time) *HeroMovement {
	// Randomize direction per hero so they don't all walk the same way.
	dir := 1
	if hero.ID%2 == 0 {
		dir = -1
	}

	// Add per-hero position offset so heroes on the same road don't overlap.
	// Use hero ID to create a stable lateral offset of ±1.5 tiles.
	lateralOffset := (float64(hero.ID%7) - 3.0) * 0.5

	hm := &HeroMovement{
		HeroID:        hero.ID,
		Hero:          hero,
		CurrentX:      hero.PositionX + lateralOffset*0.3,
		CurrentY:      hero.PositionY + lateralOffset*0.7,
		State:         hero.State,
		LastMoveTick:  now,
		Direction:     dir,
	}

	// Restore persisted movement state.
	if hero.CurrentTownID != nil {
		hm.CurrentTownID = *hero.CurrentTownID
	}
	if hero.DestinationTownID != nil {
		hm.DestinationTownID = *hero.DestinationTownID
	}

	hm.refreshSpeed(now)

	// If the hero is dead, keep them dead.
	if hero.State == model.StateDead || hero.HP <= 0 {
		hm.State = model.StateDead
		return hm
	}

	// If fighting, leave as-is (engine combat system manages it).
	if hero.State == model.StateFighting {
		return hm
	}

	// If resting/in_town, set a short rest timer so they leave soon.
	if hero.State == model.StateResting || hero.State == model.StateInTown {
		hm.State = model.StateResting
		hm.RestUntil = now.Add(randomRestDuration())
		return hm
	}

	// Walking state: assign a road if we don't have a destination.
	if hm.DestinationTownID == 0 {
		hm.pickDestination(graph)
	}
	hm.assignRoad(graph)
	hm.State = model.StateWalking

	return hm
}

// pickDestination selects the next town the hero should walk toward.
func (hm *HeroMovement) pickDestination(graph *RoadGraph) {
	if hm.CurrentTownID == 0 {
		// Hero is not associated with any town yet, pick nearest.
		hm.CurrentTownID = graph.NearestTown(hm.CurrentX, hm.CurrentY)
	}

	idx := graph.TownOrderIndex(hm.CurrentTownID)
	if idx < 0 {
		// Fallback.
		if len(graph.TownOrder) > 0 {
			hm.DestinationTownID = graph.TownOrder[0]
		}
		return
	}

	n := len(graph.TownOrder)
	if n <= 1 {
		hm.DestinationTownID = hm.CurrentTownID
		return
	}

	nextIdx := idx + hm.Direction
	if nextIdx >= n {
		nextIdx = 0
	}
	if nextIdx < 0 {
		nextIdx = n - 1
	}

	hm.DestinationTownID = graph.TownOrder[nextIdx]
}

// assignRoad finds and configures the road from CurrentTownID to DestinationTownID.
// If no road exists (hero is mid-road), it finds the nearest town and routes from there.
func (hm *HeroMovement) assignRoad(graph *RoadGraph) {
	road := graph.FindRoad(hm.CurrentTownID, hm.DestinationTownID)
	if road == nil {
		// Try finding a road from any nearby town.
		nearest := graph.NearestTown(hm.CurrentX, hm.CurrentY)
		hm.CurrentTownID = nearest
		road = graph.FindRoad(nearest, hm.DestinationTownID)
	}
	if road == nil {
		// #region agent log
		agentDebugLog("H5", "movement.go:assignRoad", "no road after nearest retry", map[string]any{
			"currentTownID": hm.CurrentTownID, "destinationTownID": hm.DestinationTownID,
			"x": hm.CurrentX, "y": hm.CurrentY,
		})
		// #endregion
		// No road available, will retry next tick.
		return
	}

	// Create a per-hero jittered copy of waypoints so heroes don't overlap on the same road.
	jitteredWaypoints := make([]Point, len(road.Waypoints))
	copy(jitteredWaypoints, road.Waypoints)
	heroSeed := float64(hm.HeroID)
	lateralJitter := (math.Sin(heroSeed*1.7) * 1.5) // ±1.5 tiles lateral offset
	for i := 1; i < len(jitteredWaypoints)-1; i++ {
		// Apply perpendicular offset (don't jitter start/end = town centers)
		dx := jitteredWaypoints[i].X - jitteredWaypoints[max(0, i-1)].X
		dy := jitteredWaypoints[i].Y - jitteredWaypoints[max(0, i-1)].Y
		segLen := math.Hypot(dx, dy)
		if segLen > 0.1 {
			perpX := -dy / segLen
			perpY := dx / segLen
			jitter := lateralJitter * (0.7 + 0.3*math.Sin(heroSeed*0.3+float64(i)*0.5))
			jitteredWaypoints[i].X += perpX * jitter
			jitteredWaypoints[i].Y += perpY * jitter
		}
	}

	jitteredRoad := &Road{
		ID:         road.ID,
		FromTownID: road.FromTownID,
		ToTownID:   road.ToTownID,
		Distance:   road.Distance,
		Waypoints:  jitteredWaypoints,
	}

	hm.Road = jitteredRoad
	hm.WaypointIndex = 0
	hm.WaypointFraction = 0

	// Position the hero at the start of the road if they're very close to the origin town.
	if len(jitteredWaypoints) > 0 {
		start := jitteredWaypoints[0]
		dist := math.Hypot(hm.CurrentX-start.X, hm.CurrentY-start.Y)
		if dist < 5.0 {
			hm.CurrentX = start.X
			hm.CurrentY = start.Y
		}
	}
}

// refreshSpeed recalculates the effective movement speed using hero buffs/debuffs.
func (hm *HeroMovement) refreshSpeed(now time.Time) {
	// Per-hero speed variation: ±10% based on hero ID for natural spread.
	heroSpeedJitter := 0.90 + float64(hm.HeroID%21)*0.01 // 0.90 to 1.10
	hm.Speed = BaseMoveSpeed * hm.Hero.MovementSpeedMultiplier(now) * heroSpeedJitter
}

// AdvanceTick moves the hero along the road for one movement tick.
// Returns true if the hero reached the destination town this tick.
func (hm *HeroMovement) AdvanceTick(now time.Time, graph *RoadGraph) (reachedTown bool) {
	if hm.Road == nil || len(hm.Road.Waypoints) < 2 {
		return false
	}

	dt := now.Sub(hm.LastMoveTick).Seconds()
	if dt <= 0 {
		dt = MovementTickRate.Seconds()
	}
	hm.LastMoveTick = now

	hm.refreshSpeed(now)
	distThisTick := hm.Speed * dt

	for distThisTick > 0 && hm.WaypointIndex < len(hm.Road.Waypoints)-1 {
		from := hm.Road.Waypoints[hm.WaypointIndex]
		to := hm.Road.Waypoints[hm.WaypointIndex+1]
		segLen := math.Hypot(to.X-from.X, to.Y-from.Y)
		if segLen < 0.001 {
			hm.WaypointIndex++
			hm.WaypointFraction = 0
			continue
		}

		// How far along this segment we already are.
		currentDist := hm.WaypointFraction * segLen
		remaining := segLen - currentDist

		if distThisTick >= remaining {
			// Move to next waypoint.
			distThisTick -= remaining
			hm.WaypointIndex++
			hm.WaypointFraction = 0
			if hm.WaypointIndex >= len(hm.Road.Waypoints)-1 {
				// Reached final waypoint = destination town.
				last := hm.Road.Waypoints[len(hm.Road.Waypoints)-1]
				hm.CurrentX = last.X
				hm.CurrentY = last.Y
				return true
			}
		} else {
			// Partial advance within this segment.
			hm.WaypointFraction = (currentDist + distThisTick) / segLen
			hm.CurrentX = from.X + (to.X-from.X)*hm.WaypointFraction
			hm.CurrentY = from.Y + (to.Y-from.Y)*hm.WaypointFraction
			distThisTick = 0
		}
	}

	// Update position to the current waypoint position.
	if hm.WaypointIndex < len(hm.Road.Waypoints) {
		wp := hm.Road.Waypoints[hm.WaypointIndex]
		if hm.WaypointFraction == 0 {
			hm.CurrentX = wp.X
			hm.CurrentY = wp.Y
		}
	}

	return false
}

// Heading returns the angle (radians) the hero is currently facing.
func (hm *HeroMovement) Heading() float64 {
	if hm.Road == nil || hm.WaypointIndex >= len(hm.Road.Waypoints)-1 {
		return 0
	}
	to := hm.Road.Waypoints[hm.WaypointIndex+1]
	return math.Atan2(to.Y-hm.CurrentY, to.X-hm.CurrentX)
}

// TargetPoint returns the next waypoint the hero is heading toward.
func (hm *HeroMovement) TargetPoint() (float64, float64) {
	if hm.Road == nil || hm.WaypointIndex >= len(hm.Road.Waypoints)-1 {
		return hm.CurrentX, hm.CurrentY
	}
	wp := hm.Road.Waypoints[hm.WaypointIndex+1]
	return wp.X, wp.Y
}

// ShouldEncounter rolls for a random encounter, respecting the cooldown.
func (hm *HeroMovement) ShouldEncounter(now time.Time) bool {
	if now.Sub(hm.LastEncounterAt) < EncounterCooldownBase {
		return false
	}
	return rand.Float64() < EncounterChancePerTick
}

// EnterTown transitions the hero into the destination town: NPC tour (StateInTown) when there
// are NPCs, otherwise a short resting state (StateResting).
func (hm *HeroMovement) EnterTown(now time.Time, graph *RoadGraph) {
	destID := hm.DestinationTownID
	hm.CurrentTownID = destID
	hm.DestinationTownID = 0
	hm.Road = nil
	hm.TownNPCQueue = nil
	hm.NextTownNPCRollAt = time.Time{}

	ids := graph.TownNPCIDs(destID)
	if len(ids) == 0 {
		hm.State = model.StateResting
		hm.Hero.State = model.StateResting
		hm.RestUntil = now.Add(randomRestDuration())
		return
	}

	q := make([]int64, len(ids))
	copy(q, ids)
	rand.Shuffle(len(q), func(i, j int) { q[i], q[j] = q[j], q[i] })
	hm.TownNPCQueue = q
	hm.State = model.StateInTown
	hm.Hero.State = model.StateInTown
	hm.NextTownNPCRollAt = now.Add(randomTownNPCDelay())
}

// LeaveTown transitions the hero from town to walking, picking a new destination.
func (hm *HeroMovement) LeaveTown(graph *RoadGraph, now time.Time) {
	hm.TownNPCQueue = nil
	hm.NextTownNPCRollAt = time.Time{}
	hm.State = model.StateWalking
	hm.Hero.State = model.StateWalking
	hm.pickDestination(graph)
	hm.assignRoad(graph)
	hm.refreshSpeed(now)
}

func randomTownNPCDelay() time.Duration {
	rangeMs := (townNPCRollMax - townNPCRollMin).Milliseconds()
	return townNPCRollMin + time.Duration(rand.Int63n(rangeMs+1))*time.Millisecond
}

// StartFighting pauses movement for combat.
func (hm *HeroMovement) StartFighting() {
	hm.State = model.StateFighting
}

// ResumWalking resumes movement after combat.
func (hm *HeroMovement) ResumeWalking(now time.Time) {
	hm.State = model.StateWalking
	hm.LastMoveTick = now
	hm.refreshSpeed(now)
}

// Die sets the movement state to dead.
func (hm *HeroMovement) Die() {
	hm.State = model.StateDead
}

// SyncToHero writes movement state back to the hero model for persistence.
func (hm *HeroMovement) SyncToHero() {
	hm.Hero.PositionX = hm.CurrentX
	hm.Hero.PositionY = hm.CurrentY
	hm.Hero.State = hm.State
	if hm.CurrentTownID != 0 {
		id := hm.CurrentTownID
		hm.Hero.CurrentTownID = &id
	} else {
		hm.Hero.CurrentTownID = nil
	}
	if hm.DestinationTownID != 0 {
		id := hm.DestinationTownID
		hm.Hero.DestinationTownID = &id
	} else {
		hm.Hero.DestinationTownID = nil
	}
	hm.Hero.MoveState = string(hm.State)
}

// MovePayload builds the hero_move WS payload.
func (hm *HeroMovement) MovePayload() model.HeroMovePayload {
	tx, ty := hm.TargetPoint()
	return model.HeroMovePayload{
		X:       hm.CurrentX,
		Y:       hm.CurrentY,
		TargetX: tx,
		TargetY: ty,
		Speed:   hm.Speed,
		Heading: hm.Heading(),
	}
}

// RoutePayload builds the route_assigned WS payload.
func (hm *HeroMovement) RoutePayload() *model.RouteAssignedPayload {
	if hm.Road == nil {
		return nil
	}
	waypoints := make([]model.PointXY, len(hm.Road.Waypoints))
	for i, p := range hm.Road.Waypoints {
		waypoints[i] = model.PointXY{X: p.X, Y: p.Y}
	}
	return &model.RouteAssignedPayload{
		RoadID:            hm.Road.ID,
		Waypoints:         waypoints,
		DestinationTownID: hm.DestinationTownID,
		Speed:             hm.Speed,
	}
}

// PositionSyncPayload builds the position_sync WS payload.
func (hm *HeroMovement) PositionSyncPayload() model.PositionSyncPayload {
	return model.PositionSyncPayload{
		X:                hm.CurrentX,
		Y:                hm.CurrentY,
		WaypointIndex:    hm.WaypointIndex,
		WaypointFraction: hm.WaypointFraction,
		State:            string(hm.State),
	}
}

// randomRestDuration returns a random duration between TownRestMin and TownRestMax.
func randomRestDuration() time.Duration {
	rangeMs := (TownRestMax - TownRestMin).Milliseconds()
	return TownRestMin + time.Duration(rand.Int63n(rangeMs+1))*time.Millisecond
}

// EncounterStarter starts or resolves a random encounter while walking (engine: combat;
// offline: synchronous SimulateOneFight via callback).
type EncounterStarter func(hm *HeroMovement, enemy *model.Enemy, now time.Time)

// ProcessSingleHeroMovementTick applies one movement-system step as of logical time now.
// It mirrors the online engine's 500ms cadence: callers should advance now in MovementTickRate
// steps (plus a final partial step to real time) for catch-up simulation.
//
// sender may be nil to suppress all WebSocket payloads (offline ticks).
// onEncounter is required for walking encounter rolls; if nil, encounters are not triggered.
func ProcessSingleHeroMovementTick(
	heroID int64,
	hm *HeroMovement,
	graph *RoadGraph,
	now time.Time,
	sender MessageSender,
	onEncounter EncounterStarter,
) {
	if graph == nil {
		return
	}

	switch hm.State {
	case model.StateFighting, model.StateDead:
		return

	case model.StateResting:
		if now.After(hm.RestUntil) {
			hm.LeaveTown(graph, now)
			hm.SyncToHero()
			if sender != nil {
				sender.SendToHero(heroID, "town_exit", model.TownExitPayload{})
				if route := hm.RoutePayload(); route != nil {
					sender.SendToHero(heroID, "route_assigned", route)
				}
			}
		}

	case model.StateInTown:
		if len(hm.TownNPCQueue) == 0 {
			hm.LeaveTown(graph, now)
			hm.SyncToHero()
			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(hm.NextTownNPCRollAt) {
			return
		}
		if rand.Float64() < townNPCVisitChance {
			npcID := hm.TownNPCQueue[0]
			hm.TownNPCQueue = hm.TownNPCQueue[1:]
			if npc, ok := graph.NPCByID[npcID]; ok && sender != nil {
				sender.SendToHero(heroID, "town_npc_visit", model.TownNPCVisitPayload{
					NPCID: npc.ID, Name: npc.Name, Type: npc.Type, TownID: hm.CurrentTownID,
				})
			}
			hm.NextTownNPCRollAt = now.Add(randomTownNPCDelay())
		} else {
			hm.NextTownNPCRollAt = now.Add(townNPCRetryAfterMiss)
		}

	case model.StateWalking:
		// #region agent log
		if hm.Road == nil {
			agentDebugLog("H1", "movement.go:StateWalking", "walking with nil Road", map[string]any{
				"heroID": heroID, "currentTownID": hm.CurrentTownID, "destinationTownID": hm.DestinationTownID,
				"x": hm.CurrentX, "y": hm.CurrentY,
			})
		} else if len(hm.Road.Waypoints) < 2 {
			agentDebugLog("H2", "movement.go:StateWalking", "road has fewer than 2 waypoints", map[string]any{
				"heroID": heroID, "roadID": hm.Road.ID, "waypointCount": len(hm.Road.Waypoints),
			})
		}
		// #endregion
		reachedTown := hm.AdvanceTick(now, graph)

		if reachedTown {
			hm.EnterTown(now, graph)

			if sender != nil {
				town := graph.Towns[hm.CurrentTownID]
				if town != nil {
					npcInfos := make([]model.TownNPCInfo, 0, len(graph.TownNPCs[hm.CurrentTownID]))
					for _, n := range graph.TownNPCs[hm.CurrentTownID] {
						npcInfos = append(npcInfos, model.TownNPCInfo{ID: n.ID, Name: n.Name, Type: n.Type})
					}
					var restMs int64
					if hm.State == model.StateResting {
						restMs = hm.RestUntil.Sub(now).Milliseconds()
					}
					sender.SendToHero(heroID, "town_enter", model.TownEnterPayload{
						TownID:         town.ID,
						TownName:       town.Name,
						Biome:          town.Biome,
						NPCs:           npcInfos,
						RestDurationMs: restMs,
					})
				}
			}

			hm.SyncToHero()
			return
		}

		if onEncounter != nil && hm.ShouldEncounter(now) {
			// #region agent log
			agentDebugLog("H3", "movement.go:encounter", "encounter starting", map[string]any{
				"heroID": heroID, "roadNil": hm.Road == nil, "waypointCount": func() int {
					if hm.Road == nil {
						return -1
					}
					return len(hm.Road.Waypoints)
				}(),
				"x": hm.CurrentX, "y": hm.CurrentY, "currentTownID": hm.CurrentTownID,
			})
			// #endregion
			enemy := PickEnemyForLevel(hm.Hero.Level)
			hm.LastEncounterAt = now
			onEncounter(hm, &enemy, now)
			return
		}

		if sender != nil {
			sender.SendToHero(heroID, "hero_move", hm.MovePayload())
		}

		hm.Hero.PositionX = hm.CurrentX
		hm.Hero.PositionY = hm.CurrentY
	}
}
