// Package sub provides subscription server functionality for the 3x-ui panel,
// including HTTP/HTTPS servers for serving subscription links and JSON configurations.
package sub

import (
	"context"
	"crypto/tls"
	"io"
	"io/fs"
	"net"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/mhsanaei/3x-ui/v3/internal/logger"
	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
	"github.com/mhsanaei/3x-ui/v3/internal/web/locale"
	"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
	"github.com/mhsanaei/3x-ui/v3/internal/web/network"
	"github.com/mhsanaei/3x-ui/v3/internal/web/service"

	"github.com/gin-gonic/gin"
)

// Server represents the subscription server that serves subscription links and JSON configurations.
type Server struct {
	httpServer *http.Server
	listener   net.Listener

	sub            *SUBController
	settingService service.SettingService

	ctx    context.Context
	cancel context.CancelFunc
}

// NewServer creates a new subscription server instance with a cancellable context.
func NewServer() *Server {
	ctx, cancel := context.WithCancel(context.Background())
	return &Server{
		ctx:    ctx,
		cancel: cancel,
	}
}

// initRouter configures the subscription server's Gin engine, middleware,
// templates and static assets and returns the ready-to-use engine.
func (s *Server) initRouter() (*gin.Engine, error) {
	// Always run in release mode for the subscription server
	gin.DefaultWriter = io.Discard
	gin.DefaultErrorWriter = io.Discard
	gin.SetMode(gin.ReleaseMode)

	engine := gin.Default()

	subDomain, err := s.settingService.GetSubDomain()
	if err != nil {
		return nil, err
	}

	if subDomain != "" {
		engine.Use(middleware.DomainValidatorMiddleware(subDomain))
	}

	LinksPath, err := s.settingService.GetSubPath()
	if err != nil {
		return nil, err
	}

	JsonPath, err := s.settingService.GetSubJsonPath()
	if err != nil {
		return nil, err
	}

	ClashPath, err := s.settingService.GetSubClashPath()
	if err != nil {
		return nil, err
	}

	subJsonEnable, err := s.settingService.GetSubJsonEnable()
	if err != nil {
		return nil, err
	}

	subClashEnable, err := s.settingService.GetSubClashEnable()
	if err != nil {
		return nil, err
	}

	subClashAutoDetect, err := s.settingService.GetSubClashAutoDetect()
	if err != nil {
		subClashAutoDetect = false
	}

	subJsonAutoDetect, err := s.settingService.GetSubJsonAutoDetect()
	if err != nil {
		subJsonAutoDetect = false
	}

	subJsonAlwaysArray, err := s.settingService.GetSubJsonAlwaysArray()
	if err != nil {
		subJsonAlwaysArray = false
	}

	subJsonUserAgentRegex, err := s.settingService.GetSubJsonUserAgentRegex()
	if err != nil {
		subJsonUserAgentRegex = service.DefaultSubJsonUserAgentRegex
	}

	subClashUserAgentRegex, err := s.settingService.GetSubClashUserAgentRegex()
	if err != nil {
		subClashUserAgentRegex = service.DefaultSubClashUserAgentRegex
	}

	// Set base_path based on LinksPath for template rendering
	// Ensure LinksPath ends with "/" for proper asset URL generation
	basePath := LinksPath
	if basePath != "/" && !strings.HasSuffix(basePath, "/") {
		basePath += "/"
	}
	// logger.Debug("sub: Setting base_path to:", basePath)
	engine.Use(func(c *gin.Context) {
		c.Set("base_path", basePath)
	})

	Encrypt, err := s.settingService.GetSubEncrypt()
	if err != nil {
		return nil, err
	}

	RemarkTemplate, err := s.settingService.GetRemarkTemplate()
	if err != nil {
		RemarkTemplate = ""
	}

	SubUpdates, err := s.settingService.GetSubUpdates()
	if err != nil {
		SubUpdates = "10"
	}

	SubJsonMux, err := s.settingService.GetSubJsonMux()
	if err != nil {
		SubJsonMux = ""
	}

	SubJsonRules, err := s.settingService.GetSubJsonRules()
	if err != nil {
		SubJsonRules = ""
	}

	SubJsonFinalMask, err := s.settingService.GetSubJsonFinalMask()
	if err != nil {
		SubJsonFinalMask = ""
	}

	SubClashEnableRouting, err := s.settingService.GetSubClashEnableRouting()
	if err != nil {
		SubClashEnableRouting = false
	}

	SubClashRules, err := s.settingService.GetSubClashRules()
	if err != nil {
		SubClashRules = ""
	}

	SubTitle, err := s.settingService.GetSubTitle()
	if err != nil {
		SubTitle = ""
	}

	SubSupportUrl, err := s.settingService.GetSubSupportUrl()
	if err != nil {
		SubSupportUrl = ""
	}

	SubProfileUrl, err := s.settingService.GetSubProfileUrl()
	if err != nil {
		SubProfileUrl = ""
	}

	SubAnnounce, err := s.settingService.GetSubAnnounce()
	if err != nil {
		SubAnnounce = ""
	}

	SubEnableRouting, err := s.settingService.GetSubEnableRouting()
	if err != nil {
		return nil, err
	}

	SubRoutingRules, err := s.settingService.GetSubRoutingRules()
	if err != nil {
		SubRoutingRules = ""
	}

	SubHideSettings, err := s.settingService.GetSubHideSettings()
	if err != nil {
		SubHideSettings = false
	}

	SubIncyEnableRouting, err := s.settingService.GetSubIncyEnableRouting()
	if err != nil {
		SubIncyEnableRouting = false
	}

	SubIncyRoutingRules, err := s.settingService.GetSubIncyRoutingRules()
	if err != nil {
		SubIncyRoutingRules = ""
	}

	// set per-request localizer from headers/cookies
	engine.Use(locale.LocalizerMiddleware())

	// Mount the Vite-built dist/assets/ so the subscription page's JS/CSS
	// bundles load from `/assets/...`. Also mount the same FS under the
	// subscription path prefix (LinksPath + "assets") so reverse proxies
	// running the panel under a URI prefix can resolve those URLs too.
	// Note: LinksPath always starts and ends with "/" (validated in settings).
	var linksPathForAssets string
	if LinksPath == "/" {
		linksPathForAssets = "/assets"
	} else {
		linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
	}

	var assetsFS http.FileSystem
	if _, err := os.Stat("internal/web/dist/assets"); err == nil {
		assetsFS = http.FS(os.DirFS("internal/web/dist/assets"))
	} else if subFS, err := fs.Sub(distFS, "dist/assets"); err == nil {
		assetsFS = http.FS(subFS)
	} else {
		logger.Error("sub: failed to mount embedded dist assets:", err)
	}

	if assetsFS != nil {
		engine.StaticFS("/assets", assetsFS)
		if linksPathForAssets != "/assets" {
			engine.StaticFS(linksPathForAssets, assetsFS)
		}

		// Browser may resolve subpage assets relative to the request URL —
		// /sub/<basePath>/<subId>/assets/... — so route those to the same FS.
		if LinksPath != "/" {
			engine.Use(func(c *gin.Context) {
				path := c.Request.URL.Path
				pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
				if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
					_, after, ok := strings.Cut(path, "/assets/")
					if ok {
						assetPath := after // +8 to skip "/assets/"
						if assetPath != "" {
							c.FileFromFS(assetPath, assetsFS)
							c.Abort()
							return
						}
					}
				}
				c.Next()
			})
		}
	}

	g := engine.Group("/")

	s.sub = NewSUBController(g,
		WithSUBPath(LinksPath),
		WithSUBJsonPath(JsonPath),
		WithSUBClashPath(ClashPath),
		WithSUBClashAutoDetect(subClashAutoDetect),
		WithSUBClashUserAgentRegex(subClashUserAgentRegex),
		WithSUBJsonAutoDetect(subJsonAutoDetect),
		WithSUBJsonUserAgentRegex(subJsonUserAgentRegex),
		WithSUBJsonAlwaysArray(subJsonAlwaysArray),
		WithSUBJsonEnabled(subJsonEnable),
		WithSUBClashEnabled(subClashEnable),
		WithSUBEncryption(Encrypt),
		WithSUBRemarkTemplate(RemarkTemplate),
		WithSUBUpdateInterval(SubUpdates),
		WithSUBJsonMux(SubJsonMux),
		WithSUBJsonRules(SubJsonRules),
		WithSUBJsonFinalMask(SubJsonFinalMask),
		WithSUBClashEnableRouting(SubClashEnableRouting),
		WithSUBClashRules(SubClashRules),
		WithSUBTitle(SubTitle),
		WithSUBSupportURL(SubSupportUrl),
		WithSUBProfileURL(SubProfileUrl),
		WithSUBAnnounce(SubAnnounce),
		WithSUBEnableRouting(SubEnableRouting),
		WithSUBRoutingRules(SubRoutingRules),
		WithSUBHideSettings(SubHideSettings),
		WithSUBIncyEnableRouting(SubIncyEnableRouting),
		WithSUBIncyRoutingRules(SubIncyRoutingRules),
	)

	return engine, nil
}

