package main

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/labstack/echo/v4"

	"git.ma-al.com/goc_marek/ps_shop/internal/assets"
	"git.ma-al.com/goc_marek/ps_shop/internal/http/handlers"
	appmiddleware "git.ma-al.com/goc_marek/ps_shop/internal/http/middleware"
	httpproxy "git.ma-al.com/goc_marek/ps_shop/internal/http/proxy"
	pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart"
	pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog"
	psconfig "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/config"
	pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie"
	pscustomer "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/customer"
	psroutes "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/routes"
	pssession "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/session"
	"git.ma-al.com/goc_marek/ps_shop/internal/render"
	"git.ma-al.com/goc_marek/ps_shop/internal/store"
)

func main() {
	if err := run(); err != nil {
		slog.Error("fatal", "error", err)
		os.Exit(1)
	}
}

func run() error {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
	slog.SetDefault(logger)

	cfg, err := psconfig.Load()
	if err != nil {
		return fmt.Errorf("load config: %w", err)
	}

	appStore, err := store.Open(cfg.PrestaShopDBDialect, cfg.AppDBDSN)
	if err != nil {
		return fmt.Errorf("open app db: %w", err)
	}
	defer closeDB("app db", appStore)

	prestaDB, err := store.OpenPresta(cfg.PrestaShopDBDialect, cfg.PrestaShopDBDSN)
	if err != nil {
		return fmt.Errorf("open prestashop db: %w", err)
	}
	defer closeDB("prestashop db", prestaDB)

	assetManifest, err := assets.LoadManifest(cfg.AssetManifestPath)
	if err != nil {
		return fmt.Errorf("load asset manifest: %w", err)
	}

	cookieCodec, err := pscookie.NewCodec(cfg.CookieConfig())
	if err != nil {
		return fmt.Errorf("create cookie codec: %w", err)
	}

	productService := pscatalog.NewService(prestaDB, cfg.PrestaShopTablePrefix)
	customerService := pscustomer.NewService(prestaDB, cfg.PrestaShopTablePrefix)
	cartService := pscart.NewService(prestaDB, cfg.PrestaShopTablePrefix)
	routeService := psroutes.NewService(prestaDB, cfg.PrestaShopTablePrefix)
	sessionService := pssession.NewService(prestaDB, cfg.PrestaShopTablePrefix)
	productRoute, err := routeService.LoadProductRoute(context.Background())
	if err != nil {
		return fmt.Errorf("load product route rule: %w", err)
	}
	categoryRoute, err := routeService.LoadCategoryRoute(context.Background())
	if err != nil {
		return fmt.Errorf("load category route rule: %w", err)
	}
	productHandler := handlers.NewProductHandler(
		productService,
		customerService,
		cartService,
		render.New(assetManifest),
		cfg,
		categoryRoute,
	)
	categoryHandler := handlers.NewCategoryHandler(
		productService,
		customerService,
		cartService,
		render.New(assetManifest),
		cfg,
		productRoute,
		categoryRoute,
	)

	proxyHandler, err := httpproxy.New(cfg.PrestaShopProxyTarget)
	if err != nil {
		return fmt.Errorf("create reverse proxy: %w", err)
	}

	e := echo.New()
	e.HideBanner = true
	e.HidePort = true
	e.HTTPErrorHandler = appmiddleware.HTTPErrorHandler(logger)
	e.Use(
		appmiddleware.RequestID(),
		appmiddleware.RealIP(),
		appmiddleware.AccessLog(logger),
		appmiddleware.Recover(logger),
		appmiddleware.Session(cfg, cookieCodec, sessionService, productService, psroutes.CombineMatchers(productRoute, categoryRoute)),
	)

	e.Static("/dist", "web/dist")
	e.GET("/healthz", handlers.Healthz())
	e.GET("/readyz", handlers.Readyz(appStore, prestaDB, cfg.PrestaShopProxyTarget))
	e.GET("/*", func(c echo.Context) error {
		productMatch, productOK := productRoute.MatchInfo(c.Request().URL.Path)
		categoryMatch, categoryOK := categoryRoute.MatchInfo(c.Request().URL.Path)
		productSlug := ""
		productID := int64(0)
		if productOK {
			productSlug = productMatch.Slug
			productID = productMatch.ID
		}
		categorySlug := ""
		categoryID := int64(0)
		if categoryOK {
			categorySlug = categoryMatch.Slug
			categoryID = categoryMatch.ID
		}
		slog.Info("route dispatch probe",
			"method", c.Request().Method,
			"path", c.Request().URL.Path,
			"product_match", productOK,
			"product_id", productID,
			"product_slug", productSlug,
			"category_match", categoryOK,
			"category_id", categoryID,
			"category_slug", categorySlug,
		)
		if !productOK {
			if categoryOK {
				slog.Info("route dispatch", "owner", "go-category", "method", c.Request().Method, "path", c.Request().URL.Path, "slug", categorySlug, "id", categoryID)
				handlers.SetCategoryID(c, categoryID)
				handlers.SetCategorySlug(c, categorySlug)
				return categoryHandler.Show(c)
			}
			slog.Info("route dispatch", "owner", "prestashop", "method", c.Request().Method, "path", c.Request().URL.Path)
			return proxyHandler.Handle(c)
		}
		slog.Info("route dispatch", "owner", "go", "method", c.Request().Method, "path", c.Request().URL.Path, "id", productID, "slug", productSlug)
		handlers.SetProductID(c, productID)
		handlers.SetProductSlug(c, productSlug)
		return productHandler.Show(c)
	})
	e.Match([]string{
		http.MethodPost,
		http.MethodPut,
		http.MethodPatch,
		http.MethodDelete,
		http.MethodHead,
		http.MethodOptions,
		http.MethodConnect,
		http.MethodTrace,
	}, "/*", func(c echo.Context) error {
		slog.Info("route dispatch", "owner", "prestashop", "method", c.Request().Method, "path", c.Request().URL.Path)
		return proxyHandler.Handle(c)
	})

	server := &http.Server{
		Addr:              cfg.AppAddr,
		Handler:           e,
		ReadHeaderTimeout: 5 * time.Second,
	}

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

	shutdownErrCh := make(chan error, 1)
	go func() {
		<-ctx.Done()

		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		err := e.Shutdown(shutdownCtx)
		if errors.Is(err, http.ErrServerClosed) {
			err = nil
		}
		shutdownErrCh <- err
	}()

	slog.Info("starting server", "addr", cfg.AppAddr, "proxy_target", cfg.PrestaShopProxyTarget)
	if err := e.StartServer(server); err != nil && !errors.Is(err, http.ErrServerClosed) {
		return err
	}
	if ctx.Err() != nil {
		if err := <-shutdownErrCh; err != nil {
			return fmt.Errorf("shutdown server: %w", err)
		}
	}

	return nil
}

func closeDB(name string, db interface{ DB() (*sql.DB, error) }) {
	sqlDB, err := db.DB()
	if err != nil {
		slog.Error("resolve db handle", "name", name, "error", err)
		return
	}
	if err := sqlDB.Close(); err != nil {
		slog.Error("close db", "name", name, "error", err)
	}
}
