package cloudflare

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"strings"
	"time"

	"github.com/goccy/go-json"
)

// WorkerRequestParams provides parameters for worker requests for both enterprise and standard requests.
type WorkerRequestParams struct {
	ZoneID     string
	ScriptName string
}

type CreateWorkerParams struct {
	ScriptName string
	Script     string

	// DispatchNamespaceName uploads the worker to a WFP dispatch namespace if provided
	DispatchNamespaceName *string

	// Module changes the Content-Type header to specify the script is an
	// ES Module syntax script.
	Module bool

	// Logpush opts the worker into Workers Logpush logging. A nil value leaves
	// the current setting unchanged.
	//
	// Documentation: https://developers.cloudflare.com/workers/platform/logpush/
	Logpush *bool

	// TailConsumers specifies a list of Workers that will consume the logs of
	// the attached Worker.
	// Documentation: https://developers.cloudflare.com/workers/platform/tail-workers/
	TailConsumers *[]WorkersTailConsumer

	// Bindings should be a map where the keys are the binding name, and the
	// values are the binding content
	Bindings map[string]WorkerBinding

	// CompatibilityDate is a date in the form yyyy-mm-dd,
	// which will be used to determine which version of the Workers runtime is used.
	//  https://developers.cloudflare.com/workers/platform/compatibility-dates/
	CompatibilityDate string

	// CompatibilityFlags are the names of features of the Workers runtime to be enabled or disabled,
	// usually used together with CompatibilityDate.
	//  https://developers.cloudflare.com/workers/platform/compatibility-dates/#compatibility-flags
	CompatibilityFlags []string

	Placement *Placement

	// Tags are used to better manage CRUD operations at scale.
	//  https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/platform/tags/
	Tags []string
}

func (p CreateWorkerParams) RequiresMultipart() bool {
	switch {
	case p.Module:
		return true
	case p.Logpush != nil:
		return true
	case p.Placement != nil:
		return true
	case len(p.Bindings) > 0:
		return true
	case p.CompatibilityDate != "":
		return true
	case len(p.CompatibilityFlags) > 0:
		return true
	case p.TailConsumers != nil:
		return true
	case len(p.Tags) > 0:
		return true
	}

	return false
}

type UpdateWorkersScriptContentParams struct {
	ScriptName string
	Script     string

	// DispatchNamespaceName uploads the worker to a WFP dispatch namespace if provided
	DispatchNamespaceName *string

	// Module changes the Content-Type header to specify the script is an
	// ES Module syntax script.
	Module bool
}

type UpdateWorkersScriptSettingsParams struct {
	ScriptName string

	// Logpush opts the worker into Workers Logpush logging. A nil value leaves
	// the current setting unchanged.
	//
	// Documentation: https://developers.cloudflare.com/workers/platform/logpush/
	Logpush *bool

	// TailConsumers specifies a list of Workers that will consume the logs of
	// the attached Worker.
	// Documentation: https://developers.cloudflare.com/workers/platform/tail-workers/
	TailConsumers *[]WorkersTailConsumer

	// Bindings should be a map where the keys are the binding name, and the
	// values are the binding content
	Bindings map[string]WorkerBinding

	// CompatibilityDate is a date in the form yyyy-mm-dd,
	// which will be used to determine which version of the Workers runtime is used.
	//  https://developers.cloudflare.com/workers/platform/compatibility-dates/
	CompatibilityDate string

	// CompatibilityFlags are the names of features of the Workers runtime to be enabled or disabled,
	// usually used together with CompatibilityDate.
	//  https://developers.cloudflare.com/workers/platform/compatibility-dates/#compatibility-flags
	CompatibilityFlags []string

	Placement *Placement
}

// WorkerScriptParams provides a worker script and the associated bindings.
type WorkerScriptParams struct {
	ScriptName string

	// Module changes the Content-Type header to specify the script is an
	// ES Module syntax script.
	Module bool

	// Bindings should be a map where the keys are the binding name, and the
	// values are the binding content
	Bindings map[string]WorkerBinding
}

