package main

import (
	"context"
	"errors"
	"fmt"
	"net"
	"os"
	"strconv"
	"strings"
	"time"

	userpb "juwan-backend/app/users/rpc/pb"

	corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
	typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
	"github.com/zeromicro/go-zero/core/logx"
	codepb "google.golang.org/genproto/googleapis/rpc/code"
	statuspb "google.golang.org/genproto/googleapis/rpc/status"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	headerAuthUserID   = "x-auth-user-id"
	headerAuthRoleType = "x-auth-role-type"
	headerAuthIsAdmin  = "x-auth-is-admin"
	headerCookie       = "cookie"
	cookieJToken       = "JToken"
)

type authzServer struct {
	authv3.UnimplementedAuthorizationServer
	userRPC userpb.UsercenterClient
}

func (s *authzServer) Check(ctx context.Context, req *authv3.CheckRequest) (*authv3.CheckResponse, error) {
	httpReq := req.GetAttributes().GetRequest().GetHttp()
	if httpReq == nil {
		return deny(codepb.Code_INVALID_ARGUMENT, typev3.StatusCode_BadRequest, "missing http attributes"), nil
	}

	path := httpReq.GetPath()
	if isPublicPath(path) {
		return allow(nil), nil
	}

	token, ok := getCookieValue(httpReq.GetHeaders(), cookieJToken)
	if !ok || token == "" {
		return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "missing JToken cookie"), nil
	}

	userIDHeader := getHeader(httpReq.GetHeaders(), headerAuthUserID)
	if userIDHeader == "" {
		return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "missing x-auth-user-id header"), nil
	}

	userID, err := strconv.ParseInt(userIDHeader, 10, 64)
	if err != nil || userID <= 0 {
		return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "invalid x-auth-user-id"), nil
	}

	rpcCtx, cancel := context.WithTimeout(ctx, 1200*time.Millisecond)
	defer cancel()

	resp, err := s.userRPC.ValidateToken(rpcCtx, &userpb.ValidateTokenReq{
		Token:  token,
		UserId: userID,
	})
	if err != nil {
		logx.Infof("validate token rpc failed, err: %v", err)
		return deny(codepb.Code_UNAUTHENTICATED, typev3.StatusCode_Unauthorized, "validate token failed"), nil
	}
	if !resp.GetValid() {
		logx.Infof("validate token rpc failed, err: %v", err)
		return deny(codepb.Code_PERMISSION_DENIED, typev3.StatusCode_Forbidden, "token invalid"), nil
	}

	outHeaders := []*corev3.HeaderValueOption{
		{Header: &corev3.HeaderValue{Key: headerAuthUserID, Value: strconv.FormatInt(resp.GetUserId(), 10)}},
		{Header: &corev3.HeaderValue{Key: headerAuthRoleType, Value: resp.GetRoleType()}},
	}

	if getHeader(httpReq.GetHeaders(), headerAuthIsAdmin) != "" {
		outHeaders = append(outHeaders, &corev3.HeaderValueOption{Header: &corev3.HeaderValue{Key: headerAuthIsAdmin, Value: getHeader(httpReq.GetHeaders(), headerAuthIsAdmin)}})
	}

	return allow(outHeaders), nil
}

func allow(headers []*corev3.HeaderValueOption) *authv3.CheckResponse {
	return &authv3.CheckResponse{
		Status: &statuspb.Status{Code: int32(codepb.Code_OK)},
		HttpResponse: &authv3.CheckResponse_OkResponse{
			OkResponse: &authv3.OkHttpResponse{Headers: headers},
		},
	}
}

func deny(code codepb.Code, httpCode typev3.StatusCode, message string) *authv3.CheckResponse {
	return &authv3.CheckResponse{
		Status: &statuspb.Status{Code: int32(code), Message: message},
		HttpResponse: &authv3.CheckResponse_DeniedResponse{
			DeniedResponse: &authv3.DeniedHttpResponse{
				Status: &typev3.HttpStatus{Code: httpCode},
				Body:   fmt.Sprintf(`{"code":%d,"message":"%s"}`, httpCode, message),
				Headers: []*corev3.HeaderValueOption{
					{Header: &corev3.HeaderValue{Key: "content-type", Value: "application/json"}},
				},
			},
		},
	}
}

func isPublicPath(path string) bool {
	switch path {
	case "/healthz",
		"/api/v1/auth/login",
		"/api/v1/auth/register",
		"/api/v1/auth/forgot-password",
		"/api/v1/auth/reset-password",
		"/api/v1/auth/forgot-password/send",
		"/api/v1/email/verification-code/send":
		return true
	default:
		return false
	}
}

func getHeader(headers map[string]string, key string) string {
	for k, v := range headers {
		if strings.EqualFold(k, key) {
			return v
		}
	}
	return ""
}

func getCookieValue(headers map[string]string, name string) (string, bool) {
	cookieHeader := getHeader(headers, headerCookie)
	if cookieHeader == "" {
		return "", false
	}
	parts := strings.Split(cookieHeader, ";")
	for _, part := range parts {
		kv := strings.SplitN(strings.TrimSpace(part), "=", 2)
		if len(kv) != 2 {
			continue
		}
		if kv[0] == name {
			return kv[1], true
		}
	}
	return "", false
}

func getEnvWithDefault(key, defaultValue string) string {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return defaultValue
	}
	return value
}

func run() error {
	listenOn := getEnvWithDefault("LISTEN_ON", "0.0.0.0:9002")
	userRPCTarget := getEnvWithDefault("USER_RPC_TARGET", "user-rpc-svc.juwan.svc.cluster.local:9001")

	conn, err := grpc.NewClient(userRPCTarget, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return fmt.Errorf("dial user rpc failed: %w", err)
	}
	defer conn.Close()

	lis, err := net.Listen("tcp", listenOn)
	if err != nil {
		return fmt.Errorf("listen failed: %w", err)
	}

	grpcServer := grpc.NewServer()
	authv3.RegisterAuthorizationServer(grpcServer, &authzServer{userRPC: userpb.NewUsercenterClient(conn)})

	fmt.Printf("authz-adapter listening on %s, user-rpc target %s\n", listenOn, userRPCTarget)
	return grpcServer.Serve(lis)
}

func main() {
	if err := run(); err != nil {
		if !errors.Is(err, net.ErrClosed) {
			panic(err)
		}
	}
}
