// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package api

import (
	"sort"
	"time"
)

// Deployments is used to query the deployments endpoints.
type Deployments struct {
	client *Client
}

// Deployments returns a new handle on the deployments.
func (c *Client) Deployments() *Deployments {
	return &Deployments{client: c}
}

// List is used to dump all the deployments.
func (d *Deployments) List(q *QueryOptions) ([]*Deployment, *QueryMeta, error) {
	var resp []*Deployment
	qm, err := d.client.query("/v1/deployments", &resp, q)
	if err != nil {
		return nil, nil, err
	}
	sort.Sort(DeploymentIndexSort(resp))
	return resp, qm, nil
}

func (d *Deployments) PrefixList(prefix string) ([]*Deployment, *QueryMeta, error) {
	return d.List(&QueryOptions{Prefix: prefix})
}

// Info is used to query a single deployment by its ID.
func (d *Deployments) Info(deploymentID string, q *QueryOptions) (*Deployment, *QueryMeta, error) {
	var resp Deployment
	qm, err := d.client.query("/v1/deployment/"+deploymentID, &resp, q)
	if err != nil {
		return nil, nil, err
	}
	return &resp, qm, nil
}

// Allocations is used to retrieve a set of allocations that are part of the
// deployment
func (d *Deployments) Allocations(deploymentID string, q *QueryOptions) ([]*AllocationListStub, *QueryMeta, error) {
	var resp []*AllocationListStub
	qm, err := d.client.query("/v1/deployment/allocations/"+deploymentID, &resp, q)
	if err != nil {
		return nil, nil, err
	}
	sort.Sort(AllocIndexSort(resp))
	return resp, qm, nil
}

// Fail is used to fail the given deployment.
func (d *Deployments) Fail(deploymentID string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) {
	var resp DeploymentUpdateResponse
	req := &DeploymentFailRequest{
		DeploymentID: deploymentID,
	}
	wm, err := d.client.put("/v1/deployment/fail/"+deploymentID, req, &resp, q)
	if err != nil {
		return nil, nil, err
	}
	return &resp, wm, nil
}

// Pause is used to pause or unpause the given deployment.
func (d *Deployments) Pause(deploymentID string, pause bool, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) {
	var resp DeploymentUpdateResponse
	req := &DeploymentPauseRequest{
		DeploymentID: deploymentID,
		Pause:        pause,
	}
	wm, err := d.client.put("/v1/deployment/pause/"+deploymentID, req, &resp, q)
	if err != nil {
		return nil, nil, err
	}
	return &resp, wm, nil
}

// PromoteAll is used to promote all canaries in the given deployment
func (d *Deployments) PromoteAll(deploymentID string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) {
	var resp DeploymentUpdateResponse
	req := &DeploymentPromoteRequest{
		DeploymentID: deploymentID,
		All:          true,
	}
	wm, err := d.client.put("/v1/deployment/promote/"+deploymentID, req, &resp, q)
	if err != nil {
		return nil, nil, err
	}
	return &resp, wm, nil
}

// PromoteGroups is used to promote canaries in the passed groups in the given deployment
func (d *Deployments) PromoteGroups(deploymentID string, groups []string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) {
	var resp DeploymentUpdateResponse
	req := &DeploymentPromoteRequest{
		DeploymentID: deploymentID,
		Groups:       groups,
	}
	wm, err := d.client.put("/v1/deployment/promote/"+deploymentID, req, &resp, q)
	if err != nil {
		return nil, nil, err
	}
	return &resp, wm, nil
}

// Unblock is used to unblock the given deployment.
func (d *Deployments) Unblock(deploymentID string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) {
	var resp DeploymentUpdateResponse
	req := &DeploymentUnblockRequest{
		DeploymentID: deploymentID,
	}
	wm, err := d.client.put("/v1/deployment/unblock/"+deploymentID, req, &resp, q)
	if err != nil {
		return nil, nil, err
	}
	return &resp, wm, nil
}

// SetAllocHealth is used to set allocation health for allocs that are part of
// the given deployment
func (d *Deployments) SetAllocHealth(deploymentID string, healthy, unhealthy []string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) {
	var resp DeploymentUpdateResponse
	req := &DeploymentAllocHealthRequest{
		DeploymentID:           deploymentID,
		HealthyAllocationIDs:   healthy,
		UnhealthyAllocationIDs: unhealthy,
	}
	wm, err := d.client.put("/v1/deployment/allocation-health/"+deploymentID, req, &resp, q)
	if err != nil {
		return nil, nil, err
	}
	return &resp, wm, nil
}

