package rendering

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"net/url"
	"time"

	"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
)

var (
	domPurifySvgConfig = map[string]any{
		// domPurifyConfig is passed directly to DOMPurify https://github.com/cure53/DOMPurify#can-i-configure-dompurify
		"domPurifyConfig": map[string]any{
			"USE_PROFILES": map[string]bool{"svg": true, "svgFilters": true},
			"ADD_TAGS":     []string{"use"},
		},
		// allowAllLinksInSvgUseTags will preserve all `use` tags.
		// By default, we remove all non-self-referential `use` tags, i.e. those which `href` attribute does not start with `#`
		"allowAllLinksInSvgUseTags": false,
	}
	domPurifyConfigType = "DOMPurify"
)

type formFile struct {
	fileName    string
	key         string
	contentType string
	content     io.Reader
}

func createMultipartRequestBody(values []formFile) (bytes.Buffer, string, error) {
	var b bytes.Buffer
	w := multipart.NewWriter(&b)
	for _, f := range values {
		h := make(textproto.MIMEHeader)
		h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, f.key, f.fileName))
		h.Set("Content-Type", f.contentType)
		formWriter, err := w.CreatePart(h)

		if err != nil {
			return bytes.Buffer{}, "", err
		}

		if _, err := io.Copy(formWriter, f.content); err != nil {
			return bytes.Buffer{}, "", err
		}

		if x, ok := f.content.(io.Closer); ok {
			_ = x.Close()
		}
	}

	if err := w.Close(); err != nil {
		return bytes.Buffer{}, "", err
	}

	return b, w.FormDataContentType(), nil
}

func (rs *RenderingService) sanitizeViaHTTP(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) {
	sanitizerUrl, err := url.Parse(rs.sanitizeURL)
	if err != nil {
		return nil, err
	}

	configJson, err := json.Marshal(map[string]any{
		"config":     domPurifySvgConfig,
		"configType": domPurifyConfigType,
	})
	if err != nil {
		rs.log.Error("Sanitizer - HTTP: failed to create the request config", "error", err, "filename", req.Filename)
		return nil, fmt.Errorf("config creation fail: %s", err)
	}

	body, contentType, err := createMultipartRequestBody([]formFile{
		{
			fileName:    "config",
			key:         "config",
			contentType: "application/json",
			content:     bytes.NewReader(configJson),
		},
		{
			fileName:    req.Filename,
			key:         "file",
			contentType: "image/svg+xml",
			content:     bytes.NewReader(req.Content),
		},
	})
	if err != nil {
		rs.log.Error("Sanitizer - HTTP: failed to create the request body", "error", err, "filename", req.Filename)
		return nil, fmt.Errorf("body creation fail: %s", err)
	}

	reqContext, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()
	httpReq, err := http.NewRequestWithContext(reqContext, "POST", sanitizerUrl.String(), &body)
	if err != nil {
		rs.log.Error("Sanitizer - HTTP: failed to create the HTTP request", "error", err, "filename", req.Filename)
		return nil, err
	}

	httpReq.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", rs.Cfg.BuildVersion))
	httpReq.Header.Set("Content-Type", contentType)

	rs.log.Debug("Sanitizer - HTTP: calling", "filename", req.Filename, "contentLength", len(req.Content), "url", sanitizerUrl)
	// make request to renderer server
	resp, err := netClient.Do(httpReq)
	if err != nil {
		rs.log.Error("Sanitizer - HTTP: failed to send request", "error", err)
		return nil, fmt.Errorf("sanitizer - HTTP: failed to send request: %w", err)
	}

	defer func() {
		if err := resp.Body.Close(); err != nil {
			rs.log.Error("Sanitizer - HTTP: failed to close response body", "statusCode", resp.StatusCode, "error", err)
		}
	}()

	if resp.StatusCode != http.StatusOK {
		if body, err := io.ReadAll(resp.Body); body != nil {
			rs.log.Error("Sanitizer - HTTP: failed to sanitize", "statusCode", resp.StatusCode, "error", err, "resp", string(body))
		} else {
			rs.log.Error("Sanitizer - HTTP: failed to sanitize", "statusCode", resp.StatusCode, "error", err)
		}
		return nil, fmt.Errorf("sanitizer - HTTP: failed to sanitize %s", req.Filename)
	}

	sanitized, err := io.ReadAll(resp.Body)
	if err != nil {
		rs.log.Error("Sanitizer - HTTP: failed to read response body", "error", err, "filename", req.Filename)
		return nil, fmt.Errorf("sanitizer - HTTP: failed to read response body: %s", err)
	}

	return &SanitizeSVGResponse{Sanitized: sanitized}, nil
}

func (rs *RenderingService) sanitizeSVGViaPlugin(ctx context.Context, req *SanitizeSVGRequest) (*SanitizeSVGResponse, error) {
	ctx, cancel := context.WithTimeout(ctx, time.Second*20)
	defer cancel()

	domPurifyConfig, err := json.Marshal(domPurifySvgConfig)
	if err != nil {
		rs.log.Error("Sanitizer - plugin: failed to parse domPurifyConfig")
		return nil, fmt.Errorf("sanitizer - plugin: failed to parse domPurifyConfig %s", err)
	}
	grpcReq := &pluginextensionv2.SanitizeRequest{
		Filename:   req.Filename,
		Content:    req.Content,
		ConfigType: domPurifyConfigType,
		Config:     domPurifyConfig,
	}
	rs.log.Debug("Sanitizer - plugin: calling", "filename", req.Filename, "contentLength", len(req.Content))

	rc, err := rs.plugin.Client()
	if err != nil {
		return nil, err
	}
	rsp, err := rc.Sanitize(ctx, grpcReq)
	if err != nil {
		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
			rs.log.Info("Sanitizer - plugin: time out")
			return nil, ErrTimeout
		}

		return nil, err
	}

	if rsp.Error != "" {
		return nil, fmt.Errorf("sanitizer - plugin: failed to sanitize: %s", rsp.Error)
	}

	return &SanitizeSVGResponse{Sanitized: rsp.Sanitized}, nil
}
