package flow

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"time"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/httpz"
	starjson "go.starlark.net/lib/json"
	startime "go.starlark.net/lib/time"
	"go.starlark.net/starlark"
	"go.starlark.net/starlarkstruct"
)

const (
	selectBuiltin        = "select"
	signalBuiltin        = "signal"
	signalChannelBuiltin = "signal_channel"
	sleepBuiltin         = "sleep"
	timerBuiltin         = "timer"
	gitlabURLVarName     = "gitlab_url"
	gitlabTokenVarName   = "gitlab_token"
)

// EventHandlerContext defines an interface for DSL functionalities
// TODO: eventually abstract starlark types away ... maybe?!
type EventHandlerContext interface {
	HTTPDo(reqURL string, method string, query url.Values, header http.Header, body []byte) (*HTTPResponse, error)
	Select(cases *starlark.Dict, defaultCase starlark.Value, timeout time.Duration, timeoutValue starlark.Value) (starlark.Value, error)
	Signal(workflowID, signalName, payloadJSON, runID string) (bool /* signaled */, error)
	SignalChannel(signalName string) (ReceiveChannel, error)
	Sleep(duration time.Duration) error
	Timer(duration time.Duration) Future
}

type HTTPResponse struct {
	Status     string
	StatusCode uint32
	Header     http.Header
	Body       []byte
}

type eventHandlerContextModule struct {
	ctx  EventHandlerContext
	vars starlark.StringDict
}

func (m *eventHandlerContextModule) toStarlarkModule() *starlarkstruct.Module {
	w := &starlarkstruct.Module{
		Name: "eventHandlerContext",
		Members: starlark.StringDict{
			"vars": &starlarkstruct.Module{
				Name:    "vars",
				Members: m.vars,
			},
			"http": &starlarkstruct.Module{
				Name: "http",
				Members: starlark.StringDict{
					"do": starlark.NewBuiltin("http_do", m.httpDo),
				},
			},
			"gitlab_client": &starlarkstruct.Module{
				Name: "gitlab_client",
				Members: starlark.StringDict{
					"do": starlark.NewBuiltin("gitlab_client_http_do", m.gitlabClientHTTPDo),
				},
			},
			selectBuiltin:        starlark.NewBuiltin(selectBuiltin, m.selectFunc),
			signalBuiltin:        starlark.NewBuiltin(signalBuiltin, m.signal),
			signalChannelBuiltin: starlark.NewBuiltin(signalChannelBuiltin, m.signalChannel),
			sleepBuiltin:         starlark.NewBuiltin(sleepBuiltin, m.sleep),
			timerBuiltin:         starlark.NewBuiltin(timerBuiltin, m.timer),
		},
	}
	w.Freeze()
	return w
}

func (m *eventHandlerContextModule) httpDo(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	if len(args) > 0 {
		return nil, fmt.Errorf("%s: unexpected positional arguments", fn.Name())
	}
	var (
		reqURL     string
		method     = http.MethodGet
		queryDict  *starlark.Dict
		headerDict *starlark.Dict
		body       starlark.Bytes
	)
	err := starlark.UnpackArgs(fn.Name(), nil, kwargs,
		"url", &reqURL,
		"method??", &method,
		"query??", &queryDict,
		"header??", &headerDict,
		"body??", &body,
	)
	if err != nil {
		return nil, err
	}

	query, err := dictToURLValues(queryDict)
	if err != nil {
		return nil, err
	}

	header, err := dictToHTTPHeader(headerDict)
	if err != nil {
		return nil, err
	}

	response, err := m.ctx.HTTPDo(reqURL, method, query, header, []byte(body))
	if err != nil {
		return nil, err
	}

	responseHeaders, err := httpHeaderToDict(response.Header)
	if err != nil {
		return nil, err
	}

	return &starlarkstruct.Module{
		Name: "http_response",
		Members: starlark.StringDict{
			"status_code": starlark.MakeUint64(uint64(response.StatusCode)),
			"status":      starlark.String(response.Status),
			"header":      responseHeaders,
			"body":        starlark.Bytes(response.Body),
		},
	}, err
}

