package controller

import (
	"errors"
	"fmt"

	"galaxy/model/order"

	e "galaxy/error"

	"galaxy/game/internal/model/game"

	"github.com/google/uuid"
)

func (c *Controller) ValidateOrder(actor string, commands ...order.DecodableCommand) (err error) {
	for i := range commands {
		if _, ok := commands[i].(order.CommandRaceQuit); ok && i != len(commands)-1 {
			err = e.NewQuitCommandFollowedByCommandError()
		}
		if err != nil {
			return err
		}
		err = errors.Join(err, c.applyCommand(actor, commands[i]))
	}
	return
}

func (c *Controller) applyCommand(actor string, cmd order.DecodableCommand) (err error) {
	var m *order.CommandMeta
	if v, ok := order.AsCommand[*order.CommandRaceQuit](cmd); ok {
		m = &v.CommandMeta
		err = c.RaceQuit(actor)
	} else if v, ok := order.AsCommand[*order.CommandRaceVote](cmd); ok {
		m = &v.CommandMeta
		err = c.RaceVote(actor, v.Acceptor)
	} else if v, ok := order.AsCommand[*order.CommandRaceRelation](cmd); ok {
		m = &v.CommandMeta
		err = c.RaceRelation(actor, v.Acceptor, v.Relation)
	} else if v, ok := order.AsCommand[*order.CommandShipClassCreate](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipClassCreate(actor, v.Name, v.Drive, int(v.Armament), v.Weapons, v.Shields, v.Cargo)
	} else if v, ok := order.AsCommand[*order.CommandShipClassMerge](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipClassMerge(actor, v.Name, v.Target)
	} else if v, ok := order.AsCommand[*order.CommandShipClassRemove](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipClassRemove(actor, v.Name)
	} else if v, ok := order.AsCommand[*order.CommandShipGroupLoad](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupLoad(actor, uuid.MustParse(v.ID), v.Cargo, v.Quantity)
	} else if v, ok := order.AsCommand[*order.CommandShipGroupUnload](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupUnload(actor, uuid.MustParse(v.ID), v.Quantity)
	} else if v, ok := order.AsCommand[*order.CommandShipGroupSend](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupSend(actor, uuid.MustParse(v.ID), uint(v.Destination))
	} else if v, ok := order.AsCommand[*order.CommandShipGroupUpgrade](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupUpgrade(actor, uuid.MustParse(v.ID), v.Tech, v.Level)
	} else if v, ok := order.AsCommand[*order.CommandShipGroupMerge](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupMerge(actor)
	} else if v, ok := order.AsCommand[*order.CommandShipGroupBreak](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupBreak(actor, uuid.MustParse(v.ID), uuid.MustParse(v.NewID), uint(v.Quantity))
	} else if v, ok := order.AsCommand[*order.CommandShipGroupDismantle](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupDismantle(actor, uuid.MustParse(v.ID))
	} else if v, ok := order.AsCommand[*order.CommandShipGroupTransfer](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupTransfer(actor, v.Acceptor, uuid.MustParse(v.ID))
	} else if v, ok := order.AsCommand[*order.CommandShipGroupJoinFleet](cmd); ok {
		m = &v.CommandMeta
		err = c.ShipGroupJoinFleet(actor, v.Name, uuid.MustParse(v.ID))
	} else if v, ok := order.AsCommand[*order.CommandFleetMerge](cmd); ok {
		m = &v.CommandMeta
		err = c.FleetMerge(actor, v.Name, v.Target)
	} else if v, ok := order.AsCommand[*order.CommandFleetSend](cmd); ok {
		m = &v.CommandMeta
		err = c.FleetSend(actor, v.Name, uint(v.Destination))
	} else if v, ok := order.AsCommand[*order.CommandScienceCreate](cmd); ok {
		m = &v.CommandMeta
		err = c.ScienceCreate(actor, v.Name, v.Drive, v.Weapons, v.Shields, v.Cargo)
	} else if v, ok := order.AsCommand[*order.CommandScienceRemove](cmd); ok {
		m = &v.CommandMeta
		err = c.ScienceRemove(actor, v.Name)
	} else if v, ok := order.AsCommand[*order.CommandPlanetRename](cmd); ok {
		m = &v.CommandMeta
		err = c.PlanetRename(actor, v.Number, v.Name)
	} else if v, ok := order.AsCommand[*order.CommandPlanetProduce](cmd); ok {
		m = &v.CommandMeta
		err = c.PlanetProduce(actor, v.Number, v.Production, v.Subject)
	} else if v, ok := order.AsCommand[*order.CommandPlanetRouteSet](cmd); ok {
		m = &v.CommandMeta
		err = c.PlanetRouteSet(actor, v.LoadType, uint(v.Origin), uint(v.Destination))
	} else if v, ok := order.AsCommand[*order.CommandPlanetRouteRemove](cmd); ok {
		m = &v.CommandMeta
		err = c.PlanetRouteRemove(actor, v.LoadType, uint(v.Origin))
	} else {
		return e.NewUnrecognizedCommandError(cmd.CommandType().String())
	}

	if ge, ok := errors.AsType[*e.GenericError](err); ok {
		m.Result(ge.Code)
	} else if err != nil {
		panic(fmt.Errorf("error applying command has unknown origin: %w", err))
	} else {
		m.Result(0)
	}

	return
}

