package protofilter

import (
	"context"
	"io"
	"net"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"

	"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
	"github.com/v2fly/v2ray-core/v5/common/environment/filesystemcap"
	"github.com/v2fly/v2ray-core/v5/common/protoext"
)

//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen

func FilterProtoConfig(ctx context.Context, config proto.Message) error {
	messageProtoReflect := config.ProtoReflect()
	return filterMessage(ctx, messageProtoReflect)
}

func filterMessage(ctx context.Context, message protoreflect.Message) error {
	var err error
	type fileRead struct {
		filename string
		field    string
	}
	var fileReadingQueue []fileRead

	type pendingWrite struct {
		field protoreflect.FieldDescriptor
		value protoreflect.Value
	}

	var pendingWriteQueue []pendingWrite

	message.Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool {
		v2extension, ferr := protoext.GetFieldOptions(descriptor)
		if ferr == nil {
			if v2extension.Forbidden {
				if value.Bool() {
					err = newError("a forbidden value is set ", descriptor.FullName())
					return false
				}
			}

			if v2extension.ConvertTimeReadFileInto != "" {
				fileReadingQueue = append(fileReadingQueue, fileRead{
					filename: value.String(),
					field:    v2extension.ConvertTimeReadFileInto,
				})
			}

			if v2extension.ConvertTimeParseIp != "" {
				ipValue := net.ParseIP(value.String())
				target := message.Descriptor().Fields().ByTextName(v2extension.ConvertTimeParseIp)
				if ipValue.To4() != nil {
					ipValue = ipValue.To4()
				}
				pendingWriteQueue = append(pendingWriteQueue, pendingWrite{
					field: target,
					value: protoreflect.ValueOf([]byte(ipValue)),
				})
			}
		}

		switch descriptor.Kind() {
		case protoreflect.MessageKind:
			if descriptor.IsMap() {
				err = filterMap(ctx, descriptor, value.Map())
				break
			}
			if descriptor.IsList() {
				err = filterList(ctx, value.List())
				break
			}
			err = filterMessage(ctx, value.Message())
		}
		return true
	})

	if err != nil {
		return err
	}

	fsenvironment := envctx.EnvironmentFromContext(ctx)
	fsifce, fsifceOk := fsenvironment.(filesystemcap.FileSystemCapabilitySet)
	for _, v := range fileReadingQueue {
		if !fsifceOk {
			return newError("unable to read file as filesystem capability is not given")
		}
		field := message.Descriptor().Fields().ByTextName(v.field)
		if v.filename == "" {
			continue
		}

		if len(message.Get(field).Bytes()) > 0 {
			continue
		}

		file, err := fsifce.OpenFileForRead()(v.filename)
		if err != nil {
			return newError("unable to open file").Base(err)
		}
		fileContent, err := io.ReadAll(file)
		if err != nil {
			return newError("unable to read file").Base(err)
		}
		file.Close()
		message.Set(field, protoreflect.ValueOf(fileContent))
	}

	for _, v := range pendingWriteQueue {
		message.Set(v.field, v.value)
	}
	return nil
}

func filterMap(ctx context.Context, descriptor protoreflect.FieldDescriptor, mapValue protoreflect.Map) error {
	if descriptor.MapValue().Kind() != protoreflect.MessageKind {
		return nil
	}
	var err error
	mapValue.Range(func(key protoreflect.MapKey, value protoreflect.Value) bool {
		err = filterMessage(ctx, value.Message())
		return err == nil
	})
	return err
}

func filterList(ctx context.Context, listValue protoreflect.List) error {
	var err error
	size := listValue.Len()
	for i := 0; i < size; i++ {
		err = filterMessage(ctx, listValue.Get(i).Message())
		if err != nil {
			return err
		}
	}
	return nil
}
