package world

import (
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"hash/fnv"
	"math/rand"
	"strings"
)

const mapVersion = "v1"

// MapRef is a lightweight map descriptor returned by hero init.
type MapRef struct {
	MapID               string `json:"mapId"`
	MapVersion          string `json:"mapVersion"`
	ETag                string `json:"etag"`
	Biome               string `json:"biome"`
	RecommendedLevelMin int    `json:"recommendedLevelMin"`
	RecommendedLevelMax int    `json:"recommendedLevelMax"`
}

// Tile is a single ground cell in the map grid.
type Tile struct {
	X       int    `json:"x"`
	Y       int    `json:"y"`
	Terrain string `json:"terrain"`
}

// Object is a world object rendered on top of tiles.
type Object struct {
	ID   string `json:"id"`
	Type string `json:"type"`
	X    int    `json:"x"`
	Y    int    `json:"y"`
}

// SpawnPoint defines where entities can be spawned.
type SpawnPoint struct {
	ID   string `json:"id"`
	Kind string `json:"kind"`
	X    int    `json:"x"`
	Y    int    `json:"y"`
}

// ServerMap is the full map payload returned by /maps/{mapId}.
type ServerMap struct {
	MapID               string       `json:"mapId"`
	MapVersion          string       `json:"mapVersion"`
	Biome               string       `json:"biome"`
	RecommendedLevelMin int          `json:"recommendedLevelMin"`
	RecommendedLevelMax int          `json:"recommendedLevelMax"`
	Width               int          `json:"width"`
	Height              int          `json:"height"`
	Tiles               []Tile       `json:"tiles"`
	Objects             []Object     `json:"objects"`
	SpawnPoints         []SpawnPoint `json:"spawnPoints"`
}

type levelBand struct {
	min   int
	max   int
	biome string
}

var levelBands = []levelBand{
	{min: 1, max: 10, biome: "meadow"},
	{min: 11, max: 20, biome: "forest"},
	{min: 21, max: 35, biome: "ruins"},
	{min: 36, max: 50, biome: "canyon"},
	{min: 51, max: 70, biome: "swamp"},
	{min: 71, max: 100, biome: "volcanic"},
	{min: 101, max: 999, biome: "astral"},
}

type mapData struct {
	ref  MapRef
	data ServerMap
}

// Service provides deterministic map refs and payloads for MVP.
type Service struct {
	byID map[string]mapData
}

// NewService creates a map service with precomputed deterministic maps.
func NewService() *Service {
	s := &Service{
		byID: make(map[string]mapData, len(levelBands)),
	}
	for _, band := range levelBands {
		mapID := fmt.Sprintf("%s-%d-%d", band.biome, band.min, band.max)
		serverMap := generateMap(mapID, band)
		etag := computeETag(serverMap)
		s.byID[mapID] = mapData{
			ref: MapRef{
				MapID:               mapID,
				MapVersion:          mapVersion,
				ETag:                etag,
				Biome:               band.biome,
				RecommendedLevelMin: band.min,
				RecommendedLevelMax: band.max,
			},
			data: serverMap,
		}
	}
	return s
}

// RefForLevel returns a deterministic map reference for hero level.
func (s *Service) RefForLevel(level int) MapRef {
	band := bandForLevel(level)
	mapID := fmt.Sprintf("%s-%d-%d", band.biome, band.min, band.max)
	if entry, ok := s.byID[mapID]; ok {
		return entry.ref
	}
	// Fallback should not happen in normal flow.
	return MapRef{
		MapID:               mapID,
		MapVersion:          mapVersion,
		Biome:               band.biome,
		RecommendedLevelMin: band.min,
		RecommendedLevelMax: band.max,
	}
}

// GetMap returns map payload and ETag by map ID.
func (s *Service) GetMap(mapID string) (*ServerMap, string, bool) {
	entry, ok := s.byID[strings.TrimSpace(mapID)]
	if !ok {
		return nil, "", false
	}
	m := entry.data
	return &m, entry.ref.ETag, true
}

func bandForLevel(level int) levelBand {
	if level < 1 {
		level = 1
	}
	for _, b := range levelBands {
		if level >= b.min && level <= b.max {
			return b
		}
	}
	return levelBands[len(levelBands)-1]
}

func generateMap(mapID string, band levelBand) ServerMap {
	const (
		width  = 24
		height = 24
	)

	seed := hashSeed(mapID, band.biome, band.min, band.max)
	rng := rand.New(rand.NewSource(seed))

	tiles := make([]Tile, 0, width*height)
	isRoad := make(map[[2]int]struct{}, width*2)

	roadY := clamp(9+rng.Intn(6), 2, height-3)
	currentY := roadY
	for x := 0; x < width; x++ {
		if x > 0 && x%4 == 0 {
			currentY = clamp(currentY+rng.Intn(3)-1, 2, height-3)
		}
		isRoad[[2]int{x, currentY}] = struct{}{}
		isRoad[[2]int{x, currentY + 1}] = struct{}{}
	}

	baseTerrain := biomeBaseTerrain(band.biome)
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			terrain := baseTerrain
			if _, ok := isRoad[[2]int{x, y}]; ok {
				terrain = "road"
			}
			tiles = append(tiles, Tile{
				X:       x,
				Y:       y,
				Terrain: terrain,
			})
		}
	}

	objects := make([]Object, 0, 96)
	objID := 1
	for y := 1; y < height-1; y++ {
		for x := 1; x < width-1; x++ {
			if _, ok := isRoad[[2]int{x, y}]; ok {
				continue
			}
			roll := rng.Intn(1000)
			switch {
			case roll < 70:
				objects = append(objects, Object{
					ID:   fmt.Sprintf("obj-%d", objID),
					Type: "tree",
					X:    x,
					Y:    y,
				})
				objID++
			case roll < 120:
				objects = append(objects, Object{
					ID:   fmt.Sprintf("obj-%d", objID),
					Type: "bush",
					X:    x,
					Y:    y,
				})
				objID++
			}
		}
	}

	spawnPoints := []SpawnPoint{
		{ID: "hero-start", Kind: "hero", X: 1, Y: roadY},
		{ID: "enemy-main", Kind: "enemy", X: width - 3, Y: roadY},
		{ID: "enemy-alt", Kind: "enemy", X: width - 5, Y: clamp(roadY+1, 1, height-2)},
	}

	return ServerMap{
		MapID:               mapID,
		MapVersion:          mapVersion,
		Biome:               band.biome,
		RecommendedLevelMin: band.min,
		RecommendedLevelMax: band.max,
		Width:               width,
		Height:              height,
		Tiles:               tiles,
		Objects:             objects,
		SpawnPoints:         spawnPoints,
	}
}

func biomeBaseTerrain(biome string) string {
	switch biome {
	case "forest", "meadow":
		return "grass"
	case "canyon":
		return "dirt"
	case "ruins":
		return "stone"
	case "swamp":
		return "mud"
	case "volcanic":
		return "ash"
	case "astral":
		return "ether"
	default:
		return "grass"
	}
}

func computeETag(m ServerMap) string {
	payload, _ := json.Marshal(m)
	sum := sha1.Sum(payload)
	return `"` + hex.EncodeToString(sum[:]) + `"`
}

func hashSeed(parts ...any) int64 {
	h := fnv.New64a()
	for _, p := range parts {
		fmt.Fprint(h, p, "|")
	}
	return int64(h.Sum64() & 0x7fffffffffffffff)
}

func clamp(v, min, max int) int {
	if v < min {
		return min
	}
	if v > max {
		return max
	}
	return v
}