// WorkerRoute is used to map traffic matching a URL pattern to a workers
//
// API reference: https://api.cloudflare.com/#worker-routes-properties
type WorkerRoute struct {
	ID         string `json:"id,omitempty"`
	Pattern    string `json:"pattern"`
	ScriptName string `json:"script,omitempty"`
}

// WorkerRoutesResponse embeds Response struct and slice of WorkerRoutes.
type WorkerRoutesResponse struct {
	Response
	Routes []WorkerRoute `json:"result"`
}

// WorkerRouteResponse embeds Response struct and a single WorkerRoute.
type WorkerRouteResponse struct {
	Response
	WorkerRoute `json:"result"`
}

// WorkerScript Cloudflare Worker struct with metadata.
type WorkerScript struct {
	WorkerMetaData
	Script     string `json:"script"`
	UsageModel string `json:"usage_model,omitempty"`
}

type WorkersTailConsumer struct {
	Service     string  `json:"service"`
	Environment *string `json:"environment,omitempty"`
	Namespace   *string `json:"namespace,omitempty"`
}

// WorkerMetaData contains worker script information such as size, creation & modification dates.
type WorkerMetaData struct {
	ID               string                 `json:"id,omitempty"`
	ETAG             string                 `json:"etag,omitempty"`
	Size             int                    `json:"size,omitempty"`
	CreatedOn        time.Time              `json:"created_on,omitempty"`
	ModifiedOn       time.Time              `json:"modified_on,omitempty"`
	Logpush          *bool                  `json:"logpush,omitempty"`
	TailConsumers    *[]WorkersTailConsumer `json:"tail_consumers,omitempty"`
	LastDeployedFrom *string                `json:"last_deployed_from,omitempty"`
	DeploymentId     *string                `json:"deployment_id,omitempty"`
	PlacementFields
	PipelineHash *string `json:"pipeline_hash,omitempty"`
}

// WorkerListResponse wrapper struct for API response to worker script list API call.
type WorkerListResponse struct {
	Response
	ResultInfo
	WorkerList []WorkerMetaData `json:"result"`
}

// WorkerScriptResponse wrapper struct for API response to worker script calls.
type WorkerScriptResponse struct {
	Response
	Module       bool
	WorkerScript `json:"result"`
}

// WorkerScriptSettingsResponse wrapper struct for API response to worker script settings calls.
type WorkerScriptSettingsResponse struct {
	Response
	WorkerMetaData
}

type ListWorkersParams struct{}

type DeleteWorkerParams struct {
	ScriptName string

	// DispatchNamespaceName is the dispatch namespace the Worker is uploaded to.
	DispatchNamespace *string
}

type PlacementMode string

const (
	PlacementModeOff   PlacementMode = ""
	PlacementModeSmart PlacementMode = "smart"
)

type PlacementStatus string

// Placement contains all the worker placement information.
type Placement struct {

	// Mode is the placement mode for the worker (e.g. "smart").
	Mode PlacementMode `json:"mode"`

	// Status is the status of the placement (readonly).
	Status PlacementStatus `json:"status,omitempty"`
}

// PlacementFields contains all the worker placement fields (deprecated and nested).
// This struct is meant to be embedded, it exists for locality of deprecated and regular fields.
type PlacementFields struct {

	// PlacementMode.
	//
	// Deprecated: Use Placement.Mode instead.
	PlacementMode *PlacementMode `json:"placement_mode,omitempty"`

	// Placement.
	Placement *Placement `json:"placement,omitempty"`
}

// DeleteWorker deletes a single Worker.
//
// API reference: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/delete/
func (api *API) DeleteWorker(ctx context.Context, rc *ResourceContainer, params DeleteWorkerParams) error {
	if rc.Level != AccountRouteLevel {
		return ErrRequiredAccountLevelResourceContainer
	}

	if rc.Identifier == "" {
		return ErrMissingAccountID
	}

	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, params.ScriptName)
	if params.DispatchNamespace != nil && *params.DispatchNamespace != "" {
		uri = fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces/%s/scripts/%s", rc.Identifier, *params.DispatchNamespace, params.ScriptName)
	}

	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)

	var r WorkerScriptResponse
	if err != nil {
		return err
	}

	err = json.Unmarshal(res, &r)
	if err != nil {
		return fmt.Errorf("%s: %w", errUnmarshalError, err)
	}

	return nil
}

