package grpc_interface

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"

	pb "github.com/ArkLabsHQ/fulmine/api-spec/protobuf/gen/go/fulmine/v1"
	"github.com/ArkLabsHQ/fulmine/internal/core/application"
	"github.com/ArkLabsHQ/fulmine/internal/core/ports"
	"github.com/ArkLabsHQ/fulmine/internal/interface/grpc/handlers"
	"github.com/ArkLabsHQ/fulmine/internal/interface/grpc/interceptors"
	"github.com/ArkLabsHQ/fulmine/internal/interface/web"
	"github.com/ArkLabsHQ/fulmine/pkg/macaroon"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	log "github.com/sirupsen/logrus"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	grpchealth "google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/protobuf/encoding/protojson"
)

type service struct {
	cfg         Config
	appSvc      *application.Service
	httpServer  *http.Server
	grpcServer  *grpc.Server
	unlockerSvc ports.Unlocker
	macaroonSvc macaroon.Service
	appStopCh   chan struct{}
	feStopCh    chan struct{}
}

func NewService(
	cfg Config,
	appSvc *application.Service,
	unlockerSvc ports.Unlocker,
	sentryEnabled bool,
	macaroonSvc macaroon.Service,
	arkServer string,
) (*service, error) {
	if err := cfg.Validate(); err != nil {
		return nil, fmt.Errorf("invalid config: %s", err)
	}

	appStopCh := make(chan struct{}, 1)
	feStopCh := make(chan struct{}, 1)

	grpcConfig := []grpc.ServerOption{
		interceptors.MacaroonAuthInterceptor(macaroonSvc),
		interceptors.MacaroonStreamAuthInterceptor(macaroonSvc),
		interceptors.UnaryInterceptor(sentryEnabled),
		interceptors.StreamInterceptor(sentryEnabled),
	}
	if cfg.WithTLS {
		return nil, fmt.Errorf("tls termination not supported yet")
	}
	creds := insecure.NewCredentials()
	if !cfg.insecure() {
		creds = credentials.NewTLS(cfg.tlsConfig())
	}
	grpcConfig = append(grpcConfig, grpc.Creds(creds))

	grpcServer := grpc.NewServer(grpcConfig...)

	walletHandler := handlers.NewWalletHandler(appSvc)
	pb.RegisterWalletServiceServer(grpcServer, walletHandler)

	serviceHandler := handlers.NewServiceHandler(appSvc)
	pb.RegisterServiceServer(grpcServer, serviceHandler)

	notificationHandler := handlers.NewNotificationHandler(appSvc, appStopCh)
	pb.RegisterNotificationServiceServer(grpcServer, notificationHandler)

	healthHandler := handlers.NewHealthHandler()
	grpchealth.RegisterHealthServer(grpcServer, healthHandler)

	gatewayCreds := insecure.NewCredentials()
	if !cfg.insecure() {
		gatewayCreds = credentials.NewTLS(&tls.Config{
			InsecureSkipVerify: true, // #nosec
		})
	}
	gatewayOpts := grpc.WithTransportCredentials(gatewayCreds)
	conn, err := grpc.NewClient(
		cfg.gatewayAddress(), gatewayOpts,
	)
	if err != nil {
		return nil, err
	}

	authHeaderMatcher := func(key string) (string, bool) {
		switch key {
		case "X-Macaroon":
			return "macaroon", true
		default:
			return key, false
		}
	}
	gwmux := runtime.NewServeMux(
		runtime.WithIncomingHeaderMatcher(authHeaderMatcher),
		runtime.WithHealthzEndpoint(grpchealth.NewHealthClient(conn)),
		runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{
			MarshalOptions: protojson.MarshalOptions{
				Indent:    "  ",
				Multiline: true,
			},
			UnmarshalOptions: protojson.UnmarshalOptions{
				DiscardUnknown: true,
			},
		}),
	)
	ctx := context.Background()
	if err := pb.RegisterServiceHandler(
		ctx, gwmux, conn,
	); err != nil {
		return nil, err
	}
	if err := pb.RegisterWalletServiceHandler(
		ctx, gwmux, conn,
	); err != nil {
		return nil, err
	}
	if err := pb.RegisterNotificationServiceHandler(
		ctx, gwmux, conn,
	); err != nil {
		return nil, err
	}

	feHandler := web.NewService(appSvc, feStopCh, sentryEnabled, arkServer)

	mux := http.NewServeMux()
	mux.Handle("/", feHandler)
	mux.Handle("/api/", http.StripPrefix("/api", gwmux))
	mux.Handle("/static/", feHandler)

	httpServerHandler := http.Handler(mux)
	if cfg.insecure() {
		httpServerHandler = h2c.NewHandler(httpServerHandler, &http2.Server{})
	}

	httpServer := &http.Server{
		Addr:      cfg.httpAddress(),
		Handler:   httpServerHandler,
		TLSConfig: cfg.tlsConfig(),
	}

	svc := &service{
		cfg,
		appSvc,
		httpServer,
		grpcServer,
		unlockerSvc,
		macaroonSvc,
		appStopCh,
		feStopCh,
	}

	if macaroonSvc != nil {
		go svc.listenToWalletUpdates()
	}

	return svc, nil
}

func (s *service) Start() error {
	listener, err := net.Listen("tcp", s.cfg.grpcAddress())
	if err != nil {
		return err
	}
	// nolint:all
	go s.grpcServer.Serve(listener)
	log.Infof("started GRPC server at %s", s.cfg.grpcAddress())

	if s.cfg.insecure() {
		// nolint:all
		go s.httpServer.ListenAndServe()
	} else {
		// nolint:all
		go s.httpServer.ListenAndServeTLS("", "")
	}
	log.Infof("started HTTP server at %s", s.cfg.httpAddress())

	if s.unlockerSvc != nil {
		if err := s.autoUnlock(); err != nil {
			log.Warnf("failed to auto-unlock: %v", err)
		}
	}

	return nil
}

// autoUnlock attempts to unlock the wallet automatically using the unlocker service
func (s *service) autoUnlock() error {
	ctx := context.Background()

	if !s.appSvc.IsReady() {
		log.Debug("wallet not initialized, skipping auto unlock")
		return nil
	}

	password, err := s.unlockerSvc.GetPassword(ctx)
	if err != nil {
		return fmt.Errorf("failed to get password: %s", err)
	}

	if err := s.appSvc.UnlockNode(ctx, password); err != nil {
		return fmt.Errorf("failed to auto unlock: %s", err)
	}

	log.Info("wallet auto unlocked")
	return nil
}

func (s *service) Stop() {
	s.appStopCh <- struct{}{}
	s.feStopCh <- struct{}{}

	s.grpcServer.GracefulStop()
	log.Info("stopped grpc server")
	// nolint:all
	s.httpServer.Shutdown(context.Background())
	log.Info("stopped http server")
}

func (s *service) listenToWalletUpdates() {
	ctx := context.Background()
	for update := range s.appSvc.GetWalletUpdates() {
		switch update.Type {
		case application.WalletInit, application.WalletUnlock:
			if err := s.macaroonSvc.Unlock(ctx, update.Password); err != nil {
				log.WithError(err).Fatal("failed to setup macaroon service")
			}
			if err := s.macaroonSvc.Generate(ctx); err != nil {
				log.WithError(err).Fatal("failed to generate macaroons")
			}
		case application.WalletReset:
			if err := s.macaroonSvc.Reset(ctx); err != nil {
				log.WithError(err).Fatal("failed to reset macaroon service")
			}
		}
	}
}
