package api

import (
	"encoding/json"
	"log"
	"time"
)

const (
	AddrFamilyIPv4 = 1
	AddrFamilyIPv6 = 2
)

// Route is a prefix with BGP information.
type Route struct {
	// ID         string  `json:"id"`
	NeighborID *string `json:"neighbor_id"`

	Network    string        `json:"network"`
	Interface  *string       `json:"interface"`
	Gateway    *string       `json:"gateway"`
	Metric     int           `json:"metric"`
	BGP        *BGPInfo      `json:"bgp"`
	Age        time.Duration `json:"age"`
	Type       []string      `json:"type"` // [BGP, unicast, univ]
	Primary    bool          `json:"primary"`
	LearntFrom *string       `json:"learnt_from"`
	AddrFamily uint8         `json:"address_family"` // 1=IPv4, 2=IPv6

	Details *json.RawMessage `json:"details"`
}

func (r *Route) String() string {
	s, _ := json.Marshal(r)
	return string(s)
}

// MatchAddrFamily checks if the route matches the given address family
func (r *Route) MatchAddrFamily(family uint8) bool {
	return r.AddrFamily == family
}

// MatchSourceID implements Filterable interface for routes
func (r *Route) MatchSourceID(id string) bool {
	return true // A route has no source info so we exclude this filter
}

// MatchASN is not defined
func (r *Route) MatchASN(asn int) bool {
	return true // Same here
}

// MatchCommunity checks for the presence of a BGP community
func (r *Route) MatchCommunity(community Community) bool {
	return r.BGP.HasCommunity(community)
}

// MatchExtCommunity checks for the presence of a BGP extended community
func (r *Route) MatchExtCommunity(community ExtCommunity) bool {
	return r.BGP.HasExtCommunity(community)
}

// MatchLargeCommunity checks for the presence of a large BGP community
func (r *Route) MatchLargeCommunity(community Community) bool {
	return r.BGP.HasLargeCommunity(community)
}

// Routes is a collection of routes
type Routes []*Route

func (routes Routes) Len() int {
	return len(routes)
}

func (routes Routes) Less(i, j int) bool {
	return routes[i].Network < routes[j].Network
}

func (routes Routes) Swap(i, j int) {
	routes[i], routes[j] = routes[j], routes[i]
}

// ToLookupRoutes prepares routes for lookup
func (routes Routes) ToLookupRoutes(
	state string,
	rs *LookupRouteServer,
	neighbors map[string]*Neighbor,
) LookupRoutes {
	lookupRoutes := make(LookupRoutes, 0, len(routes))
	for _, route := range routes {
		neighbor, ok := neighbors[*route.NeighborID]
		if !ok {
			log.Println("prepare route, neighbor not found:", route.NeighborID)
			continue
		}
		lr := &LookupRoute{
			Route:       route,
			State:       state,
			Neighbor:    neighbor,
			RouteServer: rs,
		}
		lr.Route.Details = nil
		lookupRoutes = append(lookupRoutes, lr)
	}
	return lookupRoutes
}

// RoutesResponse contains all routes from a source
type RoutesResponse struct {
	Response
	Imported    Routes `json:"imported"`
	Filtered    Routes `json:"filtered"`
	NotExported Routes `json:"not_exported"`
}

// CacheTTL returns the cache ttl of the response
func (res *RoutesResponse) CacheTTL() time.Duration {
	now := time.Now().UTC()
	return res.Response.Meta.TTL.Sub(now)
}

// Merge combines two routes responses by appending
func (res *RoutesResponse) Merge(other *RoutesResponse) {
	res.Imported = append(res.Imported, other.Imported...)
	res.Filtered = append(res.Filtered, other.Filtered...)
	res.NotExported = append(res.NotExported, other.NotExported...)
}

// TimedResponse include the duration of the request
type TimedResponse struct {
	RequestDuration float64 `json:"request_duration_ms"`
}

// Pagination information, including the
// current page, total pages, page size, etc...
type Pagination struct {
	Page         int `json:"page"`
	PageSize     int `json:"page_size"`
	TotalPages   int `json:"total_pages"`
	TotalResults int `json:"total_results"`
}

