package zone

import (
	"time"

	"github.com/titanappdev/rofs/apps/world/internal/formula"
	"github.com/titanappdev/rofs/apps/world/internal/monster"
	"github.com/titanappdev/rofs/apps/world/internal/player"
)

func (z *Zone) initMonsters() {
	defs := []struct {
		id, name       string
		x, y, hp, dmg, armor, xp, gmin, gmax, resp int
	}{
		{"goblin_1", "Goblin", 20, 20, 30, 5, 2, 15, 2, 8, 45},
		{"goblin_2", "Goblin", 30, 28, 30, 5, 2, 15, 2, 8, 45},
		{"wolf_1", "Dire Wolf", 15, 35, 50, 8, 5, 25, 5, 15, 90},
	}
	for _, d := range defs {
		m := monster.New(d.id, d.name, d.x, d.y, d.hp, d.dmg, d.armor, d.xp, d.gmin, d.gmax, d.resp)
		z.monsters[m.ID] = m
		_ = z.ReserveTile(m.X, m.Y, m.ID)
	}
}

func (z *Zone) handleAttack(cmd Command) Result {
	p, ok := z.players[cmd.PlayerID]
	if !ok {
		return Result{Success: false, Error: "not in world"}
	}
	if p.Fatigue < 1 {
		return Result{Success: false, Error: "too fatigued"}
	}

	dir, _ := cmd.Payload["direction"].(string)
	tx, ty := p.X, p.Y
	switch dir {
	case "north":
		ty--
	case "south":
		ty++
	case "east":
		tx++
	case "west":
		tx--
	default:
		return Result{Success: false, Error: "invalid direction"}
	}

	targetKey := TileKey(tx, ty)
	targetID, occupied := z.occupancy[targetKey]
	if !occupied {
		return Result{Success: false, Error: "nothing to attack"}
	}

	m, isMonster := z.monsters[targetID]
	if !isMonster || m.Dead {
		return Result{Success: false, Error: "invalid target"}
	}

	rawDmg := formula.PhysicalDamage(p.Strength, 2)
	dmg := formula.ApplyArmor(rawDmg, m.Armor)
	m.HP -= dmg
	p.Fatigue--
	p.InCombat = true
	p.LastCombat = time.Now()
	p.LastAction = time.Now()

	logEntry := map[string]any{
		"attacker": p.CharacterID, "target": m.Name,
		"damage": dmg, "target_hp": max(0, m.HP),
	}
	z.broadcastCombat(p, logEntry)

	if m.HP <= 0 {
		m.Dead = true
		m.RespawnAt = time.Now().Add(time.Duration(m.RespawnS) * time.Second).Unix()
		z.ReleaseTile(m.X, m.Y)
		z.broadcastCombat(p, map[string]any{
			"event": "kill", "monster": m.Name, "xp": m.XP,
		})
	}

	return Result{
		Success: true,
		Payload: map[string]any{
			"viewport": z.buildViewport(p),
			"combat":   logEntry,
			"stats":    z.playerStats(p),
		},
	}
}

func (z *Zone) handleChat(cmd Command) Result {
	p, ok := z.players[cmd.PlayerID]
	if !ok {
		return Result{Success: false, Error: "not in world"}
	}
	body, _ := cmd.Payload["body"].(string)
	channel, _ := cmd.Payload["channel"].(string)
	if body == "" || len(body) > 500 {
		return Result{Success: false, Error: "invalid message"}
	}
	if channel == "" {
		channel = "world"
	}
	msg := map[string]any{
		"channel": channel, "sender": p.CharacterID, "body": body,
	}
	z.broadcastAll("chat", msg)
	return Result{Success: true}
}

