// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package helpers

import (
	"fmt"
	"strings"

	"google.golang.org/protobuf/compiler/protogen"

	"github.com/cilium/tetragon/tools/protoc-gen-go-tetragon/common"
)

func generateResponseTypeString(g *protogen.GeneratedFile, files []*protogen.File) error {
	oneofs, err := common.GetEventsResponseOneofs(files)
	if err != nil {
		return err
	}

	doCases := func() string {
		var ret string
		for _, oneof := range oneofs {
			msgGoIdent := common.TetragonApiIdent(g, "GetEventsResponse_"+oneof.TypeName)
			typeGoIdent := common.TetragonApiIdent(g, "EventType_"+strings.ToUpper(oneof.FieldName))

			ret += `case *` + msgGoIdent + `:
                return ` + typeGoIdent + `.String(), nil
            `
		}
		return ret
	}

	tetragonGER := common.TetragonApiIdent(g, "GetEventsResponse")

	g.P(`// ResponseTypeString returns an event's type as a string
    func ResponseTypeString(response *` + tetragonGER + `) (string, error) {
        if response == nil {
            return "", ` + common.FmtErrorf(g, "Response is nil") + `
        }

        event := response.Event
        if event == nil {
            return "", ` + common.FmtErrorf(g, "Event is nil") + `
        }

        switch event.(type) {
            ` + doCases() + `
        }
        return "", ` + common.FmtErrorf(g, "Unhandled response type %T", "event") + `
        }`)

	return nil
}

func generateResponseGetProcess(g *protogen.GeneratedFile) error {
	tetragonProcess := common.ProcessIdent(g)
	tetragonGER := common.TetragonApiIdent(g, "GetEventsResponse")

	g.P(`// ResponseGetProcess returns a GetEventsResponse's process if it exists
    func ResponseGetProcess(response *` + tetragonGER + `) *` + tetragonProcess + ` {
        if response == nil {
            return nil
        }

        event := response.Event
        if event == nil {
            return nil
        }

        return ResponseInnerGetProcess(event)
	 }`)

	return nil
}

func generateResponseInnerGetProcess(g *protogen.GeneratedFile, files []*protogen.File) error {
	events, err := common.GetEvents(files)
	if err != nil {
		return err
	}

	tetragonProcess := common.ProcessIdent(g)

	doCases := func() string {
		var ret string
		for _, msg := range events {
			if !common.IsProcessEvent(msg) {
				continue
			}

			goIdent := common.TetragonApiIdent(g, "GetEventsResponse_"+msg.GoIdent.GoName)

			ret += `case *` + goIdent + `:
                return ev.` + msg.GoIdent.GoName + `.Process
            `
		}
		return ret
	}

	ifaceIdent := common.TetragonApiIdent(g, "IsGetEventsResponse_Event")

	g.P(`// ResponseInnerGetProcess returns a GetEventsResponse inner event's process if it exists
    func ResponseInnerGetProcess(event ` + ifaceIdent + `) *` + tetragonProcess + ` {
        switch ev := event.(type) {
            ` + doCases() + `
        }
        return nil
	 }`)

	return nil
}

func generateResponseGetProcessKprobe(g *protogen.GeneratedFile) error {
	processKprobe := common.ProcessKprobeIdent(g)
	tetragonGER := common.TetragonApiIdent(g, "GetEventsResponse")

	g.P(`// ResponseGetProcessKprobe returns a GetEventsResponse's process if it exists
    func ResponseGetProcessKprobe(response *` + tetragonGER + `) *` + processKprobe + ` {
        if response == nil {
            return nil
        }

		return response.GetProcessKprobe()
	 }`)

	return nil
}

func generateResponseGetParent(g *protogen.GeneratedFile) error {
	tetragonProcess := common.ProcessIdent(g)
	tetragonGER := common.TetragonApiIdent(g, "GetEventsResponse")

	g.P(`// ResponseGetParent returns a GetEventsResponse's parent process if it exists
    func ResponseGetParent(response *` + tetragonGER + `) *` + tetragonProcess + ` {
        if response == nil {
            return nil
        }

        event := response.Event
        if event == nil {
            return nil
        }

        return ResponseInnerGetParent(event)
	 }`)

	return nil
}

func generateResponseInnerGetParent(g *protogen.GeneratedFile, files []*protogen.File) error {
	events, err := common.GetEvents(files)
	if err != nil {
		return err
	}

	tetragonProcess := common.ProcessIdent(g)

	doCases := func() string {
		var ret string
		for _, msg := range events {
			if !common.IsParentEvent(msg) {
				continue
			}

			goIdent := common.TetragonApiIdent(g, "GetEventsResponse_"+msg.GoIdent.GoName)

			ret += `case *` + goIdent + `:
                return ev.` + msg.GoIdent.GoName + `.Parent
            `
		}
		return ret
	}

	ifaceIdent := common.TetragonApiIdent(g, "IsGetEventsResponse_Event")

	g.P(`// ResponseInnerGetParent returns a GetEventsResponse inner event's parent process if it exists
    func ResponseInnerGetParent(event ` + ifaceIdent + `) *` + tetragonProcess + ` {
        switch ev := event.(type) {
            ` + doCases() + `
        }
        return nil
	 }`)

	return nil
}

func generateResponseGetAncestors(g *protogen.GeneratedFile) error {
	tetragonProcess := common.ProcessIdent(g)
	tetragonGER := common.TetragonApiIdent(g, "GetEventsResponse")

	g.P(`// ResponseGetAncestors returns a GetEventsResponse's ancestors processes if they exists
    func ResponseGetAncestors(response *` + tetragonGER + `) []*` + tetragonProcess + ` {
        if response == nil {
            return nil
        }

        event := response.Event
        if event == nil {
            return nil
        }

        return ResponseInnerGetAncestors(event)
	 }`)

	return nil
}

