package cloudflare

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

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

var (
	ErrInvalidImagesAPIVersion = errors.New("invalid images API version")
	ErrMissingImageID          = errors.New("required image ID missing")
)

type ImagesAPIVersion string

const (
	ImagesAPIVersionV1 ImagesAPIVersion = "v1"
	ImagesAPIVersionV2 ImagesAPIVersion = "v2"
)

// Image represents a Cloudflare Image.
type Image struct {
	ID                string                 `json:"id"`
	Filename          string                 `json:"filename"`
	Meta              map[string]interface{} `json:"meta,omitempty"`
	RequireSignedURLs bool                   `json:"requireSignedURLs"`
	Variants          []string               `json:"variants"`
	Uploaded          time.Time              `json:"uploaded"`
}

// UploadImageParams is the data required for an Image Upload request.
type UploadImageParams struct {
	File              io.ReadCloser
	URL               string
	Name              string
	RequireSignedURLs bool
	Metadata          map[string]interface{}
}

// write writes the image upload data to a multipart writer, so
// it can be used in an HTTP request.
func (b UploadImageParams) write(mpw *multipart.Writer) error {
	if b.File == nil && b.URL == "" {
		return errors.New("a file or url to upload must be specified")
	}

	if b.File != nil {
		name := b.Name
		part, err := mpw.CreateFormFile("file", name)
		if err != nil {
			return err
		}
		_, err = io.Copy(part, b.File)
		if err != nil {
			_ = b.File.Close()
			return err
		}
		_ = b.File.Close()
	}

	if b.URL != "" {
		err := mpw.WriteField("url", b.URL)
		if err != nil {
			return err
		}
	}

	// According to the Cloudflare docs, this field defaults to false.
	// For simplicity, we will only send it if the value is true, however
	// if the default is changed to true, this logic will need to be updated.
	if b.RequireSignedURLs {
		err := mpw.WriteField("requireSignedURLs", "true")
		if err != nil {
			return err
		}
	}

	if b.Metadata != nil {
		part, err := mpw.CreateFormField("metadata")
		if err != nil {
			return err
		}
		err = json.NewEncoder(part).Encode(b.Metadata)
		if err != nil {
			return err
		}
	}

	return nil
}

// UpdateImageParams is the data required for an UpdateImage request.
type UpdateImageParams struct {
	ID                string                 `json:"-"`
	RequireSignedURLs bool                   `json:"requireSignedURLs"`
	Metadata          map[string]interface{} `json:"metadata,omitempty"`
}

// CreateImageDirectUploadURLParams is the data required for a CreateImageDirectUploadURL request.
type CreateImageDirectUploadURLParams struct {
	Version           ImagesAPIVersion       `json:"-"`
	Expiry            *time.Time             `json:"expiry,omitempty"`
	Metadata          map[string]interface{} `json:"metadata,omitempty"`
	RequireSignedURLs *bool                  `json:"requireSignedURLs,omitempty"`
}

// ImageDirectUploadURLResponse is the API response for a direct image upload url.
type ImageDirectUploadURLResponse struct {
	Result ImageDirectUploadURL `json:"result"`
	Response
}

// ImageDirectUploadURL .
type ImageDirectUploadURL struct {
	ID        string `json:"id"`
	UploadURL string `json:"uploadURL"`
}

// ImagesListResponse is the API response for listing all images.
type ImagesListResponse struct {
	Result struct {
		Images []Image `json:"images"`
	} `json:"result"`
	Response
}

// ImageDetailsResponse is the API response for getting an image's details.
type ImageDetailsResponse struct {
	Result Image `json:"result"`
	Response
}

// ImagesStatsResponse is the API response for image stats.
type ImagesStatsResponse struct {
	Result struct {
		Count ImagesStatsCount `json:"count"`
	} `json:"result"`
	Response
}

// ImagesStatsCount is the stats attached to a ImagesStatsResponse.
type ImagesStatsCount struct {
	Current int64 `json:"current"`
	Allowed int64 `json:"allowed"`
}

type ListImagesParams struct {
	ResultInfo
}

