// Package fs implements galaxy/storage.Storage using the filesystem.
package fs

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"sync"

	gerr "galaxy/error"
	"galaxy/model/client"
	"galaxy/model/order"
	"galaxy/model/report"
	"galaxy/util"

	"github.com/google/uuid"
)

const (
	// stateFileName is the file name under the storage root where [client.State] is stored.
	stateFileName = "state.dat"

	// gameDataFileSuffix is the extension for per-turn [client.GameData] files.
	gameDataFileSuffix = ".dat"

	defaultFilePerm = 0o644
	oldFileSuffix   = ".old"
	newFileSuffix   = ".new"
)

// StateFilePath returns the path to the persisted [client.State] file under root.
func StateFilePath(root string) string {
	return filepath.Join(root, stateFileName)
}

// GameDataFilePath returns the legacy per-game data file path under root.
//
// The storage implementation keeps turn data in per-turn files under a game directory
// and does not use this helper internally.
func GameDataFilePath(root string, id fmt.Stringer) string {
	return filepath.Join(root, id.String()) + gameDataFileSuffix
}

type pathLock struct {
	mu   sync.Mutex
	refs int
}

type fsStorage struct {
	storageRoot string

	locksMu sync.Mutex
	locks   map[string]*pathLock

	readFileFn   func(string) ([]byte, error)
	writeFileFn  func(string, []byte, os.FileMode) error
	renameFileFn func(string, string) error
	removeFileFn func(string) error
}

type storedOrder struct {
	GameID    uuid.UUID         `json:"game_id"`
	UpdatedAt int64             `json:"updatedAt"`
	Commands  []json.RawMessage `json:"cmd"`
}

type storedGameData struct {
	Turn   uint          `json:"turn"`
	Report report.Report `json:"report"`
	Order  *storedOrder  `json:"order,omitempty"`
}

// NewFS returns a filesystem-backed implementation of galaxy/storage.Storage rooted at storageRoot.
// storageRoot must already exist, be a directory, and be writable by the current user.
func NewFS(storageRoot string) (*fsStorage, error) {
	fmt.Println("using fs root:", storageRoot)
	absRoot, err := filepath.Abs(storageRoot)
	if err != nil {
		return nil, classifyStorageError(fmt.Errorf("new fs storage: resolve absolute path for %q: %w", storageRoot, err))
	}
	if ok, err := util.PathExists(absRoot, true); err != nil {
		return nil, classifyStorageError(fmt.Errorf("new fs storage: check path %q exists: %w", absRoot, err))
	} else if !ok {
		return nil, classifyStorageError(fmt.Errorf("new fs storage: path %q does not exist", absRoot))
	}
	if ok, err := util.Writable(absRoot); err != nil {
		return nil, classifyStorageError(fmt.Errorf("new fs storage: check path %q writable: %w", absRoot, err))
	} else if !ok {
		return nil, classifyStorageError(fmt.Errorf("new fs storage: path %q is not writable", absRoot))
	}

	return &fsStorage{
		storageRoot:  absRoot,
		locks:        make(map[string]*pathLock),
		readFileFn:   os.ReadFile,
		writeFileFn:  os.WriteFile,
		renameFileFn: os.Rename,
		removeFileFn: os.Remove,
	}, nil
}

func (s *fsStorage) StateExistsAsync(callback func(bool, error)) {
	go func() {
		exists, err := s.StateExists()
		if callback != nil {
			callback(exists, err)
		}
	}()
}

func (s *fsStorage) LoadStateAsync(callback func(client.State, error)) {
	go func() {
		state, err := s.LoadState()
		if callback != nil {
			callback(state, err)
		}
	}()
}

func (s *fsStorage) SaveStateAsync(state client.State, callback func(error)) {
	go func() {
		err := s.SaveState(state)
		if callback != nil {
			callback(err)
		}
	}()
}

