package stub

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
	"strings"
	"text/template"
)

var (
	protoPackageRe = regexp.MustCompile(`^package\s+([a-zA-Z0-9_.]+)\s*;`)
	protoServiceRe = regexp.MustCompile(`^service\s+(\w+)\s*\{`)
	protoRPCRe     = regexp.MustCompile(`^\s*rpc\s+(\w+)\s*\(`)
	protoStreamRe  = regexp.MustCompile(`\bstream\b`)
)

// ParseProtoFiles parses .proto files and extracts service definitions
// (package, service name, method names) for stub code generation.
// Streaming RPCs are skipped — only unary methods are included.
func ParseProtoFiles(files []string) ([]ProtoServiceDef, error) {
	var defs []ProtoServiceDef
	for _, f := range files {
		svcs, err := parseProtoForStub(f)
		if err != nil {
			return nil, fmt.Errorf("parse %s: %w", f, err)
		}
		defs = append(defs, svcs...)
	}
	return defs, nil
}

func parseProtoForStub(path string) ([]ProtoServiceDef, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var (
		defs       []ProtoServiceDef
		pkg        string
		inService  bool
		current    ProtoServiceDef
		braceDepth int
	)

	scanner := bufio.NewScanner(f)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())

		if strings.HasPrefix(line, "//") {
			continue
		}

		if m := protoPackageRe.FindStringSubmatch(line); m != nil {
			pkg = m[1]
			continue
		}

		if !inService {
			if m := protoServiceRe.FindStringSubmatch(line); m != nil {
				inService = true
				braceDepth = 1
				current = ProtoServiceDef{
					Package: pkg,
					Service: m[1],
				}
				continue
			}
		}

		if !inService {
			continue
		}

		braceDepth += strings.Count(line, "{") - strings.Count(line, "}")

		if braceDepth <= 0 {
			defs = append(defs, current)
			inService = false
			continue
		}

		if m := protoRPCRe.FindStringSubmatch(line); m != nil {
			// Skip streaming RPCs — only unary methods get stub handlers.
			if protoStreamRe.MatchString(line) {
				continue
			}
			current.Methods = append(current.Methods, m[1])
		}
	}

	if inService {
		defs = append(defs, current)
	}

	return defs, scanner.Err()
}

// GenerateProtoStub generates Go source files for a gRPC stub server.
// The server registers each service with manually-constructed ServiceDescs
// and returns empty (zero-value) protobuf responses for every unary method.
// Streaming RPCs are excluded during parsing.
func GenerateProtoStub(cfg *Config, defs []ProtoServiceDef) (map[string]string, error) {
	if len(defs) == 0 {
		return nil, fmt.Errorf("no proto service definitions to generate stub for %s", cfg.Service)
	}

	port := cfg.GRPCPort
	if port == 0 {
		port = DefaultGRPCPort
	}

	data := protoTemplateData{
		Service:  cfg.Service,
		Port:     port,
		Services: defs,
	}

	var b strings.Builder
	if err := protoMainTmpl.Execute(&b, data); err != nil {
		return nil, fmt.Errorf("execute proto template: %w", err)
	}

	files := map[string]string{
		"main.go": b.String(),
	}
	return files, nil
}

type protoTemplateData struct {
	Service  string
	Port     int
	Services []ProtoServiceDef
}

var protoMainTmpl = template.Must(template.New("proto-main").Parse(`package main

import (
	"context"
	"fmt"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
)

// Stub gRPC server for {{.Service}}.
// Returns zero-value (empty) responses for all unary methods.

func main() {
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", {{.Port}}))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
{{range .Services}}
	// Register {{.Package}}.{{.Service}}
	s.RegisterService(&grpc.ServiceDesc{
		ServiceName: "{{.Package}}.{{.Service}}",
		HandlerType: (*any)(nil),
		Methods: []grpc.MethodDesc{
{{- range .Methods}}
			{
				MethodName: "{{.}}",
				Handler:    zeroHandler,
			},
{{- end}}
		},
	}, &struct{}{})
{{end}}
	log.Printf("{{.Service}} gRPC stub listening on :%d", {{.Port}})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

// zeroHandler returns an empty response for any unary RPC.
// emptypb.Empty marshals to zero bytes, which is valid as the zero value
// of any proto3 message type.
func zeroHandler(_ any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
	req := &emptypb.Empty{}
	if err := dec(req); err != nil {
		return nil, status.Errorf(codes.Internal, "decode: %v", err)
	}
	if interceptor == nil {
		return &emptypb.Empty{}, nil
	}
	info := &grpc.UnaryServerInfo{}
	handler := func(_ context.Context, _ any) (any, error) {
		return &emptypb.Empty{}, nil
	}
	return interceptor(ctx, req, info, handler)
}
`))