// gitlabClientHTTPDo performs HTTP request with a prepared GitLab URL and token.
// NOTE: the lack of abstraction between httpDo and gitlabClientHTTPDo is intentional at this point.
// We don't really know how it will evolve, e.g. with REST vs. GraphQL or what other "goodies" we
// can provide from AutoFlow side, like status code handling, content type setting and verification etc.
// We should also pass the URL and token directly without doing the starlark.Value roundtrip.
func (m *eventHandlerContextModule) gitlabClientHTTPDo(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	if len(args) > 0 {
		return nil, fmt.Errorf("%s: unexpected positional arguments", fn.Name())
	}
	if !m.vars.Has(gitlabURLVarName) {
		return nil, fmt.Errorf("%s: unable to use because %q variable is not configured", fn.Name(), gitlabURLVarName)
	}
	if !m.vars.Has(gitlabTokenVarName) {
		return nil, fmt.Errorf("%s: unable to use because %q variable is not configured", fn.Name(), gitlabTokenVarName)
	}
	var (
		reqPath    string
		method     = http.MethodGet
		queryDict  *starlark.Dict
		headerDict *starlark.Dict
		body       starlark.Bytes
		json       starlark.Value
	)
	err := starlark.UnpackArgs(fn.Name(), nil, kwargs,
		"path", &reqPath,
		"method??", &method,
		"query??", &queryDict,
		"header??", &headerDict,
		"body??", &body,
		"json??", &json,
	)
	if err != nil {
		return nil, err
	}

	if body.Truth() && json != nil {
		return nil, fmt.Errorf("%s: cannot specify body and json in same request", fn.Name())
	}

	query, err := dictToURLValues(queryDict)
	if err != nil {
		return nil, err
	}

	header, err := dictToHTTPHeader(headerDict)
	if err != nil {
		return nil, err
	}

	if json != nil {
		payloadJSON, err := starlark.Call(thread, starjson.Module.Members["encode"], starlark.Tuple{json}, nil) //nolint:govet
		if err != nil {
			return nil, err
		}

		payloadJSONStr, ok := payloadJSON.(starlark.String)
		if !ok {
			return nil, errors.New("encoded payload is not a starlark string")
		}
		body = starlark.Bytes(payloadJSONStr)
		header[httpz.ContentTypeHeader] = []string{"application/json"}
	}

	gitlabURL, ok := m.vars[gitlabURLVarName].(starlark.String)
	if !ok {
		return nil, fmt.Errorf("%s: failed to use GitLab URL variable as string", fn.Name())
	}

	gitlabToken, ok := m.vars[gitlabTokenVarName].(starlark.String)
	if !ok {
		return nil, fmt.Errorf("%s: failed to use GitLab token variable as string", fn.Name())
	}

	u, err := url.JoinPath(gitlabURL.GoString(), reqPath)
	if err != nil {
		return nil, err
	}

	header[httpz.AuthorizationHeader] = []string{fmt.Sprintf("Bearer %s", gitlabToken.GoString())}

	response, err := m.ctx.HTTPDo(u, method, query, header, []byte(body))
	if err != nil {
		return nil, err
	}

	responseHeaders, err := httpHeaderToDict(response.Header)
	if err != nil {
		return nil, err
	}

	return &starlarkstruct.Module{
		Name: "http_response",
		Members: starlark.StringDict{
			"status_code": starlark.MakeUint64(uint64(response.StatusCode)),
			"status":      starlark.String(response.Status),
			"header":      responseHeaders,
			"body":        starlark.Bytes(response.Body),
			"json": starlark.NewBuiltin("json", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
				if ct := response.Header.Get(httpz.ContentTypeHeader); !httpz.IsContentType(ct, "application/json") {
					return nil, fmt.Errorf("response %s must be application/json to decode body as JSON but got %q", httpz.ContentTypeHeader, ct)
				}
				return starlark.Call(thread, starjson.Module.Members["decode"], starlark.Tuple{starlark.String(response.Body)}, nil)
			}),
		},
	}, err
}

func (m *eventHandlerContextModule) selectFunc(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var (
		cases        *starlark.Dict
		defaultCase  starlark.Value = starlark.None
		timeout      startime.Duration
		timeoutValue starlark.Value = starlark.False
	)
	err := starlark.UnpackPositionalArgs(fn.Name(), args, nil, 1, &cases)
	if err != nil {
		return nil, err
	}
	err = starlark.UnpackArgs(fn.Name(), nil, kwargs,
		"default??", &defaultCase,
		"timeout??", &timeout,
		"timeout_value??", &timeoutValue,
	)
	if err != nil {
		return nil, err
	}

	return m.ctx.Select(cases, defaultCase, time.Duration(timeout), timeoutValue)
}

func (m *eventHandlerContextModule) signal(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	if len(args) > 0 {
		return nil, fmt.Errorf("%s: unexpected positional arguments", fn.Name())
	}

	var (
		workflowID, runID, signalName string
		payload                       starlark.Value = starlark.None
	)

	err := starlark.UnpackArgs(fn.Name(), nil, kwargs,
		"workflow_id", &workflowID,
		"signal_name", &signalName,
		"payload?", &payload,
		"run_id??", &runID,
	)
	if err != nil {
		return nil, err
	}

	payloadJSON, err := starlark.Call(thread, starjson.Module.Members["encode"], starlark.Tuple{payload}, nil)
	if err != nil {
		return nil, err
	}

	payloadJSONStr, ok := payloadJSON.(starlark.String)
	if !ok {
		return nil, errors.New("encoded payload is not a starlark string")
	}

	signaled, err := m.ctx.Signal(workflowID, signalName, string(payloadJSONStr), runID)
	if err != nil {
		return nil, err
	}
	return starlark.Bool(signaled), nil
}

func (m *eventHandlerContextModule) signalChannel(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var signalName starlark.String
	err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 1, &signalName)
	if err != nil {
		return nil, err
	}

	sc, err := m.ctx.SignalChannel(string(signalName))
	if err != nil {
		return nil, err
	}
	return newReceiveChannel(sc), nil
}

func (m *eventHandlerContextModule) sleep(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var d startime.Duration
	err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 1, &d)
	if err != nil {
		return nil, err
	}

	if err := m.ctx.Sleep(time.Duration(d)); err != nil {
		return nil, err
	}
	return starlark.None, nil
}

func (m *eventHandlerContextModule) timer(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
	var d startime.Duration
	err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 1, &d)
	if err != nil {
		return nil, err
	}

	return &FutureType{f: m.ctx.Timer(time.Duration(d))}, nil
}