func (s *fsStorage) ReportExistsAsync(id client.GameID, turn uint, callback func(bool, error)) {
	go func() {
		exists, err := s.gameDataExistsSync(id, turn)
		if callback != nil {
			callback(exists, err)
		}
	}()
}

func (s *fsStorage) LoadReportAsync(id client.GameID, turn uint, callback func(report.Report, error)) {
	go func() {
		rep, err := s.loadReportSync(id, turn)
		if callback != nil {
			callback(rep, err)
		}
	}()
}

func (s *fsStorage) SaveReportAsync(id client.GameID, turn uint, rep report.Report, callback func(error)) {
	go func() {
		err := s.saveReportSync(id, turn, rep)
		if callback != nil {
			callback(err)
		}
	}()
}

func (s *fsStorage) LoadOrderAsync(id client.GameID, turn uint, callback func(order.UserGamesOrder, error)) {
	go func() {
		o, err := s.loadOrderSync(id, turn)
		if callback != nil {
			callback(o, err)
		}
	}()
}

func (s *fsStorage) SaveOrderAsync(id client.GameID, turn uint, o order.UserGamesOrder, callback func(error)) {
	go func() {
		err := s.saveOrderSync(id, turn, o)
		if callback != nil {
			callback(err)
		}
	}()
}

func (s *fsStorage) FileExists(path string) (bool, string, error) {
	absPath, err := s.resolvePath(path)
	if err != nil {
		return false, "", classifyStorageError(err)
	}

	var exists bool
	err = s.withPathLock(absPath, func() error {
		var opErr error
		exists, opErr = s.fileExistsUnlocked(absPath)
		return opErr
	})
	if err != nil {
		return false, "", classifyStorageError(err)
	}
	if !exists {
		return false, absPath, nil
	}
	return true, absPath, nil
}

func (s *fsStorage) ReadFile(path string) ([]byte, error) {
	absPath, err := s.resolvePath(path)
	if err != nil {
		return nil, classifyStorageError(err)
	}

	var data []byte
	err = s.withPathLock(absPath, func() error {
		var opErr error
		data, opErr = s.readFileUnlocked(absPath)
		return opErr
	})
	return data, classifyStorageError(err)
}

func (s *fsStorage) WriteFile(path string, data []byte) error {
	absPath, err := s.resolvePath(path)
	if err != nil {
		return classifyStorageError(err)
	}

	return classifyStorageError(s.withPathLock(absPath, func() error {
		return s.writeFileUnlocked(absPath, data)
	}))
}

func (s *fsStorage) DeleteFile(path string) error {
	absPath, err := s.resolvePath(path)
	if err != nil {
		return classifyStorageError(err)
	}

	return classifyStorageError(s.withPathLock(absPath, func() error {
		exists, err := s.fileExistsUnlocked(absPath)
		if err != nil {
			return err
		}
		if !exists {
			return fmt.Errorf("delete file %q: %w", absPath, os.ErrNotExist)
		}
		if err := s.removeFileFn(absPath); err != nil {
			return fmt.Errorf("delete file %q: %w", absPath, err)
		}
		return nil
	}))
}

func (s *fsStorage) ListFiles() ([]string, error) {
	files := make([]string, 0)
	err := filepath.WalkDir(s.storageRoot, func(path string, d os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if d.IsDir() {
			return nil
		}
		if strings.HasSuffix(d.Name(), newFileSuffix) || strings.HasSuffix(d.Name(), oldFileSuffix) {
			return nil
		}

		relPath, err := filepath.Rel(s.storageRoot, path)
		if err != nil {
			return fmt.Errorf("resolve relative path for %q: %w", path, err)
		}
		files = append(files, filepath.Clean(relPath))
		return nil
	})
	if err != nil {
		return nil, classifyStorageError(fmt.Errorf("list files under %q: %w", s.storageRoot, err))
	}

	slices.Sort(files)
	return files, nil
}

