package app

import (
	"context"
	"errors"
	"fmt"
	"github.com/themakers/hlog"
	"go.uber.org/zap/zapcore"
	"os/signal"
	"penahub.gitlab.yandexcloud.net/backend/penahub_common/mongo"
	"penahub.gitlab.yandexcloud.net/external/trashlog/app"
	"penahub.gitlab.yandexcloud.net/external/trashlog/wrappers/zaptrashlog"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/controller/http"
	"syscall"
	"time"

	"github.com/twmb/franz-go/pkg/kgo"
	"go.uber.org/zap"
	qutils "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/utils"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/initialize"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/server"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/closer"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/kafka"
	tb "gopkg.in/tucnak/telebot.v2"
)

const (
	shutdownTimeout = 5 * time.Second
)

type Build struct {
	Commit    string
	Version   string
	BuildTime int64
}

func Run(config *models.Config, logger *zap.Logger, build Build) (appErr error) {
	defer func() {
		if recovered := recover(); recovered != nil {
			appErr = errors.New("recovered panic on application run")
			logger.Error("recovered panic on application run", zap.Any("recovered", recovered))
		}
	}()

	closer := closer.New()

	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer cancel()

	clickHouseLogger, err := zaptrashlog.NewCore(ctx, zap.InfoLevel, config.Service.TrashLogHost, build.Version, build.Commit, build.BuildTime)
	if err != nil {
		panic(err)
	}

	loggerForHlog := logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
		return zapcore.NewTee(core, clickHouseLogger)
	}), zap.AddCallerSkip(2))

	loggerHlog := hlog.New(loggerForHlog).Module(config.Service.ModuleLogger)
	loggerHlog.With(models.AllFields{})
	loggerHlog.Emit(app.InfoSvcStarted{})

	if err = kafka.Initialize(ctx, config.Service.Kafka.Brokers, []string{
		config.Service.Kafka.Tariff.Topic,
	}); err != nil {
		return fmt.Errorf("failed initialize kafka: %w", err)
	}

	mongoDB, err := mongo.Connect(ctx, &mongo.ConnectDeps{
		Configuration: &config.Database,
		Timeout:       10 * time.Second,
	})
	if err != nil {
		return fmt.Errorf("failed connection to db: %w", err)
	}

	kafkaTariffClient, err := kgo.NewClient(
		kgo.SeedBrokers(config.Service.Kafka.Brokers...),
		kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),
		kgo.DefaultProduceTopic(config.Service.Kafka.Tariff.Topic),
		kgo.ConsumeTopics(config.Service.Kafka.Tariff.Topic),
	)
	if err != nil {
		return err
	}

	brokers := initialize.NewBrokers(initialize.BrokersDeps{
		Logger:       logger,
		TariffClient: kafkaTariffClient,
	})

	err = kafkaTariffClient.Ping(ctx)
	if err != nil {
		return err
	}

	clients := initialize.NewClients(initialize.ClientsDeps{
		Logger:                       logger,
		AuthURL:                      &config.Service.AuthMicroservice.URL,
		HubadminURL:                  &config.Service.HubadminMicroservice.URL,
		CurrencyURL:                  &config.Service.CurrencyMicroservice.URL,
		DiscountServiceConfiguration: &config.Service.DiscountMicroservice,
		PaymentServiceConfiguration:  &config.Service.PaymentMicroservice,
		VerificationURL:              &config.Service.VerificationMicroservice.URL,
		TemplategenURL:               &config.Service.TemplategenMicroserviceURL.URL,
		MailClient:                   &config.Service.Mail,
		CodewordServiceHost:          &config.Service.CodewordMicroservice,
	})

	repositories := initialize.NewRepositories(initialize.RepositoriesDeps{
		Logger:  logger,
		MongoDB: mongoDB,
	})

	notificationBot, err := tb.NewBot(tb.Settings{
		Token:     config.Service.NotificationBotToken,
		Verbose:   false,
		ParseMode: tb.ModeHTML,
		Poller: &tb.LongPoller{
			Timeout: time.Second,
		},
	})
	if err != nil {
		//logger.Emit(json.Token(err))
		return err
	}

	services := initialize.NewServices(initialize.ServicesDeps{
		Logger:            logger,
		Repositories:      repositories,
		Clients:           clients,
		ConfigurationGRPC: &config.GRPC,
		Brokers:           brokers,
		Notifier:					 notificationBot,
		NotificationChannel:	config.Service.NotificationChannel,
		AdminURL: 				 config.Service.AdminURL,
	})

	rpcControllers := initialize.NewRpcControllers(initialize.RpcControllersDeps{
		Logger:       logger,
		Services:     services,
		Repositories: repositories,
		HLogger:      loggerHlog,
	})

	encrypt := qutils.NewEncrypt(config.Service.PubKey, config.Service.PrivKey)
	middleWare := http.NewMiddleWare(logger)

	httpControllers := initialize.NewHttpControllers(initialize.HttpControllersDeps{
		Logger:       logger,
		Encrypt:      encrypt,
		Producer:     brokers.TariffProducer,
		GRPC:         &config.GRPC,
		Repositories: repositories,
		Clients:      clients,
		MiddleWare:   middleWare,
	})

	serverClientHTTP := server.NewServer(server.ServerConfig{
		Logger:      logger,
		Hlog:        loggerHlog,
		Controllers: []server.Controller{httpControllers.CurrencyClientController, httpControllers.HistoryClientController, httpControllers.CartClientController, httpControllers.WalletClientController, httpControllers.AccountClientController},
		JWTConfig:   &config.Service.JWT,
	})

	serverAdminHTTP := server.NewServer(server.ServerConfig{
		Logger:      logger,
		Hlog:        loggerHlog,
		Controllers: []server.Controller{httpControllers.AccountAdminController, httpControllers.CurrencyAdminController, httpControllers.HistoryAdminController},
		JWTConfig:   &config.Service.JWT,
	})

	serverClientHTTP.ListRoutes()
	serverAdminHTTP.ListRoutes()

	serverGRPC, grpcErr := server.NewGRPC(server.DepsGRPC{Logger: logger})
	if grpcErr != nil {
		return grpcErr.Wrap("failed to init grpc server")
	}

	serverGRPC.Register(rpcControllers)

	go func() {
		if err := serverClientHTTP.Start(config.HTTP.ClientHost + ":" + config.HTTP.ClientPort); err != nil {
			logger.Error("Server external startup error", zap.Error(err))
			cancel()
		}
	}()

	go func() {
		if err := serverAdminHTTP.Start(config.HTTP.AdminHost + ":" + config.HTTP.AdminPort); err != nil {
			logger.Error("Server internal startup error", zap.Error(err))
			cancel()
		}
	}()

	go serverGRPC.Run(&config.GRPC)

	closer.Add(mongoDB.Client().Disconnect)
	closer.Add(serverClientHTTP.Shutdown)
	closer.Add(serverAdminHTTP.Shutdown)
	closer.Add(serverGRPC.Stop)
	closer.Add(closer.Wrap(kafkaTariffClient.Close))

	<-ctx.Done()

	logger.Info("shutting down app gracefully")

	shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)

	defer cancel()

	if err := closer.Close(shutdownCtx); err != nil {
		return fmt.Errorf("closer: %w", err)
	}

	return nil
}