func (c *Controller) applyOrders(t uint) error {
	raceOrder := make(map[int][]order.DecodableCommand)
	raceOrderUpdated := make(map[int]int64)
	commandRace := make(map[string]string)
	challenge := make(map[string]*order.CommandShipGroupUnload)
	cmdApplied := make(map[string]bool)

	for ri := range c.Cache.listRaceActingIdx() {
		o, ok, err := c.Repo.LoadOrder(t, c.Cache.g.Race[ri].ID)
		if err != nil {
			return err
		}
		if !ok {
			continue
		}
		raceOrder[ri] = o.Commands
		raceOrderUpdated[ri] = o.UpdatedAt
		for i := range o.Commands {
			commandRace[o.Commands[i].CommandID()] = c.Cache.g.Race[ri].Name
			if v, ok := order.AsCommand[*order.CommandShipGroupUnload](o.Commands[i]); ok {
				if _, ok := challenge[v.ID]; ok {
					panic(fmt.Sprintf("unload command %s already cached", v.ID))
				}
				if ok, err := c.shouldChallenge(v); err != nil {
					return err
				} else if ok {
					challenge[v.ID] = v
				}
			}
		}
	}

	for _, cmdID := range c.challengeUnload(challenge) {
		if err := c.applyCommand(commandRace[cmdID], challenge[cmdID]); err == nil {
			cmdApplied[cmdID] = true
		}
	}

	for ri := range raceOrder {
		for _, cmd := range raceOrder[ri] {
			if v, ok := cmdApplied[cmd.CommandID()]; ok && v {
				continue
			}
			// any command might fail due to challenged planets colonization
			_ = c.applyCommand(commandRace[cmd.CommandID()], cmd)
		}
		// re-save order to persist possible changed commands result outcome
		if err := c.Repo.SaveOrder(t, c.Cache.g.Race[ri].ID, &order.UserGamesOrder{
			GameID:    c.Cache.g.ID,
			UpdatedAt: raceOrderUpdated[ri],
			Commands:  raceOrder[ri],
		}); err != nil {
			return err
		}
	}

	return nil
}

func (c *Controller) shouldChallenge(cmd *order.CommandShipGroupUnload) (resut bool, err error) {
	sgi, ok := c.Cache.shipGroupIndexByID(uuid.MustParse(cmd.ID))
	if !ok {
		err = e.NewGameStateError("challenge group unload: group not found: %v", cmd.ID)
		return
	}
	sg := c.Cache.ShipGroup(sgi)
	pn, ok := sg.AtPlanet()
	if !ok || sg.CargoType == nil {
		return false, nil
	}
	p := c.Cache.MustPlanet(pn)
	if p.Owned() || *sg.CargoType != game.CargoColonist {
		return false, nil
	}

	return true, nil
}

func (c *Controller) challengeUnload(challenge map[string]*order.CommandShipGroupUnload) []string {
	if len(challenge) == 0 {
		return nil
	}
	planetRaceQuantity := make(map[uint]map[int]float64, 0)
	raceCommand := make(map[uint]map[int][]string)
	for cmdID, cmd := range challenge {
		sgi, ok := c.Cache.shipGroupIndexByID(uuid.MustParse(cmd.ID))
		if !ok {
			panic(fmt.Sprintf("challenge group unload: group not found: %v", cmd.ID))
		}
		sg := c.Cache.ShipGroup(sgi)
		ri := c.Cache.ShipGroupOwnerRaceIndex(sgi)
		pn, ok := sg.AtPlanet()
		if _, ok := raceCommand[pn]; !ok {
			raceCommand[pn] = make(map[int][]string)
		}
		raceCommand[pn][ri] = append(raceCommand[pn][ri], cmdID)
		if _, ok := planetRaceQuantity[pn]; !ok {
			planetRaceQuantity[pn] = make(map[int]float64)
		}
		planetRaceQuantity[pn][ri] = planetRaceQuantity[pn][ri] + UnloadCargoRequest(float64(sg.Load), cmd.Quantity)
	}

	result := make([]string, 0)
	for pn := range planetRaceQuantity {
		if len(planetRaceQuantity[pn]) < 2 {
			continue
		}
		winner := MaxOrRandomLoadId(planetRaceQuantity[pn], func(ri int) float64 { return float64(c.Cache.g.Race[ri].Votes) })
		result = append(result, raceCommand[pn][winner]...)
	}
	return result
}
