package grpcserver

import (
	"context"
	"log"
	"strings"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/peer"
	"google.golang.org/grpc/status"

	"git.wntrmute.dev/kyle/mcr/internal/auth"
	"git.wntrmute.dev/kyle/mcr/internal/server"
)

// authBypassMethods contains the full gRPC method names that bypass
// authentication. Health is the only method that does not require auth.
var authBypassMethods = map[string]bool{
	"/mcr.v1.AdminService/Health": true,
}

// adminRequiredMethods contains the full gRPC method names that require
// the admin role. Adding an RPC without adding it to the correct map is
// a security defect per ARCHITECTURE.md.
var adminRequiredMethods = map[string]bool{
	// Registry admin operations.
	"/mcr.v1.RegistryService/DeleteRepository": true,
	"/mcr.v1.RegistryService/GarbageCollect":   true,
	"/mcr.v1.RegistryService/GetGCStatus":      true,

	// Policy management — all RPCs require admin.
	"/mcr.v1.PolicyService/ListPolicyRules":  true,
	"/mcr.v1.PolicyService/CreatePolicyRule": true,
	"/mcr.v1.PolicyService/GetPolicyRule":    true,
	"/mcr.v1.PolicyService/UpdatePolicyRule": true,
	"/mcr.v1.PolicyService/DeletePolicyRule": true,

	// Audit — requires admin.
	"/mcr.v1.AuditService/ListAuditEvents": true,
}

// authInterceptor validates bearer tokens from the authorization metadata.
type authInterceptor struct {
	validator server.TokenValidator
}

func newAuthInterceptor(v server.TokenValidator) *authInterceptor {
	return &authInterceptor{validator: v}
}

// unary is the unary server interceptor for auth.
func (a *authInterceptor) unary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
	// Health bypasses auth.
	if authBypassMethods[info.FullMethod] {
		return handler(ctx, req)
	}

	// Extract bearer token from authorization metadata.
	token, err := extractToken(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, "authentication required")
	}

	// Validate the token via MCIAS.
	claims, err := a.validator.ValidateToken(token)
	if err != nil {
		return nil, status.Errorf(codes.Unauthenticated, "invalid token")
	}

	// Inject claims into the context.
	ctx = auth.ContextWithClaims(ctx, claims)
	return handler(ctx, req)
}

// extractToken extracts a bearer token from the "authorization" gRPC metadata.
func extractToken(ctx context.Context) (string, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", status.Errorf(codes.Unauthenticated, "missing metadata")
	}

	vals := md.Get("authorization")
	if len(vals) == 0 {
		return "", status.Errorf(codes.Unauthenticated, "missing authorization metadata")
	}

	val := vals[0]
	const prefix = "Bearer "
	if !strings.HasPrefix(val, prefix) {
		return "", status.Errorf(codes.Unauthenticated, "invalid authorization format")
	}

	token := strings.TrimSpace(val[len(prefix):])
	if token == "" {
		return "", status.Errorf(codes.Unauthenticated, "empty bearer token")
	}

	return token, nil
}

// adminInterceptor checks that the caller has the admin role for
// methods in adminRequiredMethods.
type adminInterceptor struct{}

func newAdminInterceptor() *adminInterceptor {
	return &adminInterceptor{}
}

// unary is the unary server interceptor for admin role checks.
func (a *adminInterceptor) unary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
	if !adminRequiredMethods[info.FullMethod] {
		return handler(ctx, req)
	}

	claims := auth.ClaimsFromContext(ctx)
	if claims == nil {
		return nil, status.Errorf(codes.Unauthenticated, "authentication required")
	}

	if !hasRole(claims.Roles, "admin") {
		return nil, status.Errorf(codes.PermissionDenied, "admin role required")
	}

	return handler(ctx, req)
}

// loggingInterceptor logs the method, peer IP, status code, and duration.
// It never logs the authorization metadata value.
func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
	start := time.Now()

	peerAddr := ""
	if p, ok := peer.FromContext(ctx); ok {
		peerAddr = p.Addr.String()
	}

	resp, err := handler(ctx, req)

	duration := time.Since(start)
	code := codes.OK
	if err != nil {
		if st, ok := status.FromError(err); ok {
			code = st.Code()
		} else {
			code = codes.Unknown
		}
	}

	log.Printf("grpc %s peer=%s code=%s duration=%s", info.FullMethod, peerAddr, code, duration)

	return resp, err
}

// hasRole checks if any of the roles match the target role.
func hasRole(roles []string, target string) bool {
	for _, r := range roles {
		if r == target {
			return true
		}
	}
	return false
}
