package zone

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/titanappdev/rofs/apps/world/internal/logging"
	"github.com/titanappdev/rofs/apps/world/internal/monster"
	"github.com/titanappdev/rofs/apps/world/internal/player"
)

// Command represents a player or system action submitted to the zone queue.
type Command struct {
	ID       string
	Type     string
	PlayerID string
	Payload  map[string]any
	Seq      uint64
	ReplyCh  chan Result
}

// Result is returned after a command is processed.
type Result struct {
	Seq     uint64
	Success bool
	Error   string
	Payload map[string]any
}

// Zone runs the authoritative simulation for one map region via a single goroutine.
type Zone struct {
	id        string
	log       *logging.Logger
	tickMS    int
	width     int
	height    int
	cmdCh     chan Command
	stopCh    chan struct{}
	wg        sync.WaitGroup
	seq       uint64
	occupancy map[string]string
	blocked   map[string]bool
	players   map[string]*player.State
	monsters  map[string]*monster.State
	tickCount int
}

// New creates a zone with an ordered command channel (single-writer model).
func New(id string, tickMS, width, height int, log *logging.Logger) *Zone {
	z := &Zone{
		id:        id,
		log:       log.With(map[string]any{"zone_id": id}),
		tickMS:    tickMS,
		width:     width,
		height:    height,
		cmdCh:     make(chan Command, 256),
		stopCh:    make(chan struct{}),
		occupancy: make(map[string]string),
		blocked:   make(map[string]bool),
		players:   make(map[string]*player.State),
		monsters:  make(map[string]*monster.State),
	}
	z.initBlockedTiles()
	z.initMonsters()
	return z
}

func (z *Zone) initBlockedTiles() {
	for i := 0; i < z.width; i++ {
		z.blocked[TileKey(i, 0)] = true
		z.blocked[TileKey(i, z.height-1)] = true
	}
	for j := 0; j < z.height; j++ {
		z.blocked[TileKey(0, j)] = true
		z.blocked[TileKey(z.width-1, j)] = true
	}
}

func (z *Zone) Start(ctx context.Context) {
	z.wg.Add(1)
	go z.run(ctx)
}

func (z *Zone) Submit(cmd Command) error {
	select {
	case z.cmdCh <- cmd:
		return nil
	case <-z.stopCh:
		return fmt.Errorf("zone %s stopped", z.id)
	default:
		return fmt.Errorf("zone %s command queue full", z.id)
	}
}

func (z *Zone) Stop() {
	close(z.stopCh)
	z.wg.Wait()
}

func (z *Zone) run(ctx context.Context) {
	defer z.wg.Done()
	defer func() {
		if r := recover(); r != nil {
			z.log.Error("zone panic recovered", fmt.Errorf("%v", r), map[string]any{"panic": r})
		}
	}()

	ticker := time.NewTicker(time.Duration(z.tickMS) * time.Millisecond)
	defer ticker.Stop()
	z.log.Info("zone started", map[string]any{"tick_ms": z.tickMS, "size": fmt.Sprintf("%dx%d", z.width, z.height)})

	for {
		select {
		case <-ctx.Done():
			return
		case <-z.stopCh:
			return
		case cmd := <-z.cmdCh:
			z.processCommand(cmd)
		case <-ticker.C:
			z.tick()
		}
	}
}

func (z *Zone) processCommand(cmd Command) {
	start := time.Now()
	z.seq++
	seq := z.seq

	var result Result
	result.Seq = seq

	switch cmd.Type {
	case "enter":
		result = z.handleEnter(cmd)
	case "move":
		result = z.handleMove(cmd)
	case "attack":
		result = z.handleAttack(cmd)
	case "chat":
		result = z.handleChat(cmd)
	default:
		result = Result{Seq: seq, Success: false, Error: "unknown command"}
	}

	result.Seq = seq
	if cmd.ReplyCh != nil {
		cmd.ReplyCh <- result
	}

	z.log.Debug("command processed", map[string]any{
		"cmd_type": cmd.Type, "player_id": cmd.PlayerID, "seq": seq,
		"success": result.Success, "duration_ms": time.Since(start).Milliseconds(),
	})
}

func (z *Zone) SetPlayerConn(playerID string, fn player.BroadcastFunc) {
	if p, ok := z.players[playerID]; ok {
		p.Conn = fn
	}
}

func (z *Zone) handleEnter(cmd Command) Result {
	charID, _ := cmd.Payload["character_id"].(float64)
	accID, _ := cmd.Payload["account_id"].(float64)
	savedX, _ := cmd.Payload["tile_x"].(float64)
	savedY, _ := cmd.Payload["tile_y"].(float64)

	x, y := int(savedX), int(savedY)
	if x <= 0 {
		x, y = 25, 25
	}
	if z.IsOccupied(x, y) || z.IsBlocked(x, y) {
		nx, ny, ok := z.FindNearestFreeTile(x, y, 10)
		if !ok {
			x, y = 25, 25
		} else {
			x, y = nx, ny
		}
	}

	p := player.New(int(charID), int(accID), x, y)
	z.players[cmd.PlayerID] = p
	_ = z.ReserveTile(x, y, cmd.PlayerID)

	return Result{
		Success: true,
		Payload: map[string]any{
			"viewport": z.buildViewport(p),
			"stats":    z.playerStats(p),
		},
	}
}

