package apiclient

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"path/filepath"
	"strings"
	"time"
)

// DefaultStreamingResponseHeaderTimeout bounds the time a streamed request may
// wait for response headers without imposing a total timeout on body upload.
const DefaultStreamingResponseHeaderTimeout = 30 * time.Second

// NewStreamingHTTPClient derives a client that preserves the base client's
// redirect and cookie policy, removes its body-inclusive total timeout, and
// applies a response-header-only timeout at the transport boundary.
func NewStreamingHTTPClient(base *http.Client, responseHeaderTimeout time.Duration) *http.Client {
	if base == nil {
		base = http.DefaultClient
	}
	streaming := *base
	streaming.Timeout = 0
	streaming.Transport = newStreamingResponseHeaderTransport(
		base.Transport, responseHeaderTimeout,
	)
	return &streaming
}

func newStreamingResponseHeaderTransport(
	base http.RoundTripper,
	responseHeaderTimeout time.Duration,
) http.RoundTripper {
	if base == nil {
		base = http.DefaultTransport
	}
	if transport, ok := base.(*http.Transport); ok && transport != nil {
		cloned := transport.Clone()
		cloned.ResponseHeaderTimeout = responseHeaderTimeout
		return cloned
	}
	return rejectingStreamingRoundTripper{}
}

type rejectingStreamingRoundTripper struct{}

func (rejectingStreamingRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
	return nil, errors.New("streaming HTTP client requires an *http.Transport")
}

// PostMultipartStream sends a multipart form without materializing it in
// memory. writeBody must be replayable: it is called again only when an HTTP
// 401 causes the Agent token to be refreshed before the request is retried.
func (a *Client) PostMultipartStream(
	ctx context.Context,
	path string,
	writeBody func(*multipart.Writer) error,
	result any,
) error {
	return a.execute(ctx, "POST multipart stream "+path, attemptAuthRefreshOnly, func() error {
		return a.doPostMultipartStream(ctx, path, writeBody, result)
	})
}

func (a *Client) doPostMultipartStream(
	ctx context.Context,
	path string,
	writeBody func(*multipart.Writer) error,
	result any,
) error {
	if writeBody == nil {
		return fmt.Errorf("create multipart stream: body writer is required")
	}
	reader, pipeWriter := io.Pipe()
	writer := multipart.NewWriter(pipeWriter)
	requestPath := a.requestPath(path)
	endpoint := a.ServerURL + requestPath
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reader)
	if err != nil {
		_ = reader.Close()
		_ = pipeWriter.Close()
		return fmt.Errorf("create multipart request: %w", err)
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())
	a.setRequestHeaders(req, requestPath)

	writeDone := make(chan error, 1)
	go func() {
		writeErr := writeBody(writer)
		if writeErr == nil {
			writeErr = writer.Close()
		}
		if writeErr != nil {
			_ = pipeWriter.CloseWithError(writeErr)
		} else {
			_ = pipeWriter.Close()
		}
		writeDone <- writeErr
	}()

	client := a.StreamingHTTPClient
	if client == nil {
		client = NewStreamingHTTPClient(a.HTTPClient, DefaultStreamingResponseHeaderTimeout)
	}
	resp, requestErr := client.Do(req)
	_ = reader.Close()
	writeErr := <-writeDone
	if requestErr != nil {
		if writeErr != nil {
			return fmt.Errorf("write multipart stream: %w", writeErr)
		}
		return fmt.Errorf("multipart request: %w", requestErr)
	}
	defer closeResponseBody(resp.Body)

	if resp.StatusCode >= 400 {
		return apiErrorFromResponse(resp, http.MethodPost, requestPath)
	}
	if writeErr != nil {
		return fmt.Errorf("write multipart stream: %w", writeErr)
	}
	if result != nil {
		return json.NewDecoder(resp.Body).Decode(result)
	}
	return nil
}

func uploadContentTypeForPath(filePath string) string {
	if contentType := mime.TypeByExtension(strings.ToLower(filepath.Ext(filePath))); strings.TrimSpace(contentType) != "" {
		return contentType
	}
	return "application/octet-stream"
}