func generateResponseInnerGetAncestors(g *protogen.GeneratedFile, files []*protogen.File) error {
	events, err := common.GetEvents(files)
	if err != nil {
		return err
	}

	tetragonProcess := common.ProcessIdent(g)

	doCases := func() string {
		var ret string
		for _, msg := range events {
			if !common.IsAncestorsEvent(msg) {
				continue
			}

			goIdent := common.TetragonApiIdent(g, "GetEventsResponse_"+msg.GoIdent.GoName)

			ret += `case *` + goIdent + `:
                return ev.` + msg.GoIdent.GoName + `.Ancestors
            `
		}
		return ret
	}

	ifaceIdent := common.TetragonApiIdent(g, "IsGetEventsResponse_Event")

	g.P(`// ResponseInnerGetAncestors returns a GetEventsResponse inner event's ancestors processes if they exists
    func ResponseInnerGetAncestors(event ` + ifaceIdent + `) []*` + tetragonProcess + ` {
        switch ev := event.(type) {
            ` + doCases() + `
        }
        return nil
	 }`)

	return nil
}

func generateResponseTypeMap(g *protogen.GeneratedFile, files []*protogen.File) error {
	oneofs, err := common.GetEventsResponseOneofs(files)
	if err != nil {
		return err
	}

	doCases := func() string {
		var ret string
		for _, oneof := range oneofs {
			msgGoIdent := common.TetragonApiIdent(g, oneof.TypeName)
			ret += fmt.Sprintf("\"%s\": &%s{},\n", oneof.FieldName, msgGoIdent)
		}
		return ret
	}

	protoMessage := common.GoIdent(g, "google.golang.org/protobuf/proto", "Message")
	g.P(`// ResponseTypeMap returns a map from event field names (e.g. "process_exec") to corresponding
    // protobuf messages (e.g. &tetragon.ProcessExec{}).
    func ResponseTypeMap() map[string]` + protoMessage + `{
		return map[string]proto.Message {
            ` + doCases() + `
        }
    }`)

	return nil
}

func generateProcessEventTuple(g *protogen.GeneratedFile, files []*protogen.File) error {
	oneofs, err := common.GetEventsResponseOneofs(files)
	if err != nil {
		return err
	}

	doCases := func() string {
		var ret string
		for _, oneof := range oneofs {
			msgGoIdent := strings.Split(common.TetragonApiIdent(g, oneof.TypeName), ".")
			goIdent := common.TetragonApiIdent(g, "GetEventsResponse_"+msgGoIdent[len(msgGoIdent)-1])
			ret += fmt.Sprintf("case *%s:\n", goIdent)
			ret += fmt.Sprintf("    return \"%s\", response.Get%s(), (*tetragon.%s)(nil)\n", oneof.FieldName, msgGoIdent[len(msgGoIdent)-1], msgGoIdent[len(msgGoIdent)-1])
		}
		return ret
	}

	tetragonGER := common.TetragonApiIdent(g, "GetEventsResponse")
	g.P(`// ProcessEventMapTuple returns a tuple from event field name (e.g. "process_exec") to corresponding
    // protobuf messages for a given tetragon.GetEventsResponse (e.g. response.GetProcessExec()).
    func ProcessEventMapTuple(response *` + tetragonGER + `) (string, any, any) {
		switch response.Event.(type) {
            ` + doCases() + `
        }
		return "", nil, nil
    }`)

	return nil
}

func generateProcessEventMapEmpty(g *protogen.GeneratedFile, files []*protogen.File) error {
	oneofs, err := common.GetEventsResponseOneofs(files)
	if err != nil {
		return err
	}

	doCases := func() string {
		var ret string
		for _, oneof := range oneofs {
			msgGoIdent := strings.Split(common.TetragonApiIdent(g, oneof.TypeName), ".")
			ret += fmt.Sprintf("\"%s\": (*tetragon.%s)(nil),\n", oneof.FieldName, msgGoIdent[len(msgGoIdent)-1])
		}
		return ret
	}

	g.P(`// ProcessEventMapEmpty returns a map from event field names (e.g. "process_exec") with nil as value
    func ProcessEventMapEmpty() map[string]any {
		return map[string]any {
            ` + doCases() + `
        }
    }`)

	return nil
}

// Generate generates boilerplate helpers
func Generate(gen *protogen.Plugin, files []*protogen.File) error {
	f, err := common.GetFirstTetragonFile(files)
	if err != nil {
		return err
	}
	g := common.NewCodegenFile(gen, f, "helpers")

	if err := generateResponseTypeString(g, files); err != nil {
		return err
	}

	if err := generateResponseGetProcess(g); err != nil {
		return err
	}

	if err := generateResponseInnerGetProcess(g, files); err != nil {
		return err
	}

	if err := generateResponseGetProcessKprobe(g); err != nil {
		return err
	}

	if err := generateResponseGetParent(g); err != nil {
		return err
	}

	// nolint:revive // ignore "if-return: redundant if just return error" for clarity
	if err := generateResponseInnerGetParent(g, files); err != nil {
		return err
	}

	if err := generateResponseGetAncestors(g); err != nil {
		return err
	}

	// nolint:revive // ignore "if-return: redundant if just return error" for clarity
	if err := generateResponseInnerGetAncestors(g, files); err != nil {
		return err
	}

	if err := generateResponseTypeMap(g, files); err != nil {
		return err
	}

	if err := generateProcessEventTuple(g, files); err != nil {
		return err
	}

	if err := generateProcessEventMapEmpty(g, files); err != nil {
		return err
	}

	return nil
}