// GetWorker fetch raw script content for your worker returns string containing
// worker code js.
//
// API reference: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/get/
func (api *API) GetWorker(ctx context.Context, rc *ResourceContainer, scriptName string) (WorkerScriptResponse, error) {
	return api.GetWorkerWithDispatchNamespace(ctx, rc, scriptName, "")
}

// GetWorker fetch raw script content for your worker returns string containing
// worker code js.
//
// API reference: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/get/
func (api *API) GetWorkerWithDispatchNamespace(ctx context.Context, rc *ResourceContainer, scriptName string, dispatchNamespace string) (WorkerScriptResponse, error) {
	if rc.Level != AccountRouteLevel {
		return WorkerScriptResponse{}, ErrRequiredAccountLevelResourceContainer
	}

	if rc.Identifier == "" {
		return WorkerScriptResponse{}, ErrMissingAccountID
	}

	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, scriptName)
	if dispatchNamespace != "" {
		uri = fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces/%s/scripts/%s/content", rc.Identifier, dispatchNamespace, scriptName)
	}
	res, err := api.makeRequestContextWithHeadersComplete(ctx, http.MethodGet, uri, nil, nil)
	var r WorkerScriptResponse
	if err != nil {
		return r, err
	}

	// Check if the response type is multipart, in which case this was a module worker
	mediaType, mediaParams, _ := mime.ParseMediaType(res.Headers.Get("content-type"))
	if strings.HasPrefix(mediaType, "multipart/") {
		bytesReader := bytes.NewReader(res.Body)
		mimeReader := multipart.NewReader(bytesReader, mediaParams["boundary"])
		mimePart, err := mimeReader.NextPart()
		if err != nil {
			return r, fmt.Errorf("could not get multipart response body: %w", err)
		}
		mimePartBody, err := io.ReadAll(mimePart)
		if err != nil {
			return r, fmt.Errorf("could not read multipart response body: %w", err)
		}
		r.Script = string(mimePartBody)
		r.Module = true
	} else {
		r.Script = string(res.Body)
		r.Module = false
	}

	r.Success = true
	return r, nil
}

// ListWorkers returns list of Workers for given account.
//
// API reference: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/list/
func (api *API) ListWorkers(ctx context.Context, rc *ResourceContainer, params ListWorkersParams) (WorkerListResponse, *ResultInfo, error) {
	if rc.Level != AccountRouteLevel {
		return WorkerListResponse{}, &ResultInfo{}, ErrRequiredAccountLevelResourceContainer
	}

	if rc.Identifier == "" {
		return WorkerListResponse{}, &ResultInfo{}, ErrMissingAccountID
	}

	uri := fmt.Sprintf("/accounts/%s/workers/scripts", rc.Identifier)
	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return WorkerListResponse{}, &ResultInfo{}, err
	}

	var r WorkerListResponse
	err = json.Unmarshal(res, &r)
	if err != nil {
		return WorkerListResponse{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
	}

	return r, &r.ResultInfo, nil
}

// UploadWorker pushes raw script content for your Worker.
//
// API reference: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/
func (api *API) UploadWorker(ctx context.Context, rc *ResourceContainer, params CreateWorkerParams) (WorkerScriptResponse, error) {
	if rc.Level != AccountRouteLevel {
		return WorkerScriptResponse{}, ErrRequiredAccountLevelResourceContainer
	}

	if rc.Identifier == "" {
		return WorkerScriptResponse{}, ErrMissingAccountID
	}

	body := []byte(params.Script)
	var (
		contentType = "application/javascript"
		err         error
	)

	if params.RequiresMultipart() {
		contentType, body, err = formatMultipartBody(params)
		if err != nil {
			return WorkerScriptResponse{}, err
		}
	}

	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", rc.Identifier, params.ScriptName)
	if params.DispatchNamespaceName != nil && *params.DispatchNamespaceName != "" {
		uri = fmt.Sprintf("/accounts/%s/workers/dispatch/namespaces/%s/scripts/%s", rc.Identifier, *params.DispatchNamespaceName, params.ScriptName)
	}

	headers := make(http.Header)
	headers.Set("Content-Type", contentType)
	res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers)

	var r WorkerScriptResponse
	if err != nil {
		return r, err
	}

	err = json.Unmarshal(res, &r)
	if err != nil {
		return r, fmt.Errorf("%s: %w", errUnmarshalError, err)
	}

	return r, nil
}

