package app

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"time"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	"route256/cart/internal/app/server"
	loms_service "route256/cart/internal/clients/loms"
	product_service "route256/cart/internal/clients/products"
	"route256/cart/internal/domain/repository"
	"route256/cart/internal/domain/service"
	"route256/cart/internal/infra/config"
	"route256/cart/internal/infra/http/middlewares"
	"route256/cart/internal/infra/http/round_trippers"

	pbLOMS "route256/pkg/api/loms/v1"
)

const (
	productsRetryAttemptsDefault   = 3
	productsInitialDelaySecDefault = 1
)

type App struct {
	config *config.Config
	server http.Server
}

func NewApp(configPath string) (*App, error) {
	c, err := config.LoadConfig(configPath)
	if err != nil {
		return nil, fmt.Errorf("unable to load config: %w", err)
	}

	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
	zerolog.SetGlobalLevel(zerolog.InfoLevel)

	if c.Service.LogLevel != "" {
		level, logErr := zerolog.ParseLevel(c.Service.LogLevel)
		if logErr != nil {
			return nil, fmt.Errorf("unknown log level `%s` provided: %w", c.Service.LogLevel, logErr)
		}

		zerolog.SetGlobalLevel(level)
	}

	log.WithLevel(zerolog.GlobalLevel()).Msgf("using logging level=`%s`", zerolog.GlobalLevel().String())

	app := &App{
		config: c,
	}

	service, err := app.setupCartService()
	if err != nil {
		return nil, err
	}

	app.server.Handler = app.BootstrapHandlers(service)

	return app, nil
}

func (app *App) Shutdown(ctx context.Context) error {
	return app.server.Shutdown(ctx)
}

func (app *App) ListenAndServe(_ context.Context) error {
	address := fmt.Sprintf("%s:%s", app.config.Service.Host, app.config.Service.Port)

	l, err := net.Listen("tcp", address)
	if err != nil {
		return err
	}

	log.Info().Msgf("Serving cart at http://%s", l.Addr())

	return app.server.Serve(l)
}

func (app *App) setupCartService() (*service.CartService, error) {
	// Product service client
	transport := http.DefaultTransport
	transport = round_trippers.NewLogRoundTripper(transport)
	transport = round_trippers.NewMetricsRoundTripper(transport)
	transport = round_trippers.NewRetryRoundTripper(transport, productsRetryAttemptsDefault, productsInitialDelaySecDefault)

	httpClient := http.Client{
		Transport: transport,
		Timeout:   10 * time.Second,
	}

	productService := product_service.NewProductService(
		httpClient,
		app.config.ProductService.Token,
		fmt.Sprintf("%s:%s", app.config.ProductService.Host, app.config.ProductService.Port),
		app.config.ProductService.Limit,
		app.config.ProductService.Burst,
		app.config.Service.Workers,
	)

	// LOMS service client
	conn, err := grpc.NewClient(
		fmt.Sprintf("%s:%s", app.config.LomsService.Host, app.config.LomsService.Port),
		grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, fmt.Errorf("grpc.NewClient: %w", err)
	}

	lomsClient := pbLOMS.NewLOMSClient(conn)
	lomsService := loms_service.NewLomsService(lomsClient)

	const userCartCap = 100
	cartRepository := repository.NewInMemoryRepository(userCartCap)

	return service.NewCartService(cartRepository, productService, lomsService), nil
}

func (app *App) BootstrapHandlers(cartService *service.CartService) http.Handler {
	s := server.NewServer(cartService)

	mx := http.NewServeMux()
	mx.HandleFunc("POST /user/{user_id}/cart/{sku_id}", s.AddItemHandler)
	mx.HandleFunc("POST /checkout/{user_id}", s.CheckoutHandler)
	mx.HandleFunc("GET /user/{user_id}/cart", s.GetItemsByUserIDHandler)
	mx.HandleFunc("DELETE /user/{user_id}/cart/{sku_id}", s.DeleteItemHandler)
	mx.HandleFunc("DELETE /user/{user_id}/cart", s.DeleteItemsByUserIDHandler)

	mx.Handle("GET /metrics", promhttp.Handler())

	h := middlewares.NewTracingMiddleware(mx)
	h = middlewares.NewMetricsMiddleware(h)
	h = middlewares.NewTimerMiddleware(h)

	return h
}
