package controller

import (
	"errors"
	"time"

	"galaxy/game/internal/model/game"

	"github.com/google/uuid"

	"galaxy/model/order"
	"galaxy/model/report"

	"galaxy/game/internal/repo"
)

type Configurer func(*Param)

type Repo interface {
	// Lock must be called before any repository operations
	Lock() error

	// Release must be called after first and only repository operation
	Release() error

	// SaveTurn stores just generated new turn
	SaveNewTurn(uint, *game.Game) error

	// SaveState stores current game state updated between turns
	SaveLastState(*game.Game) error

	// LoadState retrieves game current state with required lock acquisition
	LoadState() (*game.Game, error)

	// LoadStateSafe retrieves game current state without preliminary locking
	LoadStateSafe() (*game.Game, error)

	// SaveBattle stores a new battle protocol and battle meta data for turn t
	SaveBattle(uint, *report.BattleReport, *game.BattleMeta) error

	// LoadBattle reads battle's protocol for turn t and battle id.
	// Returns false if battle with such id was never stored at turn t
	LoadBattle(t uint, id uuid.UUID) (*report.BattleReport, bool, error)

	// SaveBombing stores all prodused bombings for turn t
	SaveBombings(uint, []*game.Bombing) error

	// SaveReport stores latest report for a race
	SaveReport(uint, *report.Report) error

	// LoadReport loads report for specific turn and player id
	LoadReport(uint, uuid.UUID) (*report.Report, error)

	// SaveOrder stores order for given turn
	SaveOrder(uint, uuid.UUID, *order.UserGamesOrder) error

	// LoadOrder loads order for specific turn and player id
	LoadOrder(uint, uuid.UUID) (*order.UserGamesOrder, bool, error)
}

