package grpcserver

import (
	"context"
	"log/slog"
	"path"

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

	"git.wntrmute.dev/mc/metacrypt/internal/audit"
	"git.wntrmute.dev/mc/metacrypt/internal/auth"
	"git.wntrmute.dev/mc/metacrypt/internal/seal"
)

// sealInterceptor rejects calls with FailedPrecondition when the vault is
// sealed, for the listed methods. It is intended to run as a PreInterceptor
// in the mcdsl grpcserver chain, before logging and auth.
func sealInterceptor(sealMgr *seal.Manager, logger *slog.Logger, methods map[string]bool) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		if !methods[info.FullMethod] {
			return handler(ctx, req)
		}
		if sealMgr.State() != seal.StateUnsealed {
			logger.Debug("grpc request rejected: vault sealed", "method", info.FullMethod)
			return nil, status.Error(codes.FailedPrecondition, "vault is sealed")
		}
		return handler(ctx, req)
	}
}

// auditInterceptor logs an audit event after each RPC completes. It is
// intended to run as a PostInterceptor in the mcdsl grpcserver chain,
// after auth so that caller info is available in the context via
// auth.TokenInfoFromContext.
func auditInterceptor(auditLog *audit.Logger) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		resp, err := handler(ctx, req)

		caller := "anonymous"
		var roles []string
		if ti := auth.TokenInfoFromContext(ctx); ti != nil {
			caller = ti.Username
			roles = ti.Roles
		}

		outcome := "success"
		var errMsg string
		if err != nil {
			outcome = "error"
			if st, ok := status.FromError(err); ok {
				if st.Code() == codes.PermissionDenied || st.Code() == codes.Unauthenticated {
					outcome = "denied"
				}
				errMsg = st.Message()
			} else {
				errMsg = err.Error()
			}
		}

		auditLog.Log(ctx, audit.Event{
			Caller:    caller,
			Roles:     roles,
			Operation: path.Base(info.FullMethod),
			Resource:  info.FullMethod,
			Outcome:   outcome,
			Error:     errMsg,
		})

		return resp, err
	}
}
