package gerrit

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/url"
	"time"
)

// PatchSet contains detailed information about a specific patch set.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/json.html#patchSet
type PatchSet struct {
	Number    Number      `json:"number"`
	Revision  string      `json:"revision"`
	Parents   []string    `json:"parents"`
	Ref       string      `json:"ref"`
	Uploader  AccountInfo `json:"uploader"`
	Author    AccountInfo `json:"author"`
	CreatedOn int         `json:"createdOn"`
	IsDraft   bool        `json:"isDraft"`
	Kind      string      `json:"kind"`
}

// RefUpdate contains data about a reference update.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/json.html#refUpdate
type RefUpdate struct {
	OldRev  string `json:"oldRev"`
	NewRev  string `json:"newRev"`
	RefName string `json:"refName"`
	Project string `json:"project"`
}

// EventInfo contains information about an event emitted by Gerrit.  This
// structure can be used either when parsing streamed events or when reading
// the output of the events-log plugin.
//
// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/cmd-stream-events.html#events
type EventInfo struct {
	Type           string        `json:"type"`
	Change         ChangeInfo    `json:"change,omitempty"`
	ChangeKey      ChangeInfo    `json:"changeKey,omitempty"`
	PatchSet       PatchSet      `json:"patchSet,omitempty"`
	EventCreatedOn int           `json:"eventCreatedOn,omitempty"`
	Reason         string        `json:"reason,omitempty"`
	Abandoner      AccountInfo   `json:"abandoner,omitempty"`
	Restorer       AccountInfo   `json:"restorer,omitempty"`
	Submitter      AccountInfo   `json:"submitter,omitempty"`
	Author         AccountInfo   `json:"author,omitempty"`
	Uploader       AccountInfo   `json:"uploader,omitempty"`
	Approvals      []AccountInfo `json:"approvals,omitempty"`
	Comment        string        `json:"comment,omitempty"`
	Editor         AccountInfo   `json:"editor,omitempty"`
	Added          []string      `json:"added,omitempty"`
	Removed        []string      `json:"removed,omitempty"`
	Hashtags       []string      `json:"hashtags,omitempty"`
	RefUpdate      RefUpdate     `json:"refUpdate,omitempty"`
	Project        ProjectInfo   `json:"project,omitempty"`
	Reviewer       AccountInfo   `json:"reviewer,omitempty"`
	OldTopic       string        `json:"oldTopic,omitempty"`
	Changer        AccountInfo   `json:"changer,omitempty"`
}

// EventsLogService contains functions for querying the API provided
// by the optional events-log plugin.
type EventsLogService struct {
	client *Client
}

// EventsLogOptions contains options for querying events from the events-logs
// plugin.
type EventsLogOptions struct {
	From time.Time
	To   time.Time

	// IgnoreUnmarshalErrors will cause GetEvents to ignore any errors
	// that come up when calling json.Unmarshal. This can be useful in
	// cases where the events-log plugin was not kept up to date with
	// the Gerrit version for some reason. In these cases the events-log
	// plugin will return data structs that don't match the EventInfo
	// struct which in turn causes issues for json.Unmarshal.
	IgnoreUnmarshalErrors bool
}

// getURL returns the url that should be used in the request.  This will vary
// depending on the options provided to GetEvents.
func (events *EventsLogService) getURL(options *EventsLogOptions) (string, error) {
	parsed, err := url.Parse("/plugins/events-log/events/")
	if err != nil {
		return "", err
	}

	query := parsed.Query()

	if !options.From.IsZero() {
		query.Set("t1", options.From.Format("2006-01-02 15:04:05"))
	}

	if !options.To.IsZero() {
		query.Set("t2", options.To.Format("2006-01-02 15:04:05"))
	}

	encoded := query.Encode()
	if len(encoded) > 0 {
		parsed.RawQuery = encoded
	}

	return parsed.String(), nil
}

// GetEvents returns a list of events for the given input options.  Use of this
// function requires an authenticated user and for the events-log plugin to be
// installed. This function returns the unmarshalled EventInfo structs, response,
// failed lines and errors. Marshaling errors will cause this function to return
// before processing is complete unless you set EventsLogOptions.IgnoreUnmarshalErrors
// to true. This can be useful in cases where the events-log plugin got out of sync
// with the Gerrit version which in turn produced events which can't be transformed
// unmarshalled into EventInfo.
//
// Gerrit API docs: https://<yourserver>/plugins/events-log/Documentation/rest-api-events.html
func (events *EventsLogService) GetEvents(ctx context.Context, options *EventsLogOptions) ([]EventInfo, *Response, [][]byte, error) {
	info := []EventInfo{}
	failures := [][]byte{}
	requestURL, err := events.getURL(options)

	if err != nil {
		return info, nil, failures, err
	}

	request, err := events.client.NewRequest(ctx, "GET", requestURL, nil)
	if err != nil {
		return info, nil, failures, err
	}

	// Perform the request but do not pass in a structure to unpack
	// the response into.  The format of the response is one EventInfo
	// object per line so we need to manually handle the response here.
	response, err := events.client.Do(request, nil)
	if err != nil {
		return info, response, failures, err
	}

	body, err := io.ReadAll(response.Body)
	defer response.Body.Close() // nolint: errcheck
	if err != nil {
		return info, response, failures, err
	}

	for _, line := range bytes.Split(body, []byte("\n")) {
		if len(line) > 0 {
			event := EventInfo{}
			if err := json.Unmarshal(line, &event); err != nil { // nolint: vetshadow
				failures = append(failures, line)

				if !options.IgnoreUnmarshalErrors {
					return info, response, failures, err
				}
				continue
			}
			info = append(info, event)
		}
	}
	return info, response, failures, err
}
