package devops

import (
	"context"
	"fmt"
	"net/url"
	"strconv"
	"time"

	"github.com/go-resty/resty/v2"

	authclient "github.com/tidbcloud/aisupport/packages/sdk-go/infra/auth/client"
)

const apiBasePath = "/api/v1/devops"

// Config configures a DevOps operation API client.
type Config struct {
	// BaseURL is the DevOps backend domain, e.g. https://prod.devops.pingcap.com.
	BaseURL string
	// FrontendURL is the Ops Portal domain used to build human-facing links.
	FrontendURL string
	// Timeout bounds a single HTTP request.
	Timeout time.Duration
}

// Client talks to the DevOps operation API.
//
// It only ever calls the operation endpoints: approvals are never touched, so
// an instance created through this client always goes through whatever
// approval chain the server has configured for that operation.
type Client struct {
	http        *resty.Client
	frontendURL string
}

// NewClient builds a client authenticated with the DEVOPS_* credential found
// in the environment.
func NewClient(cfg Config) (*Client, error) {
	baseURL, err := url.JoinPath(cfg.BaseURL, apiBasePath)
	if err != nil {
		return nil, err
	}

	http := resty.New().
		SetBaseURL(baseURL).
		SetTimeout(cfg.Timeout).
		SetHeader("Accept", "application/json").
		OnAfterResponse(func(_ *resty.Client, r *resty.Response) error {
			if !r.IsSuccess() {
				return fmt.Errorf("%s %s: status %s, body: %s",
					r.Request.Method, r.Request.URL, r.Status(), string(r.Body()))
			}
			return nil
		})

	http, err = authclient.SetupClientAuth(http)
	if err != nil {
		return nil, err
	}

	return &Client{http: http, frontendURL: cfg.FrontendURL}, nil
}

// InstanceURL returns the Ops Portal page of an operation instance, where a
// human can review the approval and follow execution.
func (c *Client) InstanceURL(id uint64) string {
	if c.frontendURL == "" {
		return ""
	}
	return fmt.Sprintf("%s/operations/%d", c.frontendURL, id)
}

// ListOperationsParams filters the operation catalog.
type ListOperationsParams struct {
	Target     string
	Categories []string
}

// ListOperations returns every operation definition the server knows about.
func (c *Client) ListOperations(ctx context.Context, p ListOperationsParams) ([]*Operation, error) {
	req := c.http.R().SetContext(ctx)
	if p.Target != "" {
		req.SetQueryParam("target", p.Target)
	}
	if len(p.Categories) > 0 {
		req.SetQueryParamsFromValues(url.Values{"categories": p.Categories})
	}

	out := &listOperationResp{}
	if _, err := req.SetResult(out).Get("/operation/operations"); err != nil {
		return nil, err
	}
	return out.Operations, nil
}

// GetOperationParams asks the server to inject runtime param values (such as
// the node sizes actually available for a cluster) into the definition.
type GetOperationParams struct {
	InjectParam bool
	TargetType  string
	TargetID    string
	ServicePlan string
}

// GetOperation returns a single operation definition.
func (c *Client) GetOperation(ctx context.Context, name string, p GetOperationParams) (*Operation, error) {
	req := c.http.R().SetContext(ctx)
	if p.InjectParam {
		req.SetQueryParam("inject_param", "true")
	}
	if p.TargetType != "" {
		req.SetQueryParam("target_type", p.TargetType)
	}
	if p.TargetID != "" {
		req.SetQueryParam("target_id", p.TargetID)
	}
	if p.ServicePlan != "" {
		req.SetQueryParam("service_plan", p.ServicePlan)
	}

	out := &getOperationResp{}
	_, err := req.SetResult(out).SetPathParam("operation_name", name).
		Get("/operation/operations/{operation_name}")
	if err != nil {
		return nil, err
	}
	if out.Operation == nil {
		return nil, fmt.Errorf("operation %q not found", name)
	}
	return out.Operation, nil
}

// PreviewInstance runs the server-side pre-checks for a request without
// creating anything, and returns the advisories they produced.
func (c *Client) PreviewInstance(ctx context.Context, body CreateInstanceReq) ([]Advisory, error) {
	out := &previewInstanceResp{}
	_, err := c.http.R().SetContext(ctx).SetBody(body).SetResult(out).
		Post("/operation/instances/preview")
	if err != nil {
		return nil, err
	}
	return out.Advisories, nil
}

// CreateInstance submits an operation instance. The instance enters the
// server-side approval chain; it does not start executing until approved.
func (c *Client) CreateInstance(ctx context.Context, body CreateInstanceReq) (uint64, error) {
	out := &createInstanceResp{}
	_, err := c.http.R().SetContext(ctx).SetBody(body).SetResult(out).
		Post("/operation/instances")
	if err != nil {
		return 0, err
	}
	if out.InstanceID == nil {
		return 0, fmt.Errorf("create instance returned no instance id (trace_id: %s)", out.TraceID)
	}
	return *out.InstanceID, nil
}

// GetInstance returns one operation instance.
func (c *Client) GetInstance(ctx context.Context, id uint64) (*Instance, error) {
	out := &getInstanceResp{}
	_, err := c.http.R().SetContext(ctx).SetResult(out).
		SetPathParam("instance_id", strconv.FormatUint(id, 10)).
		Get("/operation/instances/{instance_id}")
	if err != nil {
		return nil, err
	}
	if out.Instance == nil {
		return nil, fmt.Errorf("instance %d not found", id)
	}
	return out.Instance, nil
}

// ListInstancesParams filters operation instances.
type ListInstancesParams struct {
	Operation    string
	Phase        string
	Applicant    string
	TargetID     string
	Type         string
	CreateAfter  *int64
	CreateBefore *int64
	Page         int
	PerPage      int
}

// ListInstances returns operation instances and the total match count.
func (c *Client) ListInstances(ctx context.Context, p ListInstancesParams) ([]*Instance, int64, error) {
	req := c.http.R().SetContext(ctx)
	setIfNotEmpty := func(key, value string) {
		if value != "" {
			req.SetQueryParam(key, value)
		}
	}
	setIfNotEmpty("operation", p.Operation)
	setIfNotEmpty("phase", p.Phase)
	setIfNotEmpty("applicant", p.Applicant)
	setIfNotEmpty("target_id", p.TargetID)
	setIfNotEmpty("type", p.Type)
	if p.CreateAfter != nil {
		req.SetQueryParam("create_after", strconv.FormatInt(*p.CreateAfter, 10))
	}
	if p.CreateBefore != nil {
		req.SetQueryParam("create_before", strconv.FormatInt(*p.CreateBefore, 10))
	}
	if p.Page > 0 {
		req.SetQueryParam("page", strconv.Itoa(p.Page))
	}
	if p.PerPage > 0 {
		req.SetQueryParam("per_page", strconv.Itoa(p.PerPage))
	}

	out := &listInstanceResp{}
	if _, err := req.SetResult(out).Get("/operation/instances"); err != nil {
		return nil, 0, err
	}
	return out.Instances, out.Total, nil
}

// CancelInstance withdraws an instance that has not finished yet.
func (c *Client) CancelInstance(ctx context.Context, id uint64) error {
	_, err := c.http.R().SetContext(ctx).
		SetPathParam("instance_id", strconv.FormatUint(id, 10)).
		Post("/operation/instances/{instance_id}/cancel")
	return err
}
