// Package booking provides the use-case of booking a shipping. Used by views
// facing an administrator.
package booking

import (
	"errors"
	"time"

	shipping "github.com/longjoy/micro-go-course/section19/cargo/model"
)

// ErrInvalidArgument is returned when one or more arguments are invalid.
var ErrInvalidArgument = errors.New("invalid argument")

// Service is the interface that provides booking methods.
type Service interface {
	// BookNewCargo registers a new cargo in the tracking system, not yet
	// routed.
	BookNewCargo(origin shipping.UNLocode, destination shipping.UNLocode, deadline time.Time) (shipping.TrackingID, error)

	// LoadCargo returns a read model of a shipping.
	LoadCargo(id shipping.TrackingID) (Cargo, error)

	// RequestPossibleRoutesForCargo requests a list of itineraries describing
	// possible routes for this shipping.
	RequestPossibleRoutesForCargo(id shipping.TrackingID) []shipping.Itinerary

	// AssignCargoToRoute assigns a cargo to the route specified by the
	// itinerary.
	AssignCargoToRoute(id shipping.TrackingID, itinerary shipping.Itinerary) (bool, error)

	// ChangeDestination changes the destination of a shipping.
	ChangeDestination(id shipping.TrackingID, destination shipping.UNLocode) (bool, error)

	// Cargos returns a list of all cargos that have been booked.
	Cargos() []Cargo

	// Locations returns a list of registered locations.
	Locations() []Location
}

type service struct {
	cargos         shipping.CargoRepository
	locations      shipping.LocationRepository
	handlingEvents shipping.HandlingEventRepository
	routingService shipping.RoutingService
}

func (s *service) AssignCargoToRoute(id shipping.TrackingID, itinerary shipping.Itinerary) (bool, error) {
	if id == "" || len(itinerary.Legs) == 0 {
		return false, ErrInvalidArgument
	}

	c, err := s.cargos.Find(id)
	if err != nil {
		return false, err
	}

	c.AssignToRoute(itinerary)

	return s.cargos.Store(c)
}

func (s *service) BookNewCargo(origin, destination shipping.UNLocode, deadline time.Time) (shipping.TrackingID, error) {
	if origin == "" || destination == "" || deadline.IsZero() {
		return "", ErrInvalidArgument
	}

	id := shipping.NextTrackingID()
	rs := shipping.RouteSpecification{
		Origin:          origin,
		Destination:     destination,
		ArrivalDeadline: deadline,
	}

	c := shipping.NewCargo(id, rs)

	if _, err := s.cargos.Store(c); err != nil {
		return "", err
	}

	return c.TrackingID, nil
}

func (s *service) LoadCargo(id shipping.TrackingID) (Cargo, error) {
	if id == "" {
		return Cargo{}, ErrInvalidArgument
	}

	c, err := s.cargos.Find(id)
	if err != nil {
		return Cargo{}, err
	}

	return assemble(c, s.handlingEvents), nil
}

func (s *service) ChangeDestination(id shipping.TrackingID, destination shipping.UNLocode) (bool, error) {
	if id == "" || destination == "" {
		return false, ErrInvalidArgument
	}

	c, err := s.cargos.Find(id)
	if err != nil {
		return false, err
	}

	l, err := s.locations.Find(destination)
	if err != nil {
		return false, err
	}

	c.SpecifyNewRoute(shipping.RouteSpecification{
		Origin:          c.Origin,
		Destination:     l.UNLocode,
		ArrivalDeadline: c.RouteSpecification.ArrivalDeadline,
	})

	if _, err := s.cargos.Store(c); err != nil {
		return false, err
	}

	return true, nil
}

func (s *service) RequestPossibleRoutesForCargo(id shipping.TrackingID) []shipping.Itinerary {
	if id == "" {
		return nil
	}

	c, err := s.cargos.Find(id)
	if err != nil {
		return []shipping.Itinerary{}
	}

	return s.routingService.FetchRoutesForSpecification(c.RouteSpecification)
}

func (s *service) Cargos() []Cargo {
	var result []Cargo
	for _, c := range s.cargos.FindAll() {
		result = append(result, assemble(c, s.handlingEvents))
	}
	return result
}

func (s *service) Locations() []Location {
	var result []Location
	for _, v := range s.locations.FindAll() {
		result = append(result, Location{
			UNLocode: string(v.UNLocode),
			Name:     v.Name,
		})
	}
	return result
}

// NewService creates a booking service with necessary dependencies.
func NewService(cargos shipping.CargoRepository, locations shipping.LocationRepository, events shipping.HandlingEventRepository) Service {
	return &service{
		cargos:         cargos,
		locations:      locations,
		handlingEvents: events,
	}
}

// Location is a read model for booking views.
type Location struct {
	UNLocode string `json:"locode"`
	Name     string `json:"name"`
}

// Cargo is a read model for booking views.
type Cargo struct {
	ArrivalDeadline time.Time      `json:"arrival_deadline"`
	Destination     string         `json:"destination"`
	Legs            []shipping.Leg `json:"legs,omitempty"`
	Misrouted       bool           `json:"misrouted"`
	Origin          string         `json:"origin"`
	Routed          bool           `json:"routed"`
	TrackingID      string         `json:"tracking_id"`
}

func assemble(c *shipping.Cargo, events shipping.HandlingEventRepository) Cargo {
	return Cargo{
		TrackingID:      string(c.TrackingID),
		Origin:          string(c.Origin),
		Destination:     string(c.RouteSpecification.Destination),
		Misrouted:       c.Delivery.RoutingStatus == shipping.Misrouted,
		Routed:          !c.Itinerary.IsEmpty(),
		ArrivalDeadline: c.RouteSpecification.ArrivalDeadline,
		Legs:            c.Itinerary.Legs,
	}
}