package jenkins

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"unicode"
	"unicode/utf8"

	"github.com/tidbcloud/AgentTeam/internal/extension/egress"
	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
)

const (
	maxBuildParameters     = 100
	maxParameterNameBytes  = 128
	maxParameterValueBytes = 16 << 10
	maxBuildFormBytes      = 256 << 10
	maxConsoleLogBytes     = 48 << 10
	maxFinalOutputHeadroom = 128
	maxJenkinsOutputBytes  = egress.MaxFinalResponseBytes - maxFinalOutputHeadroom
)

const (
	listJobsTree  = "jobs[name,fullName,_class,color,buildable,inQueue,lastBuild[number,result,building]]"
	getJobTree    = "name,fullName,displayName,description,buildable,inQueue,color,nextBuildNumber,lastBuild[number,result,building,timestamp,duration],queueItem[id,why,blocked,buildable,stuck],actions[parameterDefinitions[name,type,description,defaultParameterValue[value]]]"
	queueItemTree = "id,cancelled,blocked,buildable,stuck,why,inQueueSince,executable[number],task[name,fullName]"
	buildTree     = "number,displayName,fullDisplayName,building,result,timestamp,duration,estimatedDuration,description,actions[causes[shortDescription,userId,userName,upstreamBuild,upstreamProject],parameters[name,value]]"
)

type jobInput struct {
	JobPath []string `json:"jobPath"`
}

type listJobsInput struct {
	JobPath json.RawMessage `json:"jobPath,omitempty"`
}

type triggerBuildInput struct {
	JobPath    []string        `json:"jobPath"`
	Parameters json.RawMessage `json:"parameters,omitempty"`
}

type queueItemInput struct {
	QueueID int64 `json:"queueId"`
}

type numberedBuildInput struct {
	JobPath     []string `json:"jobPath"`
	BuildNumber int64    `json:"buildNumber"`
}

type consoleLogInput struct {
	JobPath     []string        `json:"jobPath"`
	BuildNumber int64           `json:"buildNumber"`
	Start       json.RawMessage `json:"start,omitempty"`
}

func handlers() map[string]actionHandler {
	return map[string]actionHandler{
		ActionListJobs:        handleListJobs,
		ActionGetJob:          handleGetJob,
		ActionTriggerBuild:    handleTriggerBuild,
		ActionGetQueueItem:    handleGetQueueItem,
		ActionGetBuild:        handleGetBuild,
		ActionGetConsoleLog:   handleGetConsoleLog,
		ActionCancelQueueItem: handleCancelQueueItem,
		ActionStopBuild:       handleStopBuild,
	}
}

func handleListJobs(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	raw json.RawMessage,
) (json.RawMessage, error) {
	var input listJobsInput
	if err := decodeActionInput(raw, &input); err != nil {
		return nil, err
	}
	segments, err := optionalJobPath(input.JobPath)
	if err != nil {
		return nil, err
	}
	path, err := jobPath(segments, "/api/json", true)
	if err != nil {
		return nil, validationError(err)
	}
	return readJSONObject(ctx, target, credentials, path, url.Values{
		"tree": []string{listJobsTree},
	})
}

func handleGetJob(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	raw json.RawMessage,
) (json.RawMessage, error) {
	var input jobInput
	if err := decodeActionInput(raw, &input); err != nil {
		return nil, err
	}
	path, err := jobPath(input.JobPath, "/api/json", false)
	if err != nil {
		return nil, validationError(err)
	}
	return readJSONObject(ctx, target, credentials, path, url.Values{
		"tree": []string{getJobTree},
	})
}

func handleTriggerBuild(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	raw json.RawMessage,
) (json.RawMessage, error) {
	var input triggerBuildInput
	if err := decodeActionInput(raw, &input); err != nil {
		return nil, err
	}
	parameters, err := optionalBuildParameters(input.Parameters)
	if err != nil {
		return nil, err
	}
	form, err := buildParametersForm(parameters)
	if err != nil {
		return nil, err
	}
	suffix := "/build"
	if len(form) != 0 {
		suffix = "/buildWithParameters"
	}
	path, err := jobPath(input.JobPath, suffix, false)
	if err != nil {
		return nil, validationError(err)
	}
	request := runner.RESTRequest{
		Method: http.MethodPost,
		Path:   path,
		Response: runner.RESTResponseSpec{
			Mode:                        runner.RESTResponseEmpty,
			SuccessStatuses:             []int{http.StatusCreated},
			ExposedHeaders:              []string{"Location"},
			ForbiddenAsPermissionDenied: true,
		},
	}
	if len(form) != 0 {
		request.Form = form
	}
	response, err := target.rest.CallResponse(ctx, credentials, request)
	if err != nil {
		return nil, err
	}
	locations := response.Headers.Values("Location")
	if len(locations) != 1 {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins returned an invalid queue location",
			nil,
		)
	}
	queueID, err := parseQueueLocation(target.baseURL, locations[0])
	if err != nil {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins returned an invalid queue location",
			err,
		)
	}
	return marshalOutput(map[string]any{"queueId": queueID})
}

func handleGetQueueItem(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	raw json.RawMessage,
) (json.RawMessage, error) {
	var input queueItemInput
	if err := decodeActionInput(raw, &input); err != nil {
		return nil, err
	}
	if err := validatePositiveSafeInteger(input.QueueID); err != nil {
		return nil, validationError(err)
	}
	path := "/queue/item/" + strconv.FormatInt(input.QueueID, 10) + "/api/json"
	return readJSONObject(ctx, target, credentials, path, url.Values{
		"tree": []string{queueItemTree},
	})
}

func handleGetBuild(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	raw json.RawMessage,
) (json.RawMessage, error) {
	path, err := decodeNumberedBuild(raw, "/api/json")
	if err != nil {
		return nil, err
	}
	return readJSONObject(ctx, target, credentials, path, url.Values{
		"tree": []string{buildTree},
	})
}

func handleGetConsoleLog(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	raw json.RawMessage,
) (json.RawMessage, error) {
	var input consoleLogInput
	if err := decodeActionInput(raw, &input); err != nil {
		return nil, err
	}
	if err := validatePositiveSafeInteger(input.BuildNumber); err != nil {
		return nil, validationError(err)
	}
	start := int64(0)
	if len(input.Start) != 0 {
		if bytes.Equal(bytes.TrimSpace(input.Start), []byte("null")) ||
			json.Unmarshal(input.Start, &start) != nil {
			return nil, validationError(fmt.Errorf("Jenkins log start is invalid"))
		}
	}
	if start < 0 || start > maxSafeInteger {
		return nil, validationError(fmt.Errorf("Jenkins log start is invalid"))
	}
	suffix := "/" + strconv.FormatInt(input.BuildNumber, 10) +
		"/logText/progressiveText"
	path, err := jobPath(input.JobPath, suffix, false)
	if err != nil {
		return nil, validationError(err)
	}
	response, err := target.rest.CallResponse(ctx, credentials, runner.RESTRequest{
		Method: http.MethodGet,
		Path:   path,
		Query: url.Values{
			"start": []string{strconv.FormatInt(start, 10)},
		},
		Response: runner.RESTResponseSpec{
			Mode:                        runner.RESTResponseText,
			MaxBytes:                    maxConsoleLogBytes,
			ExposedHeaders:              []string{"X-Text-Size", "X-More-Data"},
			ForbiddenAsPermissionDenied: true,
		},
	})
	if err != nil {
		return nil, err
	}
	if !utf8.Valid(response.Body) {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins console log was not UTF-8",
			nil,
		)
	}
	nextStart, moreData, cursorReset, err := progressiveLogCursor(
		start,
		response.Headers.Values("X-Text-Size"),
		response.Headers.Values("X-More-Data"),
	)
	if err != nil {
		return nil, err
	}
	return marshalOutput(map[string]any{
		"text":        string(response.Body),
		"nextStart":   nextStart,
		"moreData":    moreData,
		"cursorReset": cursorReset,
	})
}

func handleCancelQueueItem(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	raw json.RawMessage,
) (json.RawMessage, error) {
	var input queueItemInput
	if err := decodeActionInput(raw, &input); err != nil {
		return nil, err
	}
	if err := validatePositiveSafeInteger(input.QueueID); err != nil {
		return nil, validationError(err)
	}
	form := url.Values{"id": []string{strconv.FormatInt(input.QueueID, 10)}}
	if err := callAcknowledgement(
		ctx, target, credentials, "/queue/cancelItem", form); err != nil {
		return nil, err
	}
	return marshalOutput(map[string]any{"cancelled": true})
}

func handleStopBuild(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	raw json.RawMessage,
) (json.RawMessage, error) {
	path, err := decodeNumberedBuild(raw, "/stop")
	if err != nil {
		return nil, err
	}
	if err := callAcknowledgement(
		ctx, target, credentials, path, nil); err != nil {
		return nil, err
	}
	return marshalOutput(map[string]any{"stopped": true})
}

func readJSONObject(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	path string,
	query url.Values,
) (json.RawMessage, error) {
	body, _, err := callJSONObject(
		ctx,
		target,
		credentials,
		runner.RESTRequest{
			Method: http.MethodGet,
			Path:   path,
			Query:  query,
			Response: runner.RESTResponseSpec{
				Mode:                        runner.RESTResponseJSON,
				ForbiddenAsPermissionDenied: true,
			},
		},
	)
	if err != nil {
		return nil, err
	}
	return json.RawMessage(body), nil
}

func callAcknowledgement(
	ctx context.Context,
	target *targetClient,
	credentials runner.Credentials,
	path string,
	form url.Values,
) error {
	_, err := target.rest.CallResponse(ctx, credentials, runner.RESTRequest{
		Method: http.MethodPost,
		Path:   path,
		Form:   form,
		Response: runner.RESTResponseSpec{
			Mode: runner.RESTResponseEmpty,
			SuccessStatuses: []int{
				http.StatusOK,
				http.StatusCreated,
				http.StatusAccepted,
				http.StatusFound,
				http.StatusSeeOther,
			},
			ForbiddenAsPermissionDenied: true,
		},
	})
	return err
}

func decodeNumberedBuild(
	raw json.RawMessage,
	actionSuffix string,
) (string, error) {
	var input numberedBuildInput
	if err := decodeActionInput(raw, &input); err != nil {
		return "", err
	}
	if err := validatePositiveSafeInteger(input.BuildNumber); err != nil {
		return "", validationError(err)
	}
	suffix := "/" + strconv.FormatInt(input.BuildNumber, 10) + actionSuffix
	path, err := jobPath(input.JobPath, suffix, false)
	if err != nil {
		return "", validationError(err)
	}
	return path, nil
}

func decodeActionInput(raw json.RawMessage, target any) error {
	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) < 2 || trimmed[0] != '{' || trimmed[len(trimmed)-1] != '}' {
		return validationError(fmt.Errorf("Jenkins action input must be an object"))
	}
	decoder := json.NewDecoder(bytes.NewReader(trimmed))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return validationError(err)
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return validationError(fmt.Errorf("Jenkins action input has trailing data"))
	}
	return nil
}

func buildParametersForm(
	parameters map[string]json.RawMessage,
) (url.Values, error) {
	if len(parameters) == 0 {
		return nil, nil
	}
	if len(parameters) > maxBuildParameters {
		return nil, validationError(
			fmt.Errorf("Jenkins build parameters exceed the count limit"))
	}
	names := make([]string, 0, len(parameters))
	for name := range parameters {
		if err := validateParameterName(name); err != nil {
			return nil, validationError(err)
		}
		names = append(names, name)
	}
	sort.Strings(names)
	form := make(url.Values, len(names))
	for _, name := range names {
		value, err := buildParameterValue(parameters[name])
		if err != nil {
			return nil, validationError(err)
		}
		form.Set(name, value)
	}
	if len(form.Encode()) > maxBuildFormBytes {
		return nil, validationError(
			fmt.Errorf("Jenkins build parameters exceed the encoded size budget"))
	}
	return form, nil
}

func optionalJobPath(raw json.RawMessage) ([]string, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
		return nil, validationError(fmt.Errorf("Jenkins jobPath must be an array"))
	}
	var segments []string
	if err := json.Unmarshal(raw, &segments); err != nil {
		return nil, validationError(fmt.Errorf("Jenkins jobPath must be an array"))
	}
	return segments, nil
}

func optionalBuildParameters(
	raw json.RawMessage,
) (map[string]json.RawMessage, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	if bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
		return nil, validationError(
			fmt.Errorf("Jenkins build parameters must be an object"))
	}
	var parameters map[string]json.RawMessage
	if err := json.Unmarshal(raw, &parameters); err != nil {
		return nil, validationError(
			fmt.Errorf("Jenkins build parameters must be an object"))
	}
	return parameters, nil
}

func validateParameterName(name string) error {
	if !utf8.ValidString(name) || len(name) < 1 ||
		len(name) > maxParameterNameBytes {
		return fmt.Errorf("Jenkins build parameter name is invalid")
	}
	for _, value := range name {
		if unicode.IsControl(value) || value == '\x7f' {
			return fmt.Errorf("Jenkins build parameter name is invalid")
		}
	}
	return nil
}

func buildParameterValue(raw json.RawMessage) (string, error) {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return "", fmt.Errorf("Jenkins build parameter value is invalid")
	}
	switch typed := value.(type) {
	case string:
		if !utf8.ValidString(typed) || len(typed) > maxParameterValueBytes {
			return "", fmt.Errorf("Jenkins build parameter string is too long")
		}
		for _, value := range typed {
			if unicode.IsControl(value) || value == '\x7f' {
				return "", fmt.Errorf(
					"Jenkins build parameter string is invalid")
			}
		}
		return typed, nil
	case json.Number:
		return typed.String(), nil
	case bool:
		return strconv.FormatBool(typed), nil
	default:
		return "", fmt.Errorf("Jenkins build parameter must be scalar")
	}
}

func progressiveLogCursor(
	start int64,
	textSizeValues []string,
	moreDataValues []string,
) (int64, bool, bool, error) {
	if len(textSizeValues) != 1 {
		return 0, false, false, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins console cursor was malformed",
			nil,
		)
	}
	rawNextStart := textSizeValues[0]
	for index := range rawNextStart {
		if rawNextStart[index] < '0' || rawNextStart[index] > '9' {
			return 0, false, false, runner.Errorf(
				runner.ErrorUpstreamFailure,
				"Jenkins console cursor was malformed",
				nil,
			)
		}
	}
	nextStart, err := strconv.ParseInt(rawNextStart, 10, 64)
	if err != nil || rawNextStart == "" || nextStart > maxSafeInteger {
		return 0, false, false, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins console cursor was malformed",
			nil,
		)
	}
	moreData := false
	if len(moreDataValues) > 1 {
		return 0, false, false, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins console continuation flag was malformed",
			nil,
		)
	}
	if len(moreDataValues) == 1 {
		switch strings.ToLower(moreDataValues[0]) {
		case "true":
			moreData = true
		case "false", "":
		default:
			return 0, false, false, runner.Errorf(
				runner.ErrorUpstreamFailure,
				"Jenkins console continuation flag was malformed",
				nil,
			)
		}
	}
	return nextStart, moreData, nextStart < start, nil
}

func validatePositiveSafeInteger(value int64) error {
	if value < 1 || value > maxSafeInteger {
		return fmt.Errorf("Jenkins identifier is invalid")
	}
	return nil
}

func validationError(cause error) error {
	return runner.Errorf(
		runner.ErrorValidation,
		"Jenkins action input is invalid",
		cause,
	)
}

func marshalOutput(value any) (json.RawMessage, error) {
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins result was not encodable",
			err,
		)
	}
	if len(encoded) > maxJenkinsOutputBytes {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Jenkins result exceeded the response budget",
			nil,
		)
	}
	return json.RawMessage(encoded), nil
}