// UploadImage uploads a single image.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-upload-an-image-using-a-single-http-request
func (api *API) UploadImage(ctx context.Context, rc *ResourceContainer, params UploadImageParams) (Image, error) {
	if rc.Level != AccountRouteLevel {
		return Image{}, ErrRequiredAccountLevelResourceContainer
	}

	if params.File != nil && params.URL != "" {
		return Image{}, errors.New("file and url uploads are mutually exclusive and can only be performed individually")
	}

	uri := fmt.Sprintf("/accounts/%s/images/v1", rc.Identifier)

	body := &bytes.Buffer{}
	w := multipart.NewWriter(body)
	if err := params.write(w); err != nil {
		_ = w.Close()
		return Image{}, fmt.Errorf("error writing multipart body: %w", err)
	}
	_ = w.Close()

	res, err := api.makeRequestContextWithHeaders(
		ctx,
		http.MethodPost,
		uri,
		body,
		http.Header{
			"Accept":       []string{"application/json"},
			"Content-Type": []string{w.FormDataContentType()},
		},
	)
	if err != nil {
		return Image{}, err
	}

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

// UpdateImage updates an existing image's metadata.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-update-image
func (api *API) UpdateImage(ctx context.Context, rc *ResourceContainer, params UpdateImageParams) (Image, error) {
	if rc.Level != AccountRouteLevel {
		return Image{}, ErrRequiredAccountLevelResourceContainer
	}

	uri := fmt.Sprintf("/accounts/%s/images/v1/%s", rc.Identifier, params.ID)

	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
	if err != nil {
		return Image{}, err
	}

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

var imagesMultipartBoundary = "----CloudflareImagesGoClientBoundary"

// CreateImageDirectUploadURL creates an authenticated direct upload url.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-create-authenticated-direct-upload-url
func (api *API) CreateImageDirectUploadURL(ctx context.Context, rc *ResourceContainer, params CreateImageDirectUploadURLParams) (ImageDirectUploadURL, error) {
	if rc.Level != AccountRouteLevel {
		return ImageDirectUploadURL{}, ErrRequiredAccountLevelResourceContainer
	}

	if params.Version != "" && params.Version != ImagesAPIVersionV1 && params.Version != ImagesAPIVersionV2 {
		return ImageDirectUploadURL{}, ErrInvalidImagesAPIVersion
	}

	var err error
	var uri string
	var res []byte
	switch params.Version {
	case ImagesAPIVersionV2:
		uri = fmt.Sprintf("/%s/%s/images/%s/direct_upload", rc.Level, rc.Identifier, params.Version)
		body := &bytes.Buffer{}
		writer := multipart.NewWriter(body)
		if err := writer.SetBoundary(imagesMultipartBoundary); err != nil {
			return ImageDirectUploadURL{}, fmt.Errorf("error setting multipart boundary")
		}

		if *params.RequireSignedURLs {
			if err = writer.WriteField("requireSignedURLs", "true"); err != nil {
				return ImageDirectUploadURL{}, fmt.Errorf("error writing requireSignedURLs field: %w", err)
			}
		}
		if !params.Expiry.IsZero() {
			if err = writer.WriteField("expiry", params.Expiry.Format(time.RFC3339)); err != nil {
				return ImageDirectUploadURL{}, fmt.Errorf("error writing expiry field: %w", err)
			}
		}
		if params.Metadata != nil {
			var metadataBytes []byte
			if metadataBytes, err = json.Marshal(params.Metadata); err != nil {
				return ImageDirectUploadURL{}, fmt.Errorf("error marshalling metadata to JSON: %w", err)
			}
			if err = writer.WriteField("metadata", string(metadataBytes)); err != nil {
				return ImageDirectUploadURL{}, fmt.Errorf("error writing metadata field: %w", err)
			}
		}
		if err = writer.Close(); err != nil {
			return ImageDirectUploadURL{}, fmt.Errorf("error closing multipart writer: %w", err)
		}

		res, err = api.makeRequestContextWithHeaders(
			ctx,
			http.MethodPost,
			uri,
			body,
			http.Header{
				"Accept":       []string{"application/json"},
				"Content-Type": []string{writer.FormDataContentType()},
			},
		)
	case ImagesAPIVersionV1:
	case "":
		uri = fmt.Sprintf("/%s/%s/images/%s/direct_upload", rc.Level, rc.Identifier, ImagesAPIVersionV1)
		res, err = api.makeRequestContext(ctx, http.MethodPost, uri, params)
	default:
		return ImageDirectUploadURL{}, ErrInvalidImagesAPIVersion
	}

	if err != nil {
		return ImageDirectUploadURL{}, err
	}

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

// ListImages lists all images.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-list-images
func (api *API) ListImages(ctx context.Context, rc *ResourceContainer, params ListImagesParams) ([]Image, error) {
	uri := buildURI(fmt.Sprintf("/accounts/%s/images/v1", rc.Identifier), params)

	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return []Image{}, err
	}

	var imagesListResponse ImagesListResponse
	err = json.Unmarshal(res, &imagesListResponse)
	if err != nil {
		return []Image{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
	}
	return imagesListResponse.Result.Images, nil
}

// GetImage gets the details of an uploaded image.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-image-details
func (api *API) GetImage(ctx context.Context, rc *ResourceContainer, id string) (Image, error) {
	if rc.Level != AccountRouteLevel {
		return Image{}, ErrRequiredAccountLevelResourceContainer
	}

	uri := fmt.Sprintf("/accounts/%s/images/v1/%s", rc.Identifier, id)

	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return Image{}, err
	}

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

// GetBaseImage gets the base image used to derive variants.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-base-image
func (api *API) GetBaseImage(ctx context.Context, rc *ResourceContainer, id string) ([]byte, error) {
	if rc.Level != AccountRouteLevel {
		return []byte{}, ErrRequiredAccountLevelResourceContainer
	}

	uri := fmt.Sprintf("/accounts/%s/images/v1/%s/blob", rc.Identifier, id)

	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return nil, err
	}
	return res, nil
}

// DeleteImage deletes an image.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-delete-image
func (api *API) DeleteImage(ctx context.Context, rc *ResourceContainer, id string) error {
	if rc.Level != AccountRouteLevel {
		return ErrRequiredAccountLevelResourceContainer
	}

	uri := fmt.Sprintf("/accounts/%s/images/v1/%s", rc.Identifier, id)

	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
	if err != nil {
		return err
	}
	return nil
}

// GetImagesStats gets an account's statistics for Cloudflare Images.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-images-usage-statistics
func (api *API) GetImagesStats(ctx context.Context, rc *ResourceContainer) (ImagesStatsCount, error) {
	if rc.Level != AccountRouteLevel {
		return ImagesStatsCount{}, ErrRequiredAccountLevelResourceContainer
	}

	uri := fmt.Sprintf("/accounts/%s/images/v1/stats", rc.Identifier)

	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return ImagesStatsCount{}, err
	}

	var imagesStatsResponse ImagesStatsResponse
	err = json.Unmarshal(res, &imagesStatsResponse)
	if err != nil {
		return ImagesStatsCount{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
	}
	return imagesStatsResponse.Result.Count, nil
}
