//
// DISCLAIMER
//
// Copyright 2017 ArangoDB GmbH, Cologne, Germany
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
// Author Ewout Prangsma
//

package driver

import (
	"context"
	"encoding/json"
	"fmt"
	"reflect"
	"time"
)

// Cluster provides access to cluster wide specific operations.
// To use this interface, an ArangoDB cluster is required.
type Cluster interface {
	// Get the cluster configuration & health
	Health(ctx context.Context) (ClusterHealth, error)

	// Get the inventory of the cluster containing all collections (with entire details) of a database.
	DatabaseInventory(ctx context.Context, db Database) (DatabaseInventory, error)

	// MoveShard moves a single shard of the given collection from server `fromServer` to
	// server `toServer`.
	MoveShard(ctx context.Context, col Collection, shard ShardID, fromServer, toServer ServerID) error

	// CleanOutServer triggers activities to clean out a DBServer.
	CleanOutServer(ctx context.Context, serverID string) error

	// ResignServer triggers activities to let a DBServer resign for all shards.
	ResignServer(ctx context.Context, serverID string) error

	// IsCleanedOut checks if the dbserver with given ID has been cleaned out.
	IsCleanedOut(ctx context.Context, serverID string) (bool, error)

	// RemoveServer is a low-level option to remove a server from a cluster.
	// This function is suitable for servers of type coordinator or dbserver.
	// The use of `ClientServerAdmin.Shutdown` is highly recommended above this function.
	RemoveServer(ctx context.Context, serverID ServerID) error
}

// ServerID identifies an arangod server in a cluster.
type ServerID string

// ClusterHealth contains health information for all servers in a cluster.
type ClusterHealth struct {
	// Unique identifier of the entire cluster.
	// This ID is created when the cluster was first created.
	ID string `json:"ClusterId"`
	// Health per server
	Health map[ServerID]ServerHealth `json:"Health"`
}

// ServerSyncStatus describes the servers sync status
type ServerSyncStatus string

const (
	ServerSyncStatusUnknown   ServerSyncStatus = "UNKNOWN"
	ServerSyncStatusUndefined ServerSyncStatus = "UNDEFINED"
	ServerSyncStatusStartup   ServerSyncStatus = "STARTUP"
	ServerSyncStatusStopping  ServerSyncStatus = "STOPPING"
	ServerSyncStatusStopped   ServerSyncStatus = "STOPPED"
	ServerSyncStatusServing   ServerSyncStatus = "SERVING"
	ServerSyncStatusShutdown  ServerSyncStatus = "SHUTDOWN"
)

// ServerHealth contains health information of a single server in a cluster.
type ServerHealth struct {
	Endpoint            string           `json:"Endpoint"`
	LastHeartbeatAcked  time.Time        `json:"LastHeartbeatAcked"`
	LastHeartbeatSent   time.Time        `json:"LastHeartbeatSent"`
	LastHeartbeatStatus string           `json:"LastHeartbeatStatus"`
	Role                ServerRole       `json:"Role"`
	ShortName           string           `json:"ShortName"`
	Status              ServerStatus     `json:"Status"`
	CanBeDeleted        bool             `json:"CanBeDeleted"`
	HostID              string           `json:"Host,omitempty"`
	Version             Version          `json:"Version,omitempty"`
	Engine              EngineType       `json:"Engine,omitempty"`
	SyncStatus          ServerSyncStatus `json:"SyncStatus,omitempty"`

	// Only for Coordinators
	AdvertisedEndpoint *string `json:"AdvertisedEndpoint,omitempty"`

	// Only for Agents
	Leader  *string `json:"Leader,omitempty"`
	Leading *bool   `json:"Leading,omitempty"`
}

// ServerStatus describes the health status of a server
type ServerStatus string

const (
	// ServerStatusGood indicates server is in good state
	ServerStatusGood ServerStatus = "GOOD"
	// ServerStatusBad indicates server has missed 1 heartbeat
	ServerStatusBad ServerStatus = "BAD"
	// ServerStatusFailed indicates server has been declared failed by the supervision, this happens after about 15s being bad.
	ServerStatusFailed ServerStatus = "FAILED"
)

// DatabaseInventory describes a detailed state of the collections & shards of a specific database within a cluster.
type DatabaseInventory struct {
	// Details of database, this is present since ArangoDB 3.6
	Info DatabaseInfo `json:"properties,omitempty"`
	// Details of all collections
	Collections []InventoryCollection `json:"collections,omitempty"`
	// Details of all views
	Views []InventoryView `json:"views,omitempty"`
	State State           `json:"state,omitempty"`
	Tick  string          `json:"tick,omitempty"`
}

