package codersdk

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

	"github.com/google/uuid"
)

// APIKey: do not ever return the HashedSecret
type APIKey struct {
	ID              string      `json:"id" validate:"required"`
	UserID          uuid.UUID   `json:"user_id" validate:"required" format:"uuid"`
	LastUsed        time.Time   `json:"last_used" validate:"required" format:"date-time"`
	ExpiresAt       time.Time   `json:"expires_at" validate:"required" format:"date-time"`
	CreatedAt       time.Time   `json:"created_at" validate:"required" format:"date-time"`
	UpdatedAt       time.Time   `json:"updated_at" validate:"required" format:"date-time"`
	LoginType       LoginType   `json:"login_type" validate:"required" enums:"password,github,oidc,token"`
	Scope           APIKeyScope `json:"scope" validate:"required" enums:"all,application_connect"`
	TokenName       string      `json:"token_name" validate:"required"`
	LifetimeSeconds int64       `json:"lifetime_seconds" validate:"required"`
}

// LoginType is the type of login used to create the API key.
type LoginType string

const (
	LoginTypeUnknown  LoginType = ""
	LoginTypePassword LoginType = "password"
	LoginTypeGithub   LoginType = "github"
	LoginTypeOIDC     LoginType = "oidc"
	LoginTypeToken    LoginType = "token"
	// LoginTypeNone is used if no login method is available for this user.
	// If this is set, the user has no method of logging in.
	// API keys can still be created by an owner and used by the user.
	// These keys would use the `LoginTypeToken` type.
	LoginTypeNone LoginType = "none"
)

type APIKeyScope string

const (
	// APIKeyScopeAll is a scope that allows the user to do everything.
	APIKeyScopeAll APIKeyScope = "all"
	// APIKeyScopeApplicationConnect is a scope that allows the user
	// to connect to applications in a workspace.
	APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
)

type CreateTokenRequest struct {
	Lifetime  time.Duration `json:"lifetime"`
	Scope     APIKeyScope   `json:"scope" enums:"all,application_connect"`
	TokenName string        `json:"token_name"`
}

// GenerateAPIKeyResponse contains an API key for a user.
type GenerateAPIKeyResponse struct {
	Key string `json:"key"`
}

// CreateToken generates an API key for the user ID provided with
// custom expiration. These tokens can be used for long-lived access,
// like for use with CI.
func (c *Client) CreateToken(ctx context.Context, userID string, req CreateTokenRequest) (GenerateAPIKeyResponse, error) {
	res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), req)
	if err != nil {
		return GenerateAPIKeyResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode > http.StatusCreated {
		return GenerateAPIKeyResponse{}, ReadBodyAsError(res)
	}

	var apiKey GenerateAPIKeyResponse
	return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
}

// CreateAPIKey generates an API key for the user ID provided.
// CreateToken should be used over CreateAPIKey. CreateToken allows better
// tracking of the token's usage and allows for custom expiration.
// Only use CreateAPIKey if you want to emulate the session created for
// a browser like login.
func (c *Client) CreateAPIKey(ctx context.Context, user string) (GenerateAPIKeyResponse, error) {
	res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil)
	if err != nil {
		return GenerateAPIKeyResponse{}, err
	}
	defer res.Body.Close()
	if res.StatusCode > http.StatusCreated {
		return GenerateAPIKeyResponse{}, ReadBodyAsError(res)
	}

	var apiKey GenerateAPIKeyResponse
	return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
}

type TokensFilter struct {
	IncludeAll bool `json:"include_all"`
}

type APIKeyWithOwner struct {
	APIKey
	Username string `json:"username"`
}

type TokenConfig struct {
	MaxTokenLifetime time.Duration `json:"max_token_lifetime"`
}

// asRequestOption returns a function that can be used in (*Client).Request.
// It modifies the request query parameters.
func (f TokensFilter) asRequestOption() RequestOption {
	return func(r *http.Request) {
		q := r.URL.Query()
		q.Set("include_all", fmt.Sprintf("%t", f.IncludeAll))
		r.URL.RawQuery = q.Encode()
	}
}

// Tokens list machine API keys.
func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) ([]APIKeyWithOwner, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil, filter.asRequestOption())
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode > http.StatusOK {
		return nil, ReadBodyAsError(res)
	}
	apiKey := []APIKeyWithOwner{}
	return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
}

// APIKeyByID returns the api key by id.
func (c *Client) APIKeyByID(ctx context.Context, userID string, id string) (*APIKey, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode > http.StatusCreated {
		return nil, ReadBodyAsError(res)
	}
	apiKey := &APIKey{}
	return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
}

// APIKeyByName returns the api key by name.
func (c *Client) APIKeyByName(ctx context.Context, userID string, name string) (*APIKey, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens/%s", userID, name), nil)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode > http.StatusCreated {
		return nil, ReadBodyAsError(res)
	}
	apiKey := &APIKey{}
	return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
}

// DeleteAPIKey deletes API key by id.
func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) error {
	res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode > http.StatusNoContent {
		return ReadBodyAsError(res)
	}
	return nil
}

// GetTokenConfig returns deployment options related to token management
func (c *Client) GetTokenConfig(ctx context.Context, userID string) (TokenConfig, error) {
	res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens/tokenconfig", userID), nil)
	if err != nil {
		return TokenConfig{}, err
	}
	defer res.Body.Close()
	if res.StatusCode > http.StatusOK {
		return TokenConfig{}, ReadBodyAsError(res)
	}
	tokenConfig := TokenConfig{}
	return tokenConfig, json.NewDecoder(res.Body).Decode(&tokenConfig)
}