func (s *fsStorage) StateExists() (bool, error) {
	exists, _, err := s.FileExists(stateFileName)
	return exists, classifyStorageError(err)
}

func (s *fsStorage) LoadState() (client.State, error) {
	data, err := s.ReadFile(stateFileName)
	if err != nil {
		return client.State{}, classifyStorageError(err)
	}
	state, err := unmarshalState(data)
	return state, classifyStorageError(err)
}

func (s *fsStorage) SaveState(state client.State) error {
	data, err := marshalState(state)
	if err != nil {
		return classifyStorageError(err)
	}
	return classifyStorageError(s.WriteFile(stateFileName, data))
}

func (s *fsStorage) loadReportSync(id client.GameID, turn uint) (report.Report, error) {
	gameData, err := s.loadGameDataSync(id, turn)
	if err != nil {
		return report.Report{}, classifyStorageError(err)
	}
	return gameData.Report, nil
}

func (s *fsStorage) saveReportSync(id client.GameID, turn uint, rep report.Report) error {
	absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
	if err != nil {
		return classifyStorageError(err)
	}

	return classifyStorageError(s.withPathLock(absPath, func() error {
		gameData, err := s.loadGameDataUnlocked(absPath)
		if err != nil {
			if !errors.Is(err, os.ErrNotExist) {
				return err
			}
			gameData = client.GameData{Turn: turn}
		}

		gameData.Turn = turn
		gameData.Report = rep
		return s.writeGameDataUnlocked(absPath, gameData)
	}))
}

func (s *fsStorage) loadOrderSync(id client.GameID, turn uint) (order.UserGamesOrder, error) {
	gameData, err := s.loadGameDataSync(id, turn)
	if err != nil {
		return order.UserGamesOrder{}, classifyStorageError(err)
	}
	if gameData.Order == nil {
		return order.UserGamesOrder{}, classifyStorageError(fmt.Errorf("load order for game %q turn %d: %w", id, turn, os.ErrNotExist))
	}
	return *gameData.Order, nil
}

func (s *fsStorage) saveOrderSync(id client.GameID, turn uint, o order.UserGamesOrder) error {
	absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
	if err != nil {
		return classifyStorageError(err)
	}

	return classifyStorageError(s.withPathLock(absPath, func() error {
		gameData, err := s.loadGameDataUnlocked(absPath)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				return fmt.Errorf("save order for game %q turn %d: %w", id, turn, os.ErrNotExist)
			}
			return err
		}

		gameData.Turn = turn
		gameData.Order = &o
		return s.writeGameDataUnlocked(absPath, gameData)
	}))
}

func (s *fsStorage) gameDataExistsSync(id client.GameID, turn uint) (bool, error) {
	absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
	if err != nil {
		return false, classifyStorageError(err)
	}

	exists, err := s.fileExistsUnlocked(absPath)
	if err != nil {
		return false, classifyStorageError(err)
	}

	return exists, nil
}

func (s *fsStorage) loadGameDataSync(id client.GameID, turn uint) (client.GameData, error) {
	absPath, err := s.resolvePath(gameTurnFilePath(id, turn))
	if err != nil {
		return client.GameData{}, classifyStorageError(err)
	}

	var gameData client.GameData
	err = s.withPathLock(absPath, func() error {
		var opErr error
		gameData, opErr = s.loadGameDataUnlocked(absPath)
		return opErr
	})
	return gameData, classifyStorageError(err)
}

func (s *fsStorage) loadGameDataUnlocked(absPath string) (client.GameData, error) {
	data, err := s.readFileUnlocked(absPath)
	if err != nil {
		return client.GameData{}, err
	}
	return unmarshalGameData(data)
}

func (s *fsStorage) writeGameDataUnlocked(absPath string, gameData client.GameData) error {
	data, err := marshalGameData(gameData)
	if err != nil {
		return err
	}
	return s.writeFileUnlocked(absPath, data)
}