// GetWorkersScriptContent returns the pure script content of a worker.
//
// API reference: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/content/methods/get/
func (api *API) GetWorkersScriptContent(ctx context.Context, rc *ResourceContainer, scriptName string) (string, error) {
	if rc.Level != AccountRouteLevel {
		return "", ErrRequiredAccountLevelResourceContainer
	}

	if rc.Identifier == "" {
		return "", ErrMissingAccountID
	}

	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/content/v2", rc.Identifier, scriptName)
	res, err := api.makeRequestContextWithHeadersComplete(ctx, http.MethodGet, uri, nil, nil)
	if err != nil {
		return "", err
	}

	return string(res.Body), nil
}

// UpdateWorkersScriptContent pushes only script content, no metadata.
//
// API reference: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/content/methods/update/
func (api *API) UpdateWorkersScriptContent(ctx context.Context, rc *ResourceContainer, params UpdateWorkersScriptContentParams) (WorkerScriptResponse, error) {
	if rc.Level != AccountRouteLevel {
		return WorkerScriptResponse{}, ErrRequiredAccountLevelResourceContainer
	}

	if rc.Identifier == "" {
		return WorkerScriptResponse{}, ErrMissingAccountID
	}

	body := []byte(params.Script)
	var (
		contentType = "application/javascript"
		err         error
	)

	if params.Module {
		var formattedParams CreateWorkerParams
		formattedParams.Script = params.Script
		formattedParams.ScriptName = params.ScriptName
		formattedParams.Module = params.Module
		formattedParams.DispatchNamespaceName = params.DispatchNamespaceName
		contentType, body, err = formatMultipartBody(formattedParams)
		if err != nil {
			return WorkerScriptResponse{}, err
		}
	}

	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/content", rc.Identifier, params.ScriptName)
	if params.DispatchNamespaceName != nil {
		uri = fmt.Sprintf("/accounts/%s/workers/dispatch_namespaces/%s/scripts/%s/content", rc.Identifier, *params.DispatchNamespaceName, params.ScriptName)
	}

	headers := make(http.Header)
	headers.Set("Content-Type", contentType)
	res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers)

	var r WorkerScriptResponse
	if err != nil {
		return r, err
	}

	err = json.Unmarshal(res, &r)
	if err != nil {
		return r, fmt.Errorf("%s: %w", errUnmarshalError, err)
	}

	return r, nil
}

// GetWorkersScriptSettings returns the metadata of a worker.
//
// API reference: https://developers.cloudflare.com/api/operations/worker-script-get-settings
func (api *API) GetWorkersScriptSettings(ctx context.Context, rc *ResourceContainer, scriptName string) (WorkerScriptSettingsResponse, error) {
	if rc.Level != AccountRouteLevel {
		return WorkerScriptSettingsResponse{}, ErrRequiredAccountLevelResourceContainer
	}

	if rc.Identifier == "" {
		return WorkerScriptSettingsResponse{}, ErrMissingAccountID
	}

	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/settings", rc.Identifier, scriptName)
	res, err := api.makeRequestContextWithHeaders(ctx, http.MethodGet, uri, nil, nil)
	var r WorkerScriptSettingsResponse
	if err != nil {
		return r, err
	}

	err = json.Unmarshal(res, &r)
	if err != nil {
		return r, fmt.Errorf("%s: %w", errUnmarshalError, err)
	}

	r.Success = true

	return r, nil
}

