package ntp

import (
	"context"
	"time"

	"github.com/sagernet/sing-box/adapter"
	"github.com/sagernet/sing-box/common/dialer"
	"github.com/sagernet/sing-box/option"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/logger"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"
	"github.com/sagernet/sing/common/ntp"
)

const timeLayout = "2006-01-02 15:04:05 -0700"

var _ adapter.TimeService = (*Service)(nil)

type Service struct {
	ctx    context.Context
	cancel context.CancelFunc
	server M.Socksaddr
	dialer N.Dialer
	logger logger.Logger

	ticker      *time.Ticker
	clockOffset time.Duration
}

func NewService(ctx context.Context, router adapter.Router, logger logger.Logger, options option.NTPOptions) *Service {
	ctx, cancel := context.WithCancel(ctx)
	server := options.ServerOptions.Build()
	if server.Port == 0 {
		server.Port = 123
	}
	var interval time.Duration
	if options.Interval > 0 {
		interval = time.Duration(options.Interval)
	} else {
		interval = 30 * time.Minute
	}
	return &Service{
		ctx:    ctx,
		cancel: cancel,
		server: server,
		dialer: dialer.New(router, options.DialerOptions),
		logger: logger,
		ticker: time.NewTicker(interval),
	}
}

func (s *Service) Start() error {
	err := s.update()
	if err != nil {
		return E.Cause(err, "initialize time")
	}
	s.logger.Info("updated time: ", s.TimeFunc()().Local().Format(timeLayout))
	go s.loopUpdate()
	return nil
}

func (s *Service) Close() error {
	s.ticker.Stop()
	s.cancel()
	return nil
}

func (s *Service) TimeFunc() func() time.Time {
	return func() time.Time {
		return time.Now().Add(s.clockOffset)
	}
}

func (s *Service) loopUpdate() {
	for {
		select {
		case <-s.ctx.Done():
			return
		case <-s.ticker.C:
		}
		err := s.update()
		if err == nil {
			s.logger.Debug("updated time: ", s.TimeFunc()().Local().Format(timeLayout))
		} else {
			s.logger.Warn("update time: ", err)
		}
	}
}

func (s *Service) update() error {
	response, err := ntp.Exchange(s.ctx, s.dialer, s.server)
	if err != nil {
		return err
	}
	s.clockOffset = response.ClockOffset
	return nil
}