package clashapi

import (
	"bytes"
	"context"
	"errors"
	"net"
	"net/http"
	"os"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/sagernet/cors"
	"github.com/sagernet/sing-box/adapter"
	"github.com/sagernet/sing-box/common/trafficcontrol"
	"github.com/sagernet/sing-box/common/urltest"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/experimental"
	"github.com/sagernet/sing-box/log"
	"github.com/sagernet/sing-box/option"
	"github.com/sagernet/sing/common"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/json"
	"github.com/sagernet/sing/common/observable"
	"github.com/sagernet/sing/service"
	"github.com/sagernet/sing/service/filemanager"
	"github.com/sagernet/ws"
	"github.com/sagernet/ws/wsutil"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/render"
)

func init() {
	experimental.RegisterClashServerConstructor(NewServer)
}

var _ adapter.ClashServer = (*Server)(nil)

type Server struct {
	ctx            context.Context
	network        adapter.NetworkManager
	router         adapter.Router
	dnsRouter      adapter.DNSRouter
	outbound       adapter.OutboundManager
	endpoint       adapter.EndpointManager
	logger         log.Logger
	httpServer     *http.Server
	trafficManager *trafficcontrol.Manager
	urlTestHistory *urltest.HistoryStorage
	logDebug       bool

	mode             string
	modeList         []string
	modeUpdateAccess sync.Mutex
	modeUpdateHooks  []*observable.Subscriber[struct{}]

	externalController       bool
	externalUI               string
	externalUIDownloadURL    string
	externalUIDownloadDetour string
}

func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
	trafficManager := service.PtrFromContext[trafficcontrol.Manager](ctx)
	if trafficManager == nil {
		return nil, E.New("missing traffic manager")
	}
	urlTestHistory := service.PtrFromContext[urltest.HistoryStorage](ctx)
	if urlTestHistory == nil {
		return nil, E.New("missing URL test history storage")
	}
	chiRouter := chi.NewRouter()
	s := &Server{
		ctx:       ctx,
		network:   service.FromContext[adapter.NetworkManager](ctx),
		router:    service.FromContext[adapter.Router](ctx),
		dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
		outbound:  service.FromContext[adapter.OutboundManager](ctx),
		endpoint:  service.FromContext[adapter.EndpointManager](ctx),
		logger:    logFactory.NewLogger("clash-api"),
		httpServer: &http.Server{
			Addr:    options.ExternalController,
			Handler: chiRouter,
		},
		trafficManager:           trafficManager,
		urlTestHistory:           urlTestHistory,
		logDebug:                 logFactory.Level() >= log.LevelDebug,
		modeList:                 options.ModeList,
		externalController:       options.ExternalController != "",
		externalUIDownloadURL:    options.ExternalUIDownloadURL,
		externalUIDownloadDetour: options.ExternalUIDownloadDetour,
	}
	defaultMode := "Rule"
	if options.DefaultMode != "" {
		defaultMode = options.DefaultMode
	}
	if !common.Contains(s.modeList, defaultMode) {
		s.modeList = append([]string{defaultMode}, s.modeList...)
	}
	s.mode = defaultMode
	//goland:noinspection GoDeprecation
	//nolint:staticcheck
	if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" {
		return nil, E.New("cache_file and related fields in Clash API is deprecated in sing-box 1.8.0, use experimental.cache_file instead.")
	}
	allowedOrigins := options.AccessControlAllowOrigin
	if len(allowedOrigins) == 0 {
		allowedOrigins = []string{"*"}
	}
	cors := cors.New(cors.Options{
		AllowedOrigins:      allowedOrigins,
		AllowedMethods:      []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
		AllowedHeaders:      []string{"Content-Type", "Authorization"},
		AllowPrivateNetwork: options.AccessControlAllowPrivateNetwork,
		MaxAge:              300,
	})
	chiRouter.Use(cors.Handler)
	chiRouter.Group(func(r chi.Router) {
		r.Use(authentication(options.Secret))
		r.Get("/", hello(options.ExternalUI != ""))
		r.Get("/logs", getLogs(s.ctx, logFactory))
		r.Get("/traffic", traffic(s.ctx, trafficManager))
		r.Get("/version", version)
		r.Mount("/configs", configRouter(s, logFactory))
		r.Mount("/proxies", proxyRouter(s, s.router))
		r.Mount("/rules", ruleRouter(s.router))
		r.Mount("/connections", connectionRouter(s.ctx, s.network, trafficManager))
		r.Mount("/providers/proxies", proxyProviderRouter())
		r.Mount("/providers/rules", ruleProviderRouter())
		r.Mount("/script", scriptRouter())
		r.Mount("/profile", profileRouter())
		r.Mount("/cache", cacheRouter(ctx))
		r.Mount("/dns", dnsRouter(s.dnsRouter))

		s.setupMetaAPI(r)
	})
	if options.ExternalUI != "" {
		s.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI))
		_, err := filemanager.ReadDir(ctx, s.externalUI)
		if err != nil && !os.IsNotExist(err) {
			return nil, E.Cause(err, "read external UI directory")
		}
		chiRouter.Group(func(r chi.Router) {
			r.Get("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently).ServeHTTP)
			r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(Dir(s.externalUI))))
		})
	}
	return s, nil
}

func (s *Server) Name() string {
	return "clash server"
}

func (s *Server) Start(stage adapter.StartStage) error {
	switch stage {
	case adapter.StartStateStart:
		cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
		if cacheFile != nil {
			mode := cacheFile.LoadMode()
			if common.Any(s.modeList, func(it string) bool {
				return strings.EqualFold(it, mode)
			}) {
				s.mode = mode
			}
		}
	case adapter.StartStateStarted:
		if s.externalController {
			s.checkAndDownloadExternalUI()
			var (
				listener net.Listener
				err      error
			)
			for range 3 {
				listener, err = net.Listen("tcp", s.httpServer.Addr)
				if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) {
					time.Sleep(100 * time.Millisecond)
					continue
				}
				break
			}
			if err != nil {
				return E.Cause(err, "external controller listen error")
			}
			s.logger.Info("restful api listening at ", listener.Addr())
			go func() {
				err = s.httpServer.Serve(listener)
				if err != nil && !errors.Is(err, http.ErrServerClosed) {
					s.logger.Error("external controller serve error: ", err)
				}
			}()
		}
	}

	return nil
}

func (s *Server) Close() error {
	return common.Close(
		common.PtrOrNil(s.httpServer),
	)
}

func (s *Server) Mode() string {
	return s.mode
}

func (s *Server) ModeList() []string {
	return s.modeList
}

func (s *Server) AddModeUpdateHook(hook *observable.Subscriber[struct{}]) {
	s.modeUpdateAccess.Lock()
	defer s.modeUpdateAccess.Unlock()
	s.modeUpdateHooks = append(s.modeUpdateHooks, hook)
}

func (s *Server) SetMode(newMode string) {
	if !common.Contains(s.modeList, newMode) {
		newMode = common.Find(s.modeList, func(it string) bool {
			return strings.EqualFold(it, newMode)
		})
	}
	if !common.Contains(s.modeList, newMode) {
		return
	}
	if newMode == s.mode {
		return
	}
	s.mode = newMode
	s.modeUpdateAccess.Lock()
	for _, hook := range s.modeUpdateHooks {
		hook.Emit(struct{}{})
	}
	s.modeUpdateAccess.Unlock()
	s.dnsRouter.ClearCache()
	cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
	if cacheFile != nil {
		err := cacheFile.StoreMode(newMode)
		if err != nil {
			s.logger.Error(E.Cause(err, "save mode"))
		}
	}
	s.logger.Info("updated mode: ", newMode)
}

