package codersdk

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"golang.org/x/xerrors"

	"github.com/google/uuid"
)

type ProxyHealthStatus string

const (
	// ProxyHealthy means the proxy access url is reachable and returns a healthy
	// status code.
	ProxyHealthy ProxyHealthStatus = "ok"
	// ProxyUnreachable means the proxy access url is not responding.
	ProxyUnreachable ProxyHealthStatus = "unreachable"
	// ProxyUnhealthy means the proxy access url is responding, but there is some
	// problem with the proxy. This problem may or may not be preventing functionality.
	ProxyUnhealthy ProxyHealthStatus = "unhealthy"
	// ProxyUnregistered means the proxy has not registered a url yet. This means
	// the proxy was created with the cli, but has not yet been started.
	ProxyUnregistered ProxyHealthStatus = "unregistered"
)

type WorkspaceProxyStatus struct {
	Status ProxyHealthStatus `json:"status" table:"status,default_sort"`
	// Report provides more information about the health of the workspace proxy.
	Report    ProxyHealthReport `json:"report,omitempty" table:"report"`
	CheckedAt time.Time         `json:"checked_at" table:"checked_at" format:"date-time"`
}

// ProxyHealthReport is a report of the health of the workspace proxy.
// A healthy report will have no errors. Warnings are not fatal.
type ProxyHealthReport struct {
	// Errors are problems that prevent the workspace proxy from being healthy
	Errors []string `json:"errors"`
	// Warnings do not prevent the workspace proxy from being healthy, but
	// should be addressed.
	Warnings []string `json:"warnings"`
}

type WorkspaceProxy struct {
	// Extends Region with extra information
	Region      `table:"region,recursive_inline"`
	DerpEnabled bool `json:"derp_enabled" table:"derp_enabled"`
	DerpOnly    bool `json:"derp_only" table:"derp_only"`

	// Status is the latest status check of the proxy. This will be empty for deleted
	// proxies. This value can be used to determine if a workspace proxy is healthy
	// and ready to use.
	Status WorkspaceProxyStatus `json:"status,omitempty" table:"proxy,recursive"`

	CreatedAt time.Time `json:"created_at" format:"date-time" table:"created_at"`
	UpdatedAt time.Time `json:"updated_at" format:"date-time" table:"updated_at"`
	Deleted   bool      `json:"deleted" table:"deleted"`
	Version   string    `json:"version" table:"version"`
}

type CreateWorkspaceProxyRequest struct {
	Name        string `json:"name" validate:"required"`
	DisplayName string `json:"display_name"`
	Icon        string `json:"icon"`
}

type UpdateWorkspaceProxyResponse struct {
	Proxy      WorkspaceProxy `json:"proxy" table:"p,recursive_inline"`
	ProxyToken string         `json:"proxy_token" table:"proxy token"`
}