type Ctrl interface {
	ValidateOrder(actor string, cmd ...order.DecodableCommand) error
	// remove below funcs if /command api will be deleted
	RaceID(actor string) (uuid.UUID, error)
	RaceQuit(actor string) error
	RaceVote(actor, acceptor string) error
	RaceRelation(actor, acceptor string, rel string) error
	ShipClassCreate(actor, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error
	ShipClassMerge(actor, name, targetName string) error
	ShipClassRemove(actor, typeName string) error
	ShipGroupLoad(actor string, groupID uuid.UUID, cargoType string, quantity float64) error
	ShipGroupUnload(actor string, groupID uuid.UUID, quantity float64) error
	ShipGroupSend(actor string, groupID uuid.UUID, planetNumber uint) error
	ShipGroupUpgrade(actor string, groupID uuid.UUID, techInput string, limitLevel float64) error
	ShipGroupBreak(actor string, groupID, newID uuid.UUID, quantity uint) error
	ShipGroupMerge(actor string) error
	ShipGroupDismantle(actor string, groupID uuid.UUID) error
	ShipGroupTransfer(actor, acceptor string, groupID uuid.UUID) error
	ShipGroupJoinFleet(actor, fleetName string, groupID uuid.UUID) error
	FleetMerge(actor, fleetSourceName, fleetTargetName string) error
	FleetSend(actor, fleetName string, planetNumber uint) error
	ScienceCreate(actor, typeName string, drive, weapons, shields, cargo float64) error
	ScienceRemove(actor, typeName string) error
	PlanetRename(actor string, planetNumber int, typeName string) error
	PlanetProduce(actor string, planetNumber int, prodType, subject string) error
	PlanetRouteSet(actor, loadType string, origin, destination uint) error
	PlanetRouteRemove(actor, loadType string, origin uint) error
}

func GenerateGame(configure func(*Param), races []string) (s game.State, err error) {
	ec, err := NewRepoController(configure)
	if err != nil {
		return game.State{}, err
	}
	if err = ec.Repo.Lock(); err != nil {
		return
	}
	defer func() {
		err = errors.Join(err, ec.Repo.Release())
		if err == nil {
			s, err = GameState(configure)
		}
	}()

	_, err = NewGame(ec.Repo, races)
	return
}

func GenerateTurn(configure func(*Param)) (err error) {
	ec, err := NewRepoController(configure)
	if err != nil {
		return err
	}
	err = ec.executeLocked(func(c *Controller) error { return c.MakeTurn() })
	return
}

func LoadReport(configure func(*Param), actor string, turn uint) (*report.Report, error) {
	ec, err := NewRepoController(configure)
	if err != nil {
		return nil, err
	}
	return ec.loadReport(actor, turn)
}

func ExecuteCommand(configure func(*Param), consumer func(c Ctrl) error) (err error) {
	ec, err := NewRepoController(configure)
	if err != nil {
		return err
	}
	return ec.executeCommand(func(c *Controller) error { return consumer(c) })
}

func ValidateOrder(configure func(*Param), actor string, cmd ...order.DecodableCommand) (*order.UserGamesOrder, error) {
	ec, err := NewRepoController(configure)
	if err != nil {
		return nil, err
	}
	return ec.validateOrder(actor, cmd...)
}

func FetchOrder(configure func(*Param), actor string, turn uint) (order *order.UserGamesOrder, ok bool, err error) {
	ec, err := NewRepoController(configure)
	if err != nil {
		return nil, false, err
	}
	return ec.fetchOrder(actor, turn)
}

func FetchBattle(configure func(*Param), turn uint, ID uuid.UUID) (b *report.BattleReport, exists bool, err error) {
	ec, err := NewRepoController(configure)
	if err != nil {
		return nil, false, err
	}
	return ec.fetchBattle(turn, ID)
}

func BanishRace(configure func(*Param), actor string) error {
	ec, err := NewRepoController(configure)
	if err != nil {
		return err
	}
	return ec.banishRace(actor)
}

func GameState(configure func(*Param)) (s game.State, err error) {
	ec, err := NewRepoController(configure)
	if err != nil {
		return game.State{}, err
	}

	g, err := ec.Repo.LoadStateSafe()
	if err != nil {
		return game.State{}, err
	}

	result := &game.State{
		ID:       g.ID,
		Turn:     g.Turn,
		Stage:    g.Stage,
		Finished: g.Finished(),
		Players:  make([]game.PlayerState, len(g.Race)),
	}

	planetCount := make(map[uuid.UUID]uint)
	population := make(map[uuid.UUID]game.Float)

	for i := range g.Map.Planet {
		p := &g.Map.Planet[i]
		if p.Owner == nil {
			continue
		}
		owner := *p.Owner
		planetCount[owner] += 1
		population[owner] += p.Population
	}

	for i := range g.Race {
		r := &g.Race[i]
		result.Players[i].ID = r.ID
		result.Players[i].RaceName = r.Name
		result.Players[i].Planets = planetCount[r.ID]
		result.Players[i].Population = population[r.ID]
		result.Players[i].Extinct = r.Extinct
	}

	return *result, nil
}

type RepoController struct {
	Repo Repo
}

func NewRepoController(config Configurer) (*RepoController, error) {
	c := &Param{
		StoragePath: ".",
	}
	if config != nil {
		config(c)
	}
	r, err := repo.NewFileRepo(c.StoragePath)
	if err != nil {
		return nil, err
	}
	return &RepoController{
		Repo: r,
	}, nil
}

func (ec *RepoController) NewGameController(g *game.Game) *Controller {
	return &Controller{
		RepoController: ec,
		Cache:          NewCache(g),
	}
}

func (ec *RepoController) validateOrder(actor string, cmd ...order.DecodableCommand) (o *order.UserGamesOrder, err error) {
	err = ec.executeSafe(func(t uint, c *Controller) error {
		id, err := c.RaceID(actor)
		if err != nil {
			return err
		}
		err = c.ValidateOrder(actor, cmd...)
		if err != nil {
			return err
		}
		o = &order.UserGamesOrder{
			GameID:    c.Cache.g.ID,
			UpdatedAt: time.Now().UTC().UnixMilli(),
			Commands:  make([]order.DecodableCommand, len(cmd)),
		}
		copy(o.Commands, cmd)
		return ec.Repo.SaveOrder(t, id, o)
	})
	if err != nil {
		return nil, err
	}
	return
}

func (ec *RepoController) fetchOrder(actor string, turn uint) (order *order.UserGamesOrder, ok bool, err error) {
	err = ec.executeSafe(func(t uint, c *Controller) error {
		id, err := c.RaceID(actor)
		if err != nil {
			return err
		}
		order, ok, err = ec.Repo.LoadOrder(turn, id)
		return err
	})
	if err != nil {
		return
	}
	return
}

func (ec *RepoController) fetchBattle(turn uint, ID uuid.UUID) (order *report.BattleReport, exists bool, err error) {
	err = ec.executeSafe(func(t uint, c *Controller) error {
		order, exists, err = ec.Repo.LoadBattle(turn, ID)
		return err
	})
	return
}

func (ec *RepoController) loadReport(actor string, turn uint) (r *report.Report, err error) {
	execErr := ec.executeSafe(func(t uint, c *Controller) (exErr error) {
		id, exErr := c.RaceID(actor)
		if exErr == nil {
			r, exErr = ec.Repo.LoadReport(turn, id)
		}
		return
	})
	err = errors.Join(err, execErr)
	return
}

func (ec *RepoController) executeCommand(consumer func(*Controller) error) (err error) {
	return ec.executeLocked(func(c *Controller) error {
		err = consumer(c)
		if err == nil {
			c.Cache.StageCommand()
			err = c.saveState()
		}
		return err
	})
}

func (ec *RepoController) banishRace(actor string) (err error) {
	return ec.executeLocked(func(c *Controller) error {
		err = c.RaceBanish(actor)
		if err != nil {
			return err
		}
		return c.saveState()
	})
}

func (ec *RepoController) executeSafe(consumer func(uint, *Controller) error) (err error) {
	g, err := ec.Repo.LoadStateSafe()
	if err != nil {
		return err
	}

	err = consumer(g.Turn, ec.NewGameController(g))
	return
}

func (ec *RepoController) executeLocked(consumer func(*Controller) error) (err error) {
	if err := ec.Repo.Lock(); err != nil {
		return err
	}
	defer func() {
		err = errors.Join(err, ec.Repo.Release())
	}()

	g, err := ec.Repo.LoadState()
	if err != nil {
		return err
	}

	err = consumer(ec.NewGameController(g))
	return
}

func (c *Controller) saveState() error {
	return c.Repo.SaveLastState(c.Cache.g)
}

type Controller struct {
	*RepoController
	Cache *Cache
}

type Param struct {
	StoragePath string
}