// Start initializes and starts the subscription server with configured settings.
func (s *Server) Start() (err error) {
	// This is an anonymous function, no function name
	defer func() {
		if err != nil {
			_ = s.Stop()
		}
	}()

	subEnable, err := s.settingService.GetSubEnable()
	if err != nil {
		return err
	}
	if !subEnable {
		return nil
	}

	engine, err := s.initRouter()
	if err != nil {
		return err
	}

	certFile, err := s.settingService.GetSubCertFile()
	if err != nil {
		return err
	}
	keyFile, err := s.settingService.GetSubKeyFile()
	if err != nil {
		return err
	}
	listen, err := s.settingService.GetSubListen()
	if err != nil {
		return err
	}
	port, err := s.settingService.GetSubPort()
	if err != nil {
		return err
	}

	listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
	listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", listenAddr)
	if err != nil {
		return err
	}

	if certFile != "" || keyFile != "" {
		cert, err := tls.LoadX509KeyPair(certFile, keyFile)
		if err == nil {
			c := &tls.Config{
				Certificates: []tls.Certificate{cert},
			}
			listener = network.NewAutoHttpsListener(listener)
			listener = tls.NewListener(listener, c)
			logger.Info("Sub server running HTTPS on", listener.Addr())
		} else {
			logger.Error("Error loading certificates:", err)
			logger.Info("Sub server running HTTP on", listener.Addr())
		}
	} else {
		logger.Info("Sub server running HTTP on", listener.Addr())
	}
	s.listener = listener

	s.httpServer = &http.Server{
		Handler: engine,
		// The subscription server is the most exposed (public) listener; without
		// these a few slow-header connections exhaust it (Slowloris). Mirrors the
		// panel server timeouts in internal/web/web.go.
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       30 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       120 * time.Second,
	}

	go func() {
		_ = s.httpServer.Serve(listener)
	}()

	return nil
}

// Stop gracefully shuts down the subscription server and closes the listener.
func (s *Server) Stop() error {
	s.cancel()

	var err1 error
	var err2 error
	if s.httpServer != nil {
		shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer shutdownCancel()
		err1 = s.httpServer.Shutdown(shutdownCtx)
	}
	if s.listener != nil {
		err2 = s.listener.Close()
	}
	return common.Combine(err1, err2)
}

// GetCtx returns the server's context for cancellation and deadline management.
func (s *Server) GetCtx() context.Context {
	return s.ctx
}