// A PaginatedResponse with pagination info
type PaginatedResponse struct {
	Pagination Pagination `json:"pagination"`
}

// FilteredResponse includes filters applied and available
type FilteredResponse struct {
	FiltersAvailable    *SearchFilters `json:"filters_available"`
	FiltersApplied      *SearchFilters `json:"filters_applied"`
	FiltersNotAvailable []string       `json:"filters_not_available"`
}

const (
	// RouteStateFiltered indicates that the route
	// was not accepted by the route server.
	RouteStateFiltered = "filtered"
	// RouteStateImported indicates that the route was
	// imported by the route server.
	RouteStateImported = "imported"
)

// NeighborQuery is used in finding routes by neighbors.
// Source and Neighbor IDs are pointers to string pools.
type NeighborQuery struct {
	NeighborID *string
	SourceID   *string
}

// LookupRoute is a route with additional
// neighbor and state information
type LookupRoute struct {
	*Route

	State string `json:"state"` // Filtered, Imported, ...

	Neighbor    *Neighbor          `json:"neighbor"`
	RouteServer *LookupRouteServer `json:"routeserver"`
}

// MatchSourceID implements filterable interface for lookup routes
func (r *LookupRoute) MatchSourceID(id string) bool {
	return *r.RouteServer.ID == id
}

// MatchASN matches the neighbor's ASN
func (r *LookupRoute) MatchASN(asn int) bool {
	return r.Neighbor.MatchASN(asn)
}

// MatchCommunity checks for the presence of a BGP community.
func (r *LookupRoute) MatchCommunity(community Community) bool {
	return r.Route.BGP.HasCommunity(community)
}

// MatchExtCommunity matches an extended community
func (r *LookupRoute) MatchExtCommunity(community ExtCommunity) bool {
	return r.Route.BGP.HasExtCommunity(community)
}

// MatchLargeCommunity matches large communities.
func (r *LookupRoute) MatchLargeCommunity(community Community) bool {
	return r.Route.BGP.HasLargeCommunity(community)
}

// MatchAddrFamily matches address family.
func (r *LookupRoute) MatchAddrFamily(family uint8) bool {
	return r.Route.MatchAddrFamily(family)
}

// MatchNeighborQuery matches a neighbor query
func (r *LookupRoute) MatchNeighborQuery(query *NeighborQuery) bool {
	if r.RouteServer.ID != query.SourceID {
		return false
	}
	if r.NeighborID != query.NeighborID {
		return false
	}
	return true
}

// LookupRoutes is a collection of lookup routes.
type LookupRoutes []*LookupRoute

func (r LookupRoutes) Len() int {
	return len(r)
}

func (r LookupRoutes) Less(i, j int) bool {
	return r[i].Route.Network < r[j].Route.Network
}

func (r LookupRoutes) Swap(i, j int) {
	r[i], r[j] = r[j], r[i]
}

// RoutesLookup contains routes and pagination info
type RoutesLookup struct {
	Routes     LookupRoutes `json:"routes"`
	Pagination Pagination   `json:"pagination"`
}

// RoutesLookupResponse is a PaginatedResponse with
// a set of lookup routes, as the result of a query of
// a specific route server.
type RoutesLookupResponse struct {
	Response
	PaginatedResponse
	TimedResponse
	FilteredResponse
	Routes LookupRoutes `json:"routes"`
}

// GlobalRoutesLookupResponse is the result of a routes
// query across all route servers.
type GlobalRoutesLookupResponse struct {
	Response
	PaginatedResponse
	TimedResponse
	FilteredResponse
	Routes LookupRoutes `json:"routes"`
}

// A PaginatedRoutesResponse includes routes and pagination
// information form a single route server
type PaginatedRoutesResponse struct {
	Response
	PaginatedResponse
	TimedResponse
	FilteredResponse
	RoutesResponse
}

// A PaginatedRoutesLookupResponse TODO
type PaginatedRoutesLookupResponse struct {
	Response
	TimedResponse
	FilteredResponse

	Imported *RoutesLookup `json:"imported"`
	Filtered *RoutesLookup `json:"filtered"`

	Status *StoreStatusMeta `json:"status"`
}
