package kvm

import (
	"fmt"
	"net/http"
	"os/exec"
	"strconv"
	"time"

	"github.com/beevik/ntp"
)

const (
	timeSyncRetryStep     = 5 * time.Second
	timeSyncRetryMaxInt   = 1 * time.Minute
	timeSyncWaitNetChkInt = 100 * time.Millisecond
	timeSyncWaitNetUpInt  = 3 * time.Second
	timeSyncInterval      = 1 * time.Hour
	timeSyncTimeout       = 2 * time.Second
)

var (
	builtTimestamp        string
	timeSyncRetryInterval = 0 * time.Second
	timeSyncSuccess       = false
	defaultNTPServers     = []string{
		"time.cloudflare.com",
		"time.apple.com",
	}
)

func isTimeSyncNeeded() bool {
	if builtTimestamp == "" {
		ntpLogger.Warn().Msg("Built timestamp is not set, time sync is needed")
		return true
	}

	ts, err := strconv.Atoi(builtTimestamp)
	if err != nil {
		ntpLogger.Warn().Str("error", err.Error()).Msg("Failed to parse built timestamp")
		return true
	}

	// builtTimestamp is UNIX timestamp in seconds
	builtTime := time.Unix(int64(ts), 0)
	now := time.Now()

	ntpLogger.Debug().Str("built_time", builtTime.Format(time.RFC3339)).Str("now", now.Format(time.RFC3339)).Msg("Built time and now")

	if now.Sub(builtTime) < 0 {
		ntpLogger.Warn().Msg("System time is behind the built time, time sync is needed")
		return true
	}

	return false
}

func TimeSyncLoop() {
	for {
		if !networkState.checked {
			time.Sleep(timeSyncWaitNetChkInt)
			continue
		}

		if !networkState.Up {
			ntpLogger.Info().Msg("Waiting for network to come up")
			time.Sleep(timeSyncWaitNetUpInt)
			continue
		}

		// check if time sync is needed, but do nothing for now
		isTimeSyncNeeded()

		ntpLogger.Info().Msg("Syncing system time")
		start := time.Now()
		err := SyncSystemTime()
		if err != nil {
			ntpLogger.Error().Str("error", err.Error()).Msg("Failed to sync system time")

			// retry after a delay
			timeSyncRetryInterval += timeSyncRetryStep
			time.Sleep(timeSyncRetryInterval)
			// reset the retry interval if it exceeds the max interval
			if timeSyncRetryInterval > timeSyncRetryMaxInt {
				timeSyncRetryInterval = 0
			}

			continue
		}
		timeSyncSuccess = true
		ntpLogger.Info().Str("now", time.Now().Format(time.RFC3339)).
			Str("time_taken", time.Since(start).String()).
			Msg("Time sync successful")
		time.Sleep(timeSyncInterval) // after the first sync is done
	}
}

func SyncSystemTime() (err error) {
	now, err := queryNetworkTime()
	if err != nil {
		return fmt.Errorf("failed to query network time: %w", err)
	}
	err = setSystemTime(*now)
	if err != nil {
		return fmt.Errorf("failed to set system time: %w", err)
	}
	return nil
}

func queryNetworkTime() (*time.Time, error) {
	ntpServers, err := getNTPServersFromDHCPInfo()
	if err != nil {
		ntpLogger.Info().Err(err).Msg("failed to get NTP servers from DHCP info")
	}

	if ntpServers == nil {
		ntpServers = defaultNTPServers
		ntpLogger.Info().
			Interface("ntp_servers", ntpServers).
			Msg("Using default NTP servers")
	} else {
		ntpLogger.Info().
			Interface("ntp_servers", ntpServers).
			Msg("Using NTP servers from DHCP")
	}

	for _, server := range ntpServers {
		now, err := queryNtpServer(server, timeSyncTimeout)
		if err == nil {
			ntpLogger.Info().
				Str("ntp_server", server).
				Str("time", now.Format(time.RFC3339)).
				Msg("NTP server returned time")
			return now, nil
		} else {
			ntpLogger.Error().
				Str("ntp_server", server).
				Str("error", err.Error()).
				Msg("failed to query NTP server")
		}
	}

	httpUrls := []string{
		"http://apple.com",
		"http://cloudflare.com",
	}
	for _, url := range httpUrls {
		now, err := queryHttpTime(url, timeSyncTimeout)
		if err == nil {
			ntpLogger.Info().
				Str("http_url", url).
				Str("time", now.Format(time.RFC3339)).
				Msg("HTTP server returned time")
			return now, nil
		} else {
			ntpLogger.Error().
				Str("http_url", url).
				Str("error", err.Error()).
				Msg("failed to query HTTP server")
		}
	}

	return nil, ErrorfL(ntpLogger, "failed to query network time, all NTP servers and HTTP servers failed", nil)
}

func queryNtpServer(server string, timeout time.Duration) (now *time.Time, err error) {
	resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout})
	if err != nil {
		return nil, err
	}
	return &resp.Time, nil
}

func queryHttpTime(url string, timeout time.Duration) (*time.Time, error) {
	client := http.Client{
		Timeout: timeout,
	}
	resp, err := client.Head(url)
	if err != nil {
		return nil, err
	}
	dateStr := resp.Header.Get("Date")
	now, err := time.Parse(time.RFC1123, dateStr)
	if err != nil {
		return nil, err
	}
	return &now, nil
}

func setSystemTime(now time.Time) error {
	nowStr := now.Format("2006-01-02 15:04:05")
	output, err := exec.Command("date", "-s", nowStr).CombinedOutput()
	if err != nil {
		return fmt.Errorf("failed to run date -s: %w, %s", err, string(output))
	}
	return nil
}