func (z *Zone) tickMonsters() {
	if len(z.players) == 0 {
		return
	}
	now := time.Now().Unix()
	for _, m := range z.monsters {
		if m.Dead {
			if now >= m.RespawnAt {
				m.Dead = false
				m.HP = m.MaxHP
				m.X, m.Y = m.SpawnX, m.SpawnY
				if !z.IsOccupied(m.X, m.Y) {
					_ = z.ReserveTile(m.X, m.Y, m.ID)
				}
			}
			continue
		}
		// Simple AI: move toward nearest player if adjacent range
		nearest, dist := z.nearestPlayer(m.X, m.Y)
		if nearest == nil || dist > 8 {
			continue
		}
		if dist == 1 {
			raw := formula.PhysicalDamage(5, m.Damage)
			dmg := formula.ApplyArmor(raw, 0)
			nearest.HP -= dmg
			nearest.InCombat = true
			nearest.LastCombat = time.Now()
			z.broadcastCombat(nearest, map[string]any{
				"event": "monster_hit", "monster": m.Name, "damage": dmg,
			})
			if nearest.HP <= 0 {
				z.handlePlayerDeath(nearest)
			}
			continue
		}
		z.moveMonsterToward(m, nearest)
	}
}

func (z *Zone) moveMonsterToward(m *monster.State, target *player.State) {
	dx, dy := 0, 0
	if target.X > m.X {
		dx = 1
	} else if target.X < m.X {
		dx = -1
	} else if target.Y > m.Y {
		dy = 1
	} else if target.Y < m.Y {
		dy = -1
	}
	nx, ny := m.X+dx, m.Y+dy
	if z.IsBlocked(nx, ny) || z.IsOccupied(nx, ny) {
		return
	}
	z.ReleaseTile(m.X, m.Y)
	m.X, m.Y = nx, ny
	_ = z.ReserveTile(nx, ny, m.ID)
}

func (z *Zone) nearestPlayer(x, y int) (*player.State, int) {
	var best *player.State
	bestDist := 999
	for _, p := range z.players {
		d := abs(p.X-x) + abs(p.Y-y)
		if d < bestDist {
			bestDist = d
			best = p
		}
	}
	return best, bestDist
}

func (z *Zone) tickRegen() {
	if len(z.players) == 0 {
		return
	}
	now := time.Now()
	for _, p := range z.players {
		if p.HP <= 0 {
			continue
		}
		// Mana per second (scaled by spirit)
		if p.Mana < p.MaxMana {
			p.Mana = min(p.MaxMana, p.Mana+1+(p.Spirit/10))
		}
		// Fatigue per second
		if p.Fatigue < p.MaxFatigue && now.Sub(p.LastAction) > 2*time.Second {
			p.Fatigue = min(p.MaxFatigue, p.Fatigue+1+(p.Spirit/20))
		}
		// Health every 15s out of combat
		if !p.InCombat && now.Sub(p.LastCombat) > 15*time.Second {
			if p.HP < p.MaxHP {
				p.HP = min(p.MaxHP, p.HP+2+(p.Spirit/5))
			}
		}
		if p.InCombat && now.Sub(p.LastCombat) > 30*time.Second {
			p.InCombat = false
		}
		// Inactivity removal (2 min)
		if now.Sub(p.LastAction) > 120*time.Second {
			z.RemovePlayer(p.ID)
		}
	}
}

func (z *Zone) handlePlayerDeath(p *player.State) {
	p.HP = 0
	z.broadcastCombat(p, map[string]any{"event": "death", "character_id": p.CharacterID})
	z.RemovePlayer(p.ID)
}

func (z *Zone) broadcastCombat(p *player.State, payload map[string]any) {
	z.broadcastNearby(p.X, p.Y, "combat_log", payload)
}

func (z *Zone) broadcastNearby(cx, cy int, msgType string, payload map[string]any) {
	for _, p := range z.players {
		if abs(p.X-cx)+abs(p.Y-cy) <= 9 && p.Conn != nil {
			p.Conn(msgType, payload)
		}
	}
}

func (z *Zone) broadcastAll(msgType string, payload map[string]any) {
	for _, p := range z.players {
		if p.Conn != nil {
			p.Conn(msgType, payload)
		}
	}
}

func (z *Zone) playerStats(p *player.State) map[string]any {
	return map[string]any{
		"hp": p.HP, "max_hp": p.MaxHP,
		"mana": p.Mana, "max_mana": p.MaxMana,
		"fatigue": p.Fatigue, "max_fatigue": p.MaxFatigue,
		"in_combat": p.InCombat,
	}
}

func (z *Zone) IsBlocked(x, y int) bool {
	if x < 0 || y < 0 || x >= z.width || y >= z.height {
		return true
	}
	key := TileKey(x, y)
	return z.blocked[key]
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}
