package storage

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"

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

const heroTownSessionKeyFmt = "autohero:v1:hero:%d:town_session"

// HeroTownSessionRedis is the last persisted in-town NPC tour snapshot (reconnect / crash recovery).
type HeroTownSessionRedis struct {
	SavedAtUnixNano int64                     `json:"savedAtUnixNano"`
	State           model.GameState           `json:"state"`
	CurrentTownID   int64                     `json:"currentTownId,omitempty"`
	PositionX       float64                   `json:"positionX"`
	PositionY       float64                   `json:"positionY"`
	TownPause       *model.TownPausePersisted `json:"townPause,omitempty"`
}

// TownSessionStore mirrors in-town hero state to Redis for faster/stale-DB-safe reconnect.
type TownSessionStore struct {
	rdb *redis.Client
}

// NewTownSessionStore returns a store backed by Redis, or nil if rdb is nil.
func NewTownSessionStore(rdb *redis.Client) *TownSessionStore {
	if rdb == nil {
		return nil
	}
	return &TownSessionStore{rdb: rdb}
}

func (s *TownSessionStore) key(heroID int64) string {
	return fmt.Sprintf(heroTownSessionKeyFmt, heroID)
}

// Save stores the hero's in-town session. Caller must set hero.TownPause (e.g. after SyncToHero).
func (s *TownSessionStore) Save(ctx context.Context, heroID int64, h *model.Hero) error {
	if s == nil || s.rdb == nil || h == nil {
		return nil
	}
	if h.State != model.StateInTown {
		return nil
	}
	var townID int64
	if h.CurrentTownID != nil {
		townID = *h.CurrentTownID
	}
	payload := HeroTownSessionRedis{
		SavedAtUnixNano: time.Now().UnixNano(),
		State:           h.State,
		CurrentTownID:   townID,
		PositionX:       h.PositionX,
		PositionY:       h.PositionY,
		TownPause:       h.TownPause,
	}
	b, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	return s.rdb.Set(ctx, s.key(heroID), b, 72*time.Hour).Err()
}

// Delete removes the in-town session key (hero left town or state no longer in_town).
func (s *TownSessionStore) Delete(ctx context.Context, heroID int64) error {
	if s == nil || s.rdb == nil {
		return nil
	}
	return s.rdb.Del(ctx, s.key(heroID)).Err()
}

// Load returns the stored session, or (nil, nil) if missing.
func (s *TownSessionStore) Load(ctx context.Context, heroID int64) (*HeroTownSessionRedis, error) {
	if s == nil || s.rdb == nil {
		return nil, nil
	}
	b, err := s.rdb.Get(ctx, s.key(heroID)).Bytes()
	if err != nil {
		if errors.Is(err, redis.Nil) {
			return nil, nil
		}
		return nil, err
	}
	var p HeroTownSessionRedis
	if err := json.Unmarshal(b, &p); err != nil {
		return nil, err
	}
	return &p, nil
}