func (c *Client) CreateWorkspaceProxy(ctx context.Context, req CreateWorkspaceProxyRequest) (UpdateWorkspaceProxyResponse, error) {
	res, err := c.Request(ctx, http.MethodPost,
		"/api/v2/workspaceproxies",
		req,
	)
	if err != nil {
		return UpdateWorkspaceProxyResponse{}, xerrors.Errorf("make request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusCreated {
		return UpdateWorkspaceProxyResponse{}, ReadBodyAsError(res)
	}
	var resp UpdateWorkspaceProxyResponse
	return resp, json.NewDecoder(res.Body).Decode(&resp)
}

func (c *Client) WorkspaceProxies(ctx context.Context) (RegionsResponse[WorkspaceProxy], error) {
	res, err := c.Request(ctx, http.MethodGet,
		"/api/v2/workspaceproxies",
		nil,
	)
	if err != nil {
		return RegionsResponse[WorkspaceProxy]{}, xerrors.Errorf("make request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return RegionsResponse[WorkspaceProxy]{}, ReadBodyAsError(res)
	}

	var proxies RegionsResponse[WorkspaceProxy]
	return proxies, json.NewDecoder(res.Body).Decode(&proxies)
}

type PatchWorkspaceProxy struct {
	ID              uuid.UUID `json:"id" format:"uuid" validate:"required"`
	Name            string    `json:"name" validate:"required"`
	DisplayName     string    `json:"display_name" validate:"required"`
	Icon            string    `json:"icon" validate:"required"`
	RegenerateToken bool      `json:"regenerate_token"`
}

func (c *Client) PatchWorkspaceProxy(ctx context.Context, req PatchWorkspaceProxy) (UpdateWorkspaceProxyResponse, error) {
	res, err := c.Request(ctx, http.MethodPatch,
		fmt.Sprintf("/api/v2/workspaceproxies/%s", req.ID.String()),
		req,
	)
	if err != nil {
		return UpdateWorkspaceProxyResponse{}, xerrors.Errorf("make request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return UpdateWorkspaceProxyResponse{}, ReadBodyAsError(res)
	}
	var resp UpdateWorkspaceProxyResponse
	return resp, json.NewDecoder(res.Body).Decode(&resp)
}

func (c *Client) DeleteWorkspaceProxyByName(ctx context.Context, name string) error {
	res, err := c.Request(ctx, http.MethodDelete,
		fmt.Sprintf("/api/v2/workspaceproxies/%s", name),
		nil,
	)
	if err != nil {
		return xerrors.Errorf("make request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return ReadBodyAsError(res)
	}

	return nil
}

func (c *Client) DeleteWorkspaceProxyByID(ctx context.Context, id uuid.UUID) error {
	return c.DeleteWorkspaceProxyByName(ctx, id.String())
}

func (c *Client) WorkspaceProxyByName(ctx context.Context, name string) (WorkspaceProxy, error) {
	res, err := c.Request(ctx, http.MethodGet,
		fmt.Sprintf("/api/v2/workspaceproxies/%s", name),
		nil,
	)
	if err != nil {
		return WorkspaceProxy{}, xerrors.Errorf("make request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return WorkspaceProxy{}, ReadBodyAsError(res)
	}

	var resp WorkspaceProxy
	return resp, json.NewDecoder(res.Body).Decode(&resp)
}

func (c *Client) WorkspaceProxyByID(ctx context.Context, id uuid.UUID) (WorkspaceProxy, error) {
	return c.WorkspaceProxyByName(ctx, id.String())
}

type RegionTypes interface {
	Region | WorkspaceProxy
}

type RegionsResponse[R RegionTypes] struct {
	Regions []R `json:"regions"`
}

type Region struct {
	ID          uuid.UUID `json:"id" format:"uuid" table:"id"`
	Name        string    `json:"name" table:"name,default_sort"`
	DisplayName string    `json:"display_name" table:"display_name"`
	IconURL     string    `json:"icon_url" table:"icon_url"`
	Healthy     bool      `json:"healthy" table:"healthy"`

	// PathAppURL is the URL to the base path for path apps. Optional
	// unless wildcard_hostname is set.
	// E.g. https://us.example.com
	PathAppURL string `json:"path_app_url" table:"url"`

	// WildcardHostname is the wildcard hostname for subdomain apps.
	// E.g. *.us.example.com
	// E.g. *--suffix.au.example.com
	// Optional. Does not need to be on the same domain as PathAppURL.
	WildcardHostname string `json:"wildcard_hostname" table:"wildcard_hostname"`
}

func (c *Client) Regions(ctx context.Context) ([]Region, error) {
	res, err := c.Request(ctx, http.MethodGet,
		"/api/v2/regions",
		nil,
	)
	if err != nil {
		return nil, xerrors.Errorf("make request: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return nil, ReadBodyAsError(res)
	}

	var regions RegionsResponse[Region]
	return regions.Regions, json.NewDecoder(res.Body).Decode(&regions)
}