func marshalState(state client.State) ([]byte, error) {
	return marshalJSON(state)
}

func unmarshalState(data []byte) (client.State, error) {
	var state client.State
	if err := unmarshalJSON(data, &state); err != nil {
		return client.State{}, err
	}
	return state, nil
}

func marshalGameData(gameData client.GameData) ([]byte, error) {
	stored, err := makeStoredGameData(gameData)
	if err != nil {
		return nil, err
	}
	return marshalJSON(stored)
}

func unmarshalGameData(data []byte) (client.GameData, error) {
	var stored storedGameData
	if err := unmarshalJSON(data, &stored); err != nil {
		return client.GameData{}, err
	}
	return stored.toGameData()
}

func marshalJSON(value any) ([]byte, error) {
	data, err := json.Marshal(value)
	if err != nil {
		return nil, fmt.Errorf("marshal json: %w", err)
	}
	return data, nil
}

func unmarshalJSON(data []byte, target any) error {
	if err := json.Unmarshal(data, target); err != nil {
		return fmt.Errorf("unmarshal json: %w", err)
	}
	return nil
}

func makeStoredGameData(gameData client.GameData) (storedGameData, error) {
	stored := storedGameData{
		Turn:   gameData.Turn,
		Report: gameData.Report,
	}
	if gameData.Order == nil {
		return stored, nil
	}

	storedOrder, err := makeStoredOrder(*gameData.Order)
	if err != nil {
		return storedGameData{}, err
	}
	stored.Order = &storedOrder
	return stored, nil
}

func (d storedGameData) toGameData() (client.GameData, error) {
	gameData := client.GameData{
		Turn:   d.Turn,
		Report: d.Report,
	}
	if d.Order == nil {
		return gameData, nil
	}

	o, err := d.Order.toOrder()
	if err != nil {
		return client.GameData{}, err
	}
	gameData.Order = o
	return gameData, nil
}

func makeStoredOrder(o order.UserGamesOrder) (storedOrder, error) {
	result := storedOrder{
		GameID:    o.GameID,
		UpdatedAt: o.UpdatedAt,
		Commands:  make([]json.RawMessage, len(o.Commands)),
	}
	for i := range o.Commands {
		data, err := marshalJSON(o.Commands[i])
		if err != nil {
			return storedOrder{}, fmt.Errorf("marshal order command %d: %w", i, err)
		}
		result.Commands[i] = data
	}
	return result, nil
}

func (o *storedOrder) toOrder() (*order.UserGamesOrder, error) {
	if o == nil {
		return nil, nil
	}

	result := &order.UserGamesOrder{
		GameID:    o.GameID,
		UpdatedAt: o.UpdatedAt,
		Commands:  make([]order.DecodableCommand, len(o.Commands)),
	}
	for i := range o.Commands {
		cmd, err := parseOrderCommand(o.Commands[i])
		if err != nil {
			return nil, fmt.Errorf("decode order command %d: %w", i, err)
		}
		result.Commands[i] = cmd
	}
	return result, nil
}

func parseOrderCommand(data json.RawMessage) (order.DecodableCommand, error) {
	meta := new(order.CommandMeta)
	if err := json.Unmarshal(data, meta); err != nil {
		return nil, fmt.Errorf("decode order command metadata: %w", err)
	}

	switch meta.CmdType {
	case order.CommandTypeRaceQuit:
		return decodeOrderCommand(data, new(order.CommandRaceQuit))
	case order.CommandTypeRaceVote:
		return decodeOrderCommand(data, new(order.CommandRaceVote))
	case order.CommandTypeRaceRelation:
		return decodeOrderCommand(data, new(order.CommandRaceRelation))
	case order.CommandTypeShipClassCreate:
		return decodeOrderCommand(data, new(order.CommandShipClassCreate))
	case order.CommandTypeShipClassMerge:
		return decodeOrderCommand(data, new(order.CommandShipClassMerge))
	case order.CommandTypeShipClassRemove:
		return decodeOrderCommand(data, new(order.CommandShipClassRemove))
	case order.CommandTypeShipGroupBreak:
		return decodeOrderCommand(data, new(order.CommandShipGroupBreak))
	case order.CommandTypeShipGroupLoad:
		return decodeOrderCommand(data, new(order.CommandShipGroupLoad))
	case order.CommandTypeShipGroupUnload:
		return decodeOrderCommand(data, new(order.CommandShipGroupUnload))
	case order.CommandTypeShipGroupSend:
		return decodeOrderCommand(data, new(order.CommandShipGroupSend))
	case order.CommandTypeShipGroupUpgrade:
		return decodeOrderCommand(data, new(order.CommandShipGroupUpgrade))
	case order.CommandTypeShipGroupMerge:
		return decodeOrderCommand(data, new(order.CommandShipGroupMerge))
	case order.CommandTypeShipGroupDismantle:
		return decodeOrderCommand(data, new(order.CommandShipGroupDismantle))
	case order.CommandTypeShipGroupTransfer:
		return decodeOrderCommand(data, new(order.CommandShipGroupTransfer))
	case order.CommandTypeShipGroupJoinFleet:
		return decodeOrderCommand(data, new(order.CommandShipGroupJoinFleet))
	case order.CommandTypeFleetMerge:
		return decodeOrderCommand(data, new(order.CommandFleetMerge))
	case order.CommandTypeFleetSend:
		return decodeOrderCommand(data, new(order.CommandFleetSend))
	case order.CommandTypeScienceCreate:
		return decodeOrderCommand(data, new(order.CommandScienceCreate))
	case order.CommandTypeScienceRemove:
		return decodeOrderCommand(data, new(order.CommandScienceRemove))
	case order.CommandTypePlanetRename:
		return decodeOrderCommand(data, new(order.CommandPlanetRename))
	case order.CommandTypePlanetProduce:
		return decodeOrderCommand(data, new(order.CommandPlanetProduce))
	case order.CommandTypePlanetRouteSet:
		return decodeOrderCommand(data, new(order.CommandPlanetRouteSet))
	case order.CommandTypePlanetRouteRemove:
		return decodeOrderCommand(data, new(order.CommandPlanetRouteRemove))
	default:
		return nil, fmt.Errorf("unknown order command type %q", meta.CmdType)
	}
}

func decodeOrderCommand[T order.DecodableCommand](data json.RawMessage, target T) (T, error) {
	if err := json.Unmarshal(data, target); err != nil {
		return target, err
	}
	return target, nil
}

func (s *fsStorage) resolvePath(path string) (string, error) {
	relPath, err := normalizeRelativePath(path)
	if err != nil {
		return "", err
	}
	return filepath.Join(s.storageRoot, relPath), nil
}

func normalizeRelativePath(path string) (string, error) {
	path = strings.ReplaceAll(path, "\\", string(filepath.Separator))
	path = strings.TrimLeft(path, string(filepath.Separator))
	path = filepath.Clean(path)

	switch {
	case path == "." || path == "":
		return "", errors.New("path must not be empty")
	case filepath.IsAbs(path):
		return "", fmt.Errorf("path %q must be relative", path)
	case filepath.VolumeName(path) != "":
		return "", fmt.Errorf("path %q must not include a volume name", path)
	case path == "..":
		return "", fmt.Errorf("path %q escapes storage root", path)
	case strings.HasPrefix(path, ".."+string(filepath.Separator)):
		return "", fmt.Errorf("path %q escapes storage root", path)
	}

	return path, nil
}

func gameTurnFilePath(id fmt.Stringer, turn uint) string {
	return filepath.Join(id.String(), strconv.FormatUint(uint64(turn), 10)+gameDataFileSuffix)
}