// UpdateWorkersScriptSettings pushes only script metadata.
//
// API reference: https://developers.cloudflare.com/api/operations/worker-script-patch-settings
func (api *API) UpdateWorkersScriptSettings(ctx context.Context, rc *ResourceContainer, params UpdateWorkersScriptSettingsParams) (WorkerScriptSettingsResponse, error) {
	if rc.Level != AccountRouteLevel {
		return WorkerScriptSettingsResponse{}, ErrRequiredAccountLevelResourceContainer
	}

	if rc.Identifier == "" {
		return WorkerScriptSettingsResponse{}, ErrMissingAccountID
	}

	body, err := json.Marshal(params)
	if err != nil {
		return WorkerScriptSettingsResponse{}, err
	}
	headers := make(http.Header)
	headers.Set("Content-Type", "application/json")

	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/settings", rc.Identifier, params.ScriptName)
	res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPatch, uri, body, headers)
	var r WorkerScriptSettingsResponse
	if err != nil {
		return r, err
	}

	err = json.Unmarshal(res, &r)
	if err != nil {
		return r, fmt.Errorf("%s: %w", errUnmarshalError, err)
	}

	r.Success = true

	return r, nil
}

// Returns content-type, body, error.
func formatMultipartBody(params CreateWorkerParams) (string, []byte, error) {
	var buf = &bytes.Buffer{}
	var mpw = multipart.NewWriter(buf)
	defer mpw.Close()

	// Write metadata part
	var scriptPartName string
	meta := struct {
		BodyPart           string                 `json:"body_part,omitempty"`
		MainModule         string                 `json:"main_module,omitempty"`
		Bindings           []workerBindingMeta    `json:"bindings"`
		Logpush            *bool                  `json:"logpush,omitempty"`
		TailConsumers      *[]WorkersTailConsumer `json:"tail_consumers,omitempty"`
		CompatibilityDate  string                 `json:"compatibility_date,omitempty"`
		CompatibilityFlags []string               `json:"compatibility_flags,omitempty"`
		Placement          *Placement             `json:"placement,omitempty"`
		Tags               []string               `json:"tags"`
	}{
		Bindings:           make([]workerBindingMeta, 0, len(params.Bindings)),
		Logpush:            params.Logpush,
		TailConsumers:      params.TailConsumers,
		CompatibilityDate:  params.CompatibilityDate,
		CompatibilityFlags: params.CompatibilityFlags,
		Placement:          params.Placement,
		Tags:               params.Tags,
	}

	if params.Module {
		scriptPartName = "worker.mjs"
		meta.MainModule = scriptPartName
	} else {
		scriptPartName = "script"
		meta.BodyPart = scriptPartName
	}

	bodyWriters := make([]workerBindingBodyWriter, 0, len(params.Bindings))
	for name, b := range params.Bindings {
		bindingMeta, bodyWriter, err := b.serialize(name)
		if err != nil {
			return "", nil, err
		}

		meta.Bindings = append(meta.Bindings, bindingMeta)
		bodyWriters = append(bodyWriters, bodyWriter)
	}

	var hdr = textproto.MIMEHeader{}
	hdr.Set("content-disposition", fmt.Sprintf(`form-data; name="%s"`, "metadata"))
	hdr.Set("content-type", "application/json")
	pw, err := mpw.CreatePart(hdr)
	if err != nil {
		return "", nil, err
	}
	metaJSON, err := json.Marshal(meta)
	if err != nil {
		return "", nil, err
	}
	_, err = pw.Write(metaJSON)
	if err != nil {
		return "", nil, err
	}

	// Write script part
	hdr = textproto.MIMEHeader{}

	contentType := "application/javascript"
	if params.Module {
		contentType = "application/javascript+module"
		hdr.Set("content-disposition", fmt.Sprintf(`form-data; name="%s"; filename="%[1]s"`, scriptPartName))
	} else {
		hdr.Set("content-disposition", fmt.Sprintf(`form-data; name="%s"`, scriptPartName))
	}
	hdr.Set("content-type", contentType)

	pw, err = mpw.CreatePart(hdr)
	if err != nil {
		return "", nil, err
	}
	_, err = pw.Write([]byte(params.Script))
	if err != nil {
		return "", nil, err
	}

	// Write other bindings with parts
	for _, w := range bodyWriters {
		if w != nil {
			err = w(mpw)
			if err != nil {
				return "", nil, err
			}
		}
	}

	mpw.Close()

	return mpw.FormDataContentType(), buf.Bytes(), nil
}
