package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/golang/protobuf/proto"
	"github.com/golang/protobuf/protoc-gen-go/descriptor"
	pbplugin "github.com/golang/protobuf/protoc-gen-go/plugin"
	"github.com/pkg/errors"

	"code.justin.tv/eventbus/schema/cmd/internal/recon"
	"code.justin.tv/eventbus/schema/cmd/internal/util"
	rpcinfra "code.justin.tv/eventbus/schema/cmd/protoc-gen-eventbus-metadata/rpcinfra"
)

var errLog = log.New(os.Stderr, "", log.LstdFlags)

func main() {
	err := metadatagen()
	if err != nil {
		errLog.Printf("Fatal error: %s\n", err.Error())
		os.Exit(1)
	}
}

func metadatagen() error {
	data, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		return errors.Wrap(err, "could not read input")
	}
	var request pbplugin.CodeGeneratorRequest

	if err := proto.Unmarshal(data, &request); err != nil {
		return errors.Wrap(err, "could not parse input proto")
	}

	if len(request.FileToGenerate) == 0 {
		return errors.New("no files to generate")
	}

	response, err := buildFiles(&request)
	if err != nil {
		return errors.Wrap(err, "could not generate metadata from proto")
	}

	data, err = proto.Marshal(response)
	if err != nil {
		return errors.Wrap(err, "failed to marshal output proto")
	}
	_, err = os.Stdout.Write(data)
	if err != nil {
		return errors.Wrap(err, "failed to write output proto")
	}
	return nil
}

func buildFiles(req *pbplugin.CodeGeneratorRequest) (*pbplugin.CodeGeneratorResponse, error) {
	var response pbplugin.CodeGeneratorResponse
	lookup := recon.Recon(req)
	cwd, _ := os.Getwd()
	eventsFolder := "."
	if filepath.Base(cwd) != "events" {
		eventsFolder = "events"
	}

	for _, filename := range req.FileToGenerate {
		f := lookup.Files[filename]
		fileNameWithoutExt := strings.Split(filepath.Base(filename), ".")[0]
		pkgDir := filepath.Base(filepath.Dir(filename))
		expectedEventStreamName := util.SnakeToUpperCamel(pkgDir) + util.SnakeToUpperCamel(fileNameWithoutExt)
		eventStreamMetadata := &rpcinfra.EventDefinition{}
		schema, err := ioutil.ReadFile(filepath.Join(eventsFolder, filename))
		if err != nil {
			return nil, errors.Wrap(err, "could not read protobuf file for event stream")
		}

		// Check for a markdown file and use that as the description
		descriptionFilepath := fmt.Sprintf("%s/%s.md", filepath.Dir(filename), fileNameWithoutExt)
		description, err := ioutil.ReadFile(descriptionFilepath)
		if os.IsNotExist(err) {
			// in the future, the implementation of ReadFile may change, the returned value might
			// not always be empty in an error, so we set it just in case
			description = []byte{}
		} else if err != nil {
			return nil, errors.Wrap(err, "could not read protobuf file for event")
		}

		var fileDescProto *descriptor.FileDescriptorProto
		for _, protoFile := range req.ProtoFile {
			if protoFile.GetName() == filename {
				fileDescProto = protoFile
			}
		}
		if fileDescProto == nil {
			return nil, fmt.Errorf("could not find proto file descriptor for file '%s'", filename)
		}

		// get comments on each message type
		// TODO: grok out all the [4,X] paths and their comments
		// comments := getMessageTypeComments()

		encryptedFields := recon.GetEncryptedFields(expectedEventStreamName, f)
		encryptedFieldsRPC := make([]*rpcinfra.AuthorizedField, len(encryptedFields))
		for i := range encryptedFields {
			encryptedFieldsRPC[i] = &rpcinfra.AuthorizedField{
				MessageName: encryptedFields[i].MessageName,
				FieldName:   encryptedFields[i].FieldName,
			}
		}

		for _, messageType := range fileDescProto.GetMessageType() {
			if messageType.GetName() == expectedEventStreamName {
				eventStreamMetadata.EventType = expectedEventStreamName
				eventStreamMetadata.DisplayName = expectedEventStreamName
				eventStreamMetadata.Schema = string(schema)
				eventStreamMetadata.Description = string(description)
				eventStreamMetadata.RepoFilepath = filename
				eventStreamMetadata.AuthorizedFields = encryptedFieldsRPC
				eventStreamMetadata.Deprecated = messageType.GetOptions().GetDeprecated()

				ldapGroup, err := recon.GetOwnerLDAPGroup(messageType)
				if err != nil {
					return nil, errors.Wrapf(err, "could not determine LDAP group ownership for '%s'", messageType.GetName())
				}
				eventStreamMetadata.LdapGroup = ldapGroup
			}
		}

		if eventStreamMetadata.EventType == "" {
			return nil, fmt.Errorf("expecting message type '%s' in file '%s.proto'", expectedEventStreamName, fileNameWithoutExt)
		}

		b, err := json.Marshal(eventStreamMetadata)
		if err != nil {
			return nil, errors.Wrap(err, "could not marshal event stream metadata to json")
		}
		outFile := &pbplugin.CodeGeneratorResponse_File{
			Name:    proto.String(fmt.Sprintf("%s/%s.metadata.json", filepath.Dir(filename), fileNameWithoutExt)),
			Content: proto.String(string(b)),
		}
		response.File = append(response.File, outFile)
	}
	return &response, nil
}

func getPackageName(goPkgString string) string {
	parts := strings.Split(goPkgString, ";")
	if len(parts) == 1 {
		return filepath.Base(parts[0])
	} else if len(parts) == 2 {
		return parts[1]
	} else {
		log.Fatalf("invalid 'go_package' option '%s'", goPkgString)
		return ""
	}
}