type State struct {
	Running                bool      `json:"running,omitempty"`
	LastLogTick            string    `json:"lastLogTick,omitempty"`
	LastUncommittedLogTick string    `json:"lastUncommittedLogTick,omitempty"`
	TotalEvents            int64     `json:"totalEvents,omitempty"`
	Time                   time.Time `json:"time,omitempty"`
}

// UnmarshalJSON marshals State to arangodb json representation
func (s *State) UnmarshalJSON(d []byte) error {
	var internal interface{}

	if err := json.Unmarshal(d, &internal); err != nil {
		return err
	}

	if val, ok := internal.(string); ok {
		if val != "unused" {
			fmt.Printf("unrecognized State value: %s\n", val)
		}
		*s = State{}
		return nil
	} else {
		type Alias State
		out := Alias{}

		if err := json.Unmarshal(d, &out); err != nil {
			return &json.UnmarshalTypeError{
				Value: string(d),
				Type:  reflect.TypeOf(s).Elem(),
			}
		}
		*s = State(out)
	}

	return nil
}

// IsReady returns true if the IsReady flag of all collections is set.
func (i DatabaseInventory) IsReady() bool {
	for _, c := range i.Collections {
		if !c.IsReady {
			return false
		}
	}
	return true
}

// PlanVersion returns the plan version of the first collection in the given inventory.
func (i DatabaseInventory) PlanVersion() int64 {
	if len(i.Collections) == 0 {
		return 0
	}
	return i.Collections[0].PlanVersion
}

// CollectionByName returns the InventoryCollection with given name.
// Return false if not found.
func (i DatabaseInventory) CollectionByName(name string) (InventoryCollection, bool) {
	for _, c := range i.Collections {
		if c.Parameters.Name == name {
			return c, true
		}
	}
	return InventoryCollection{}, false
}

// ViewByName returns the InventoryView with given name.
// Return false if not found.
func (i DatabaseInventory) ViewByName(name string) (InventoryView, bool) {
	for _, v := range i.Views {
		if v.Name == name {
			return v, true
		}
	}
	return InventoryView{}, false
}

// InventoryCollection is a single element of a DatabaseInventory, containing all information
// of a specific collection.
type InventoryCollection struct {
	Parameters  InventoryCollectionParameters `json:"parameters"`
	Indexes     []InventoryIndex              `json:"indexes,omitempty"`
	PlanVersion int64                         `json:"planVersion,omitempty"`
	IsReady     bool                          `json:"isReady,omitempty"`
	AllInSync   bool                          `json:"allInSync,omitempty"`
}

// IndexByFieldsAndType returns the InventoryIndex with given fields & type.
// Return false if not found.
func (i InventoryCollection) IndexByFieldsAndType(fields []string, indexType string) (InventoryIndex, bool) {
	for _, idx := range i.Indexes {
		if idx.Type == indexType && idx.FieldsEqual(fields) {
			return idx, true
		}
	}
	return InventoryIndex{}, false
}