func authentication(serverSecret string) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		fn := func(w http.ResponseWriter, r *http.Request) {
			if serverSecret == "" {
				next.ServeHTTP(w, r)
				return
			}

			// Browser websocket not support custom header
			if r.Header.Get("Upgrade") == "websocket" && r.URL.Query().Get("token") != "" {
				token := r.URL.Query().Get("token")
				if token != serverSecret {
					render.Status(r, http.StatusUnauthorized)
					render.JSON(w, r, ErrUnauthorized)
					return
				}
				next.ServeHTTP(w, r)
				return
			}

			header := r.Header.Get("Authorization")
			bearer, token, found := strings.Cut(header, " ")

			hasInvalidHeader := bearer != "Bearer"
			hasInvalidSecret := !found || token != serverSecret
			if hasInvalidHeader || hasInvalidSecret {
				render.Status(r, http.StatusUnauthorized)
				render.JSON(w, r, ErrUnauthorized)
				return
			}
			next.ServeHTTP(w, r)
		}
		return http.HandlerFunc(fn)
	}
}

func hello(redirect bool) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		contentType := r.Header.Get("Content-Type")
		if !redirect || contentType == "application/json" {
			render.JSON(w, r, render.M{"hello": "clash"})
		} else {
			http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect)
		}
	}
}

type Traffic struct {
	Up   int64 `json:"up"`
	Down int64 `json:"down"`
}

func traffic(ctx context.Context, trafficManager *trafficcontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		var conn net.Conn
		if r.Header.Get("Upgrade") == "websocket" {
			var err error
			conn, _, _, err = ws.UpgradeHTTP(r, w)
			if err != nil {
				return
			}
			defer conn.Close()
		}

		if conn == nil {
			w.Header().Set("Content-Type", "application/json")
			render.Status(r, http.StatusOK)
		}

		tick := time.NewTicker(time.Second)
		defer tick.Stop()
		buf := &bytes.Buffer{}
		uploadTotal, downloadTotal := trafficManager.Total()
		for {
			select {
			case <-ctx.Done():
				return
			case <-tick.C:
			}
			buf.Reset()
			uploadTotalNew, downloadTotalNew := trafficManager.Total()
			err := json.NewEncoder(buf).Encode(Traffic{
				Up:   uploadTotalNew - uploadTotal,
				Down: downloadTotalNew - downloadTotal,
			})
			if err != nil {
				break
			}
			if conn == nil {
				_, err = w.Write(buf.Bytes())
				w.(http.Flusher).Flush()
			} else {
				err = wsutil.WriteServerText(conn, buf.Bytes())
			}
			if err != nil {
				break
			}

			uploadTotal = uploadTotalNew
			downloadTotal = downloadTotalNew
		}
	}
}

type Log struct {
	Type    string `json:"type"`
	Payload string `json:"payload"`
}

func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		levelText := r.URL.Query().Get("level")
		if levelText == "" {
			levelText = "info"
		}

		level, ok := log.ParseLevel(levelText)
		if ok != nil {
			render.Status(r, http.StatusBadRequest)
			render.JSON(w, r, ErrBadRequest)
			return
		}

		subscription, done, err := logFactory.Subscribe()
		if err != nil {
			render.Status(r, http.StatusNoContent)
			return
		}
		defer logFactory.UnSubscribe(subscription)

		var conn net.Conn
		if r.Header.Get("Upgrade") == "websocket" {
			conn, _, _, err = ws.UpgradeHTTP(r, w)
			if err != nil {
				return
			}
			defer conn.Close()
		}

		if conn == nil {
			w.Header().Set("Content-Type", "application/json")
			render.Status(r, http.StatusOK)
		}

		buf := &bytes.Buffer{}
		var logEntry log.Entry
		for {
			select {
			case <-ctx.Done():
				return
			case <-done:
				return
			case logEntry = <-subscription:
			}
			if logEntry.Level > level {
				continue
			}
			buf.Reset()
			err = json.NewEncoder(buf).Encode(Log{
				Type:    log.FormatLevel(logEntry.Level),
				Payload: logEntry.Message,
			})
			if err != nil {
				break
			}
			if conn == nil {
				_, err = w.Write(buf.Bytes())
				w.(http.Flusher).Flush()
			} else {
				err = wsutil.WriteServerText(conn, buf.Bytes())
			}

			if err != nil {
				break
			}
		}
	}
}

func version(w http.ResponseWriter, r *http.Request) {
	render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true, "meta": true})
}