func (z *Zone) handleMove(cmd Command) Result {
	p, ok := z.players[cmd.PlayerID]
	if !ok {
		return Result{Success: false, Error: "not in world"}
	}
	if p.InCombat {
		return Result{Success: false, Error: "in combat"}
	}
	if p.Fatigue < 1 {
		return Result{Success: false, Error: "too fatigued"}
	}

	dir, _ := cmd.Payload["direction"].(string)
	nx, ny := p.X, p.Y
	switch dir {
	case "north":
		ny--
	case "south":
		ny++
	case "east":
		nx++
	case "west":
		nx--
	default:
		return Result{Success: false, Error: "invalid direction"}
	}

	if z.IsBlocked(nx, ny) {
		return Result{Success: false, Error: "cannot move there"}
	}
	if z.IsOccupied(nx, ny) {
		return Result{Success: false, Error: "blocked"}
	}

	z.ReleaseTile(p.X, p.Y)
	p.X, p.Y = nx, ny
	p.Fatigue--
	p.LastAction = time.Now()
	_ = z.ReserveTile(nx, ny, cmd.PlayerID)

	z.broadcastNearby(p.X, p.Y, "entity_moved", map[string]any{
		"character_id": p.CharacterID, "x": p.X, "y": p.Y,
	})

	return Result{
		Success: true,
		Payload: map[string]any{
			"viewport": z.buildViewport(p),
			"stats":    z.playerStats(p),
		},
	}
}

func (z *Zone) buildViewport(center *player.State) []map[string]any {
	tiles := make([]map[string]any, 0, 81)
	for dy := -4; dy <= 4; dy++ {
		for dx := -4; dx <= 4; dx++ {
			wx, wy := center.X+dx, center.Y+dy
			sprite := "black"
			entity := ""
			if wx >= 0 && wy >= 0 && wx < z.width && wy < z.height {
				if z.IsBlocked(wx, wy) {
					sprite = "wall"
				} else {
					sprite = "floor"
				}
				key := TileKey(wx, wy)
				if eid, ok := z.occupancy[key]; ok {
					if eid == center.ID {
						sprite = "player"
						entity = "you"
					} else if _, isM := z.monsters[eid]; isM {
						sprite = "monster"
						entity = z.monsters[eid].Name
					} else if _, isP := z.players[eid]; isP {
						sprite = "player_other"
						entity = "player"
					}
				}
			}
			tiles = append(tiles, map[string]any{
				"x": dx + 4, "y": dy + 4, "sprite": sprite, "entity": entity,
			})
		}
	}
	return tiles
}

func (z *Zone) tick() {
	z.tickCount++
	if z.tickCount%10 == 0 {
		z.tickRegen()
	}
	if z.tickCount%5 == 0 {
		z.tickMonsters()
	}
}

func TileKey(x, y int) string {
	return fmt.Sprintf("%d,%d", x, y)
}

func (z *Zone) IsOccupied(x, y int) bool {
	if x < 0 || y < 0 || x >= z.width || y >= z.height {
		return true
	}
	_, ok := z.occupancy[TileKey(x, y)]
	return ok
}

func (z *Zone) ReserveTile(x, y int, entityID string) error {
	key := TileKey(x, y)
	if _, taken := z.occupancy[key]; taken {
		return fmt.Errorf("tile %s occupied", key)
	}
	z.occupancy[key] = entityID
	return nil
}

func (z *Zone) ReleaseTile(x, y int) {
	delete(z.occupancy, TileKey(x, y))
}

func (z *Zone) FindNearestFreeTile(x, y, maxRadius int) (int, int, bool) {
	if !z.IsOccupied(x, y) && !z.IsBlocked(x, y) {
		return x, y, true
	}
	for r := 1; r <= maxRadius; r++ {
		for dx := -r; dx <= r; dx++ {
			for dy := -r; dy <= r; dy++ {
				if abs(dx) != r && abs(dy) != r {
					continue
				}
				nx, ny := x+dx, y+dy
				if !z.IsOccupied(nx, ny) && !z.IsBlocked(nx, ny) {
					return nx, ny, true
				}
			}
		}
	}
	return 0, 0, false
}

func abs(n int) int {
	if n < 0 {
		return -n
	}
	return n
}

func (z *Zone) RemovePlayer(playerID string) {
	if p, ok := z.players[playerID]; ok {
		z.ReleaseTile(p.X, p.Y)
		delete(z.players, playerID)
	}
}