// InventoryCollectionParameters contains all configuration parameters of a collection in a database inventory.
type InventoryCollectionParameters struct {
	// Available from 3.7 ArangoD version.
	CacheEnabled         bool   `json:"cacheEnabled,omitempty"`
	Deleted              bool   `json:"deleted,omitempty"`
	DistributeShardsLike string `json:"distributeShardsLike,omitempty"`
	// Deprecated: since 3.7 version. It is related only to MMFiles.
	DoCompact bool `json:"doCompact,omitempty"`
	// Available from 3.7 ArangoD version.
	GloballyUniqueId string `json:"globallyUniqueId,omitempty"`
	ID               string `json:"id,omitempty"`
	// Deprecated: since 3.7 version. It is related only to MMFiles.
	IndexBuckets int              `json:"indexBuckets,omitempty"`
	Indexes      []InventoryIndex `json:"indexes,omitempty"`
	// Available from 3.9 ArangoD version.
	InternalValidatorType int `json:"internalValidatorType,omitempty"`
	// Available from 3.7 ArangoD version.
	IsDisjoint bool `json:"isDisjoint,omitempty"`
	IsSmart    bool `json:"isSmart,omitempty"`
	// Available from 3.7 ArangoD version.
	IsSmartChild bool `json:"isSmartChild,omitempty"`
	IsSystem     bool `json:"isSystem,omitempty"`
	// Deprecated: since 3.7 version. It is related only to MMFiles.
	IsVolatile bool `json:"isVolatile,omitempty"`
	// Deprecated: since 3.7 version. It is related only to MMFiles.
	JournalSize int64 `json:"journalSize,omitempty"`
	KeyOptions  struct {
		AllowUserKeys bool `json:"allowUserKeys,omitempty"`
		// Deprecated: this field has wrong type and will be removed in the future. It is not used anymore since it can cause parsing issues.
		LastValue   int64  `json:"-"`
		LastValueV2 uint64 `json:"lastValue,omitempty"`
		Type        string `json:"type,omitempty"`
	} `json:"keyOptions"`
	// Deprecated: use 'WriteConcern' instead.
	MinReplicationFactor int    `json:"minReplicationFactor,omitempty"`
	Name                 string `json:"name,omitempty"`
	NumberOfShards       int    `json:"numberOfShards,omitempty"`
	// Deprecated: since 3.7 ArangoD version.
	Path              string `json:"path,omitempty"`
	PlanID            string `json:"planId,omitempty"`
	ReplicationFactor int    `json:"replicationFactor,omitempty"`
	// Schema for collection validation.
	Schema            *CollectionSchemaOptions `json:"schema,omitempty"`
	ShadowCollections []int                    `json:"shadowCollections,omitempty"`
	ShardingStrategy  ShardingStrategy         `json:"shardingStrategy,omitempty"`
	ShardKeys         []string                 `json:"shardKeys,omitempty"`
	Shards            map[ShardID][]ServerID   `json:"shards,omitempty"`
	// Optional only for some collections.
	SmartGraphAttribute string `json:"smartGraphAttribute,omitempty"`
	// Optional only for some collections.
	SmartJoinAttribute string           `json:"smartJoinAttribute,omitempty"`
	Status             CollectionStatus `json:"status,omitempty"`
	// Available from 3.7 ArangoD version.
	SyncByRevision bool           `json:"syncByRevision,omitempty"`
	Type           CollectionType `json:"type,omitempty"`
	// Available from 3.7 ArangoD version.
	UsesRevisionsAsDocumentIds bool `json:"usesRevisionsAsDocumentIds,omitempty"`
	WaitForSync                bool `json:"waitForSync,omitempty"`
	// Available from 3.6 ArangoD version.
	WriteConcern int `json:"writeConcern,omitempty"`
	// Available from 3.10 ArangoD version.
	ComputedValues []ComputedValue `json:"computedValues,omitempty"`
}

// IsSatellite returns true if the collection is a satellite collection
func (icp *InventoryCollectionParameters) IsSatellite() bool {
	return icp.ReplicationFactor == ReplicationFactorSatellite
}

// ShardID is an internal identifier of a specific shard
type ShardID string

// InventoryIndex contains all configuration parameters of a single index of a collection in a database inventory.
type InventoryIndex struct {
	ID              string   `json:"id,omitempty"`
	Type            string   `json:"type,omitempty"`
	Fields          []string `json:"fields,omitempty"`
	Unique          bool     `json:"unique"`
	Sparse          bool     `json:"sparse"`
	Deduplicate     bool     `json:"deduplicate"`
	MinLength       int      `json:"minLength,omitempty"`
	GeoJSON         bool     `json:"geoJson,omitempty"`
	Name            string   `json:"name,omitempty"`
	ExpireAfter     int      `json:"expireAfter,omitempty"`
	Estimates       bool     `json:"estimates,omitempty"`
	FieldValueTypes string   `json:"fieldValueTypes,omitempty"`
	CacheEnabled    *bool    `json:"cacheEnabled,omitempty"`
}

// FieldsEqual returns true when the given fields list equals the
// Fields list in the InventoryIndex.
// The order of fields is irrelevant.
func (i InventoryIndex) FieldsEqual(fields []string) bool {
	return stringSliceEqualsIgnoreOrder(i.Fields, fields)
}

// InventoryView is a single element of a DatabaseInventory, containing all information
// of a specific view.
type InventoryView struct {
	Name     string   `json:"name,omitempty"`
	Deleted  bool     `json:"deleted,omitempty"`
	ID       string   `json:"id,omitempty"`
	IsSystem bool     `json:"isSystem,omitempty"`
	PlanID   string   `json:"planId,omitempty"`
	Type     ViewType `json:"type,omitempty"`
	// Include all properties from an arangosearch view.
	ArangoSearchViewProperties
}

// stringSliceEqualsIgnoreOrder returns true when the given lists contain the same elements.
// The order of elements is irrelevant.
func stringSliceEqualsIgnoreOrder(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	bMap := make(map[string]struct{})
	for _, x := range b {
		bMap[x] = struct{}{}
	}
	for _, x := range a {
		if _, found := bMap[x]; !found {
			return false
		}
	}
	return true
}