package watch_aggregator //nolint:staticcheck

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strconv"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/httpz"
)

var (
	_ watchFactory   = (*basicWatchFactory)(nil)
	_ watchInterface = (*watch)(nil)
)

const (
	acceptedContentType = "application/json"
)

type basicWatchFactory struct {
	rt http.RoundTripper
}

func (f *basicWatchFactory) new(watchRequest *WatchRequest, sender watchResponseSender) watchInterface {
	return &watch{
		id:          watchRequest.WatchId,
		watchParams: watchRequest.WatchParams,
		sender:      sender,
		rt:          f.rt,
	}
}

type watch struct {
	id          string
	watchParams *WatchParams
	sender      watchResponseSender
	rt          http.RoundTripper
}

// handle the watch
// Receives data from the watch event stream
// and forwards events to the WebSocket response channel.
func (w *watch) handle(ctx context.Context) {
	r, err := newWatchRequest(ctx, w.watchParams)
	if err != nil {
		w.sender.sendError(w.id, watchRequestFailedErrorType, fmt.Errorf("unable to construct watch request: %w", err), nil)
		return
	}
	resp, err := w.rt.RoundTrip(r)
	if err != nil {
		w.sender.sendError(w.id, watchRequestFailedErrorType, fmt.Errorf("error making watch request: %w", err), nil)
		return
	}
	// at this point we don't care about any errors. Either we're done with the watch or we got a wrong content type or an error.
	defer resp.Body.Close() // //nolint: errcheck

	if ct := resp.Header.Get(httpz.ContentTypeHeader); !httpz.IsContentType(ct, acceptedContentType) {
		w.sender.sendError(w.id, watchFailedErrorType, fmt.Errorf("got unexpected %q content-type in watch response with status code %d", ct, resp.StatusCode), nil)
		return
	}

	switch resp.StatusCode {
	case http.StatusOK:
		w.receiveWatchEvents(ctx, resp.Body)
	default:
		w.receiveErrorResponse(resp.Body)
	}
}

func (w *watch) receiveWatchEvents(ctx context.Context, body io.ReadCloser) {
	decoder := newWatchDecoder(body)
	for {
		action, obj, err := decoder.Decode()
		if err != nil {
			switch err {
			case io.EOF: //nolint: errorlint
				w.sender.sendStop(w.id)
			case io.ErrUnexpectedEOF:
				w.sender.sendError(w.id, watchFailedErrorType, fmt.Errorf("unexpected EOF during watch stream event decoding: %w", err), nil)
			default:
				if ctx.Err() == nil {
					w.sender.sendError(w.id, watchFailedErrorType, fmt.Errorf("unable to decode an event from the watch stream: %w", err), nil)
				} else {
					// Read aborted due to normal cancellation.
					w.sender.sendStop(w.id)
				}
			}

			return
		}

		w.sender.sendEvent(w.id, &k8sWatchEvent{Type: action, Object: obj})
	}
}

func (w *watch) receiveErrorResponse(body io.Reader) {
	o, err := newWatchErrorDecoder(body).decode()
	if err != nil {
		w.sender.sendError(w.id, watchRequestFailedErrorType, fmt.Errorf("unable to decode watch error: %w", err), nil)
		return
	}

	w.sender.sendError(w.id, watchRequestFailedErrorType, errors.New("unable to watch"), o)
}

func newWatchRequest(ctx context.Context, params *WatchParams) (*http.Request, error) {
	u := url.URL{
		Path:     constructAPIPath(params),
		RawQuery: constructAPIQuery(params),
	}

	r, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		u.String(),
		http.NoBody,
	)
	if err != nil {
		return nil, err
	}
	r.Header[httpz.AcceptHeader] = []string{acceptedContentType}
	r.Header[httpz.ConnectionHeader] = []string{"close"}

	return r, nil
}

func constructAPIPath(params *WatchParams) string {
	var p []string
	if params.Group == "" {
		p = []string{"/api"}
	} else {
		p = []string{"/apis", url.PathEscape(params.Group)}
	}

	p = append(p, url.PathEscape(params.Version))

	if params.Namespace != "" {
		p = append(p, "namespaces", url.PathEscape(params.Namespace))
	}

	p = append(p, url.PathEscape(params.Resource))
	return path.Join(p...)
}

func constructAPIQuery(params *WatchParams) string {
	query := url.Values{"watch": []string{"true"}}
	if params.ResourceVersion != nil {
		query.Add("resourceVersion", *params.ResourceVersion)
	}
	if params.AllowWatchBookmarks != nil {
		query.Add("allowWatchBookmarks", strconv.FormatBool(*params.AllowWatchBookmarks))
	}
	if params.SendInitialEvents != nil {
		query.Add("sendInitialEvents", strconv.FormatBool(*params.SendInitialEvents))
	}
	if params.FieldSelector != nil {
		query.Add("fieldSelector", *params.FieldSelector)
	}
	if params.LabelSelector != nil {
		query.Add("labelSelector", *params.LabelSelector)
	}
	return query.Encode()
}