const (
	DeploymentStatusRunning    = "running"
	DeploymentStatusPaused     = "paused"
	DeploymentStatusFailed     = "failed"
	DeploymentStatusSuccessful = "successful"
	DeploymentStatusCancelled  = "cancelled"
	DeploymentStatusPending    = "pending"
	DeploymentStatusBlocked    = "blocked"
	DeploymentStatusUnblocking = "unblocking"
)

// Deployment is used to serialize an deployment.
type Deployment struct {
	// ID is a generated UUID for the deployment
	ID string

	// Namespace is the namespace the deployment is created in
	Namespace string

	// JobID is the job the deployment is created for
	JobID string

	// JobVersion is the version of the job at which the deployment is tracking
	JobVersion uint64

	// JobModifyIndex is the ModifyIndex of the job which the deployment is
	// tracking.
	JobModifyIndex uint64

	// JobSpecModifyIndex is the JobModifyIndex of the job which the
	// deployment is tracking.
	JobSpecModifyIndex uint64

	// JobCreateIndex is the create index of the job which the deployment is
	// tracking. It is needed so that if the job gets stopped and reran we can
	// present the correct list of deployments for the job and not old ones.
	JobCreateIndex uint64

	// IsMultiregion specifies if this deployment is part of a multi-region deployment
	IsMultiregion bool

	// TaskGroups is the set of task groups effected by the deployment and their
	// current deployment status.
	TaskGroups map[string]*DeploymentState

	// The status of the deployment
	Status string

	// StatusDescription allows a human readable description of the deployment
	// status.
	StatusDescription string

	CreateIndex uint64
	ModifyIndex uint64
}

// DeploymentState tracks the state of a deployment for a given task group.
type DeploymentState struct {
	PlacedCanaries    []string
	AutoRevert        bool
	ProgressDeadline  time.Duration
	RequireProgressBy time.Time
	Promoted          bool
	DesiredCanaries   int
	DesiredTotal      int
	PlacedAllocs      int
	HealthyAllocs     int
	UnhealthyAllocs   int
}

// DeploymentIndexSort is a wrapper to sort deployments by CreateIndex. We
// reverse the test so that we get the highest index first.
type DeploymentIndexSort []*Deployment

func (d DeploymentIndexSort) Len() int {
	return len(d)
}

func (d DeploymentIndexSort) Less(i, j int) bool {
	return d[i].CreateIndex > d[j].CreateIndex
}

func (d DeploymentIndexSort) Swap(i, j int) {
	d[i], d[j] = d[j], d[i]
}

// DeploymentUpdateResponse is used to respond to a deployment change. The
// response will include the modify index of the deployment as well as details
// of any triggered evaluation.
type DeploymentUpdateResponse struct {
	EvalID                string
	EvalCreateIndex       uint64
	DeploymentModifyIndex uint64
	RevertedJobVersion    *uint64
	WriteMeta
}

// DeploymentAllocHealthRequest is used to set the health of a set of
// allocations as part of a deployment.
type DeploymentAllocHealthRequest struct {
	DeploymentID string

	// Marks these allocations as healthy, allow further allocations
	// to be rolled.
	HealthyAllocationIDs []string

	// Any unhealthy allocations fail the deployment
	UnhealthyAllocationIDs []string

	WriteRequest
}

// DeploymentPromoteRequest is used to promote task groups in a deployment
type DeploymentPromoteRequest struct {
	DeploymentID string

	// All is to promote all task groups
	All bool

	// Groups is used to set the promotion status per task group
	Groups []string

	WriteRequest
}

// DeploymentPauseRequest is used to pause a deployment
type DeploymentPauseRequest struct {
	DeploymentID string

	// Pause sets the pause status
	Pause bool

	WriteRequest
}

// DeploymentSpecificRequest is used to make a request specific to a particular
// deployment
type DeploymentSpecificRequest struct {
	DeploymentID string
	QueryOptions
}

// DeploymentFailRequest is used to fail a particular deployment
type DeploymentFailRequest struct {
	DeploymentID string
	WriteRequest
}

// DeploymentUnblockRequest is used to unblock a particular deployment
type DeploymentUnblockRequest struct {
	DeploymentID string
	WriteRequest
}

// SingleDeploymentResponse is used to respond with a single deployment
type SingleDeploymentResponse struct {
	Deployment *Deployment
	QueryMeta
}