func (s *fsStorage) withPathLock(absPath string, fn func() error) error {
	lock := s.acquirePathLock(absPath)
	defer s.releasePathLock(absPath)

	lock.mu.Lock()
	defer lock.mu.Unlock()

	return fn()
}

func (s *fsStorage) acquirePathLock(absPath string) *pathLock {
	s.locksMu.Lock()
	defer s.locksMu.Unlock()

	lock, ok := s.locks[absPath]
	if !ok {
		lock = &pathLock{}
		s.locks[absPath] = lock
	}
	lock.refs++
	return lock
}

func (s *fsStorage) releasePathLock(absPath string) {
	s.locksMu.Lock()
	defer s.locksMu.Unlock()

	lock, ok := s.locks[absPath]
	if !ok {
		return
	}
	lock.refs--
	if lock.refs == 0 {
		delete(s.locks, absPath)
	}
}

func (s *fsStorage) fileExistsUnlocked(absPath string) (bool, error) {
	ok, err := util.FileExists(absPath)
	if err != nil {
		return false, fmt.Errorf("check file %q exists: %w", absPath, err)
	}
	return ok, nil
}

func (s *fsStorage) readFileUnlocked(absPath string) ([]byte, error) {
	data, err := s.readFileFn(absPath)
	if err != nil {
		return nil, fmt.Errorf("read file %q: %w", absPath, err)
	}
	return data, nil
}

func (s *fsStorage) writeFileUnlocked(absPath string, data []byte) error {
	if err := s.ensureParentDir(absPath); err != nil {
		return err
	}

	targetExists, err := s.fileExistsUnlocked(absPath)
	if err != nil {
		return err
	}

	oldPath := absPath + oldFileSuffix
	oldExists, err := s.fileExistsUnlocked(oldPath)
	if err != nil {
		return err
	}
	if oldExists {
		return fmt.Errorf("write file %q: old file already exists at %q", absPath, oldPath)
	}

	newPath := absPath + newFileSuffix
	newExists, err := s.fileExistsUnlocked(newPath)
	if err != nil {
		return err
	}
	if newExists {
		return fmt.Errorf("write file %q: new file already exists at %q", absPath, newPath)
	}

	if err := s.writeFileFn(newPath, data, defaultFilePerm); err != nil {
		return fmt.Errorf("write new file %q: %w", newPath, err)
	}

	if targetExists {
		if err := s.renameFileFn(absPath, oldPath); err != nil {
			return errors.Join(
				fmt.Errorf("rename file %q to %q: %w", absPath, oldPath, err),
				s.cleanupTempFile(newPath),
			)
		}
	}

	if err := s.renameFileFn(newPath, absPath); err != nil {
		var restoreErr error
		if targetExists {
			restoreErr = s.renameFileFn(oldPath, absPath)
			if restoreErr != nil {
				restoreErr = fmt.Errorf("restore file %q from %q: %w", absPath, oldPath, restoreErr)
			}
		}
		return errors.Join(
			fmt.Errorf("rename new file %q to %q: %w", newPath, absPath, err),
			restoreErr,
			s.cleanupTempFile(newPath),
		)
	}

	if !targetExists {
		return nil
	}
	if err := s.removeFileFn(oldPath); err != nil {
		return fmt.Errorf("remove old file %q: %w", oldPath, err)
	}
	return nil
}

func (s *fsStorage) cleanupTempFile(path string) error {
	if err := s.removeFileFn(path); err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("remove temp file %q: %w", path, err)
	}
	return nil
}

func (s *fsStorage) ensureParentDir(absPath string) error {
	parentDir := filepath.Dir(absPath)
	if err := os.MkdirAll(parentDir, os.ModePerm); err != nil {
		return fmt.Errorf("create parent directory %q: %w", parentDir, err)
	}
	return nil
}

func classifyStorageError(err error) error {
	if err == nil {
		return nil
	}
	if gerr.IsStorage(err) {
		return err
	}
	return gerr.WrapStorage(err)
}
