package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
)

type Config struct {
	UpdateURL         string `json:"update_url"`
	CheckIntervalSecs int    `json:"check_interval_seconds"`
	IPCheckURL        string `json:"ip_check_url"`
}

func loadConfig(path string) (*Config, error) {
	data, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var cfg Config
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, err
	}
	return &cfg, nil
}

type configHolder struct {
	mu  sync.RWMutex
	cfg *Config
}

func (h *configHolder) get() *Config {
	h.mu.RLock()
	defer h.mu.RUnlock()
	return h.cfg
}

func (h *configHolder) set(cfg *Config) {
	h.mu.Lock()
	defer h.mu.Unlock()
	h.cfg = cfg
}

func watchConfig(path string, holder *configHolder, tickerReset chan<- time.Duration) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		fmt.Println("Config watcher: failed to create watcher:", err)
		return
	}
	defer watcher.Close()

	if err := watcher.Add(path); err != nil {
		fmt.Println("Config watcher: failed to watch file:", err)
		return
	}

	// Debounce timer — editors often fire multiple events per save
	// (truncate then write, or swapping a temp file in).
	const debounce = 100 * time.Millisecond
	var debounceTimer *time.Timer

	reload := func() {
		newCfg, err := loadConfig(path)
		if err != nil {
			fmt.Println("Config watcher: failed to reload config:", err)
			return
		}
		oldCfg := holder.get()
		holder.set(newCfg)
		fmt.Println(time.Now(), "Config reloaded")

		if newCfg.CheckIntervalSecs != oldCfg.CheckIntervalSecs {
			fmt.Printf("%v Check interval changed: %ds → %ds\n",
				time.Now(), oldCfg.CheckIntervalSecs, newCfg.CheckIntervalSecs)
			tickerReset <- time.Duration(newCfg.CheckIntervalSecs) * time.Second
		}
	}

	for {
		select {
		case event, ok := <-watcher.Events:
			if !ok {
				return
			}
			// Write and Create cover both in-place saves and atomic
			// rename-based saves (used by vim, many editors on Linux).
			if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
				// If it was an atomic rename the old watch target is gone;
				// re-add the new file under the same path.
				if event.Has(fsnotify.Create) {
					_ = watcher.Add(path)
				}
				if debounceTimer != nil {
					debounceTimer.Stop()
				}
				debounceTimer = time.AfterFunc(debounce, reload)
			}

		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			fmt.Println("Config watcher: error:", err)
		}
	}
}

func getPublicIP(ipURL string) (string, error) {
	resp, err := http.Get(ipURL)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(body)), nil
}

func updateDDNS(url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed, status code %d", resp.StatusCode)
	}
	return nil
}

func main() {
	print("Trying out some prints to see what's going on\n");
	const configPath = "config.json"

	cfg, err := loadConfig(configPath)
	if err != nil {
		fmt.Println("Failed to load config:", err)
		os.Exit(1)
	}

	holder := &configHolder{cfg: cfg}
	tickerReset := make(chan time.Duration, 1)

	go watchConfig(configPath, holder, tickerReset)

	lastIP, err := getPublicIP(cfg.IPCheckURL)
	if err != nil {
		fmt.Println("Error getting initial public IP:", err)
		lastIP = ""
	} else {
		fmt.Println(time.Now(), "Starting with current IP:", lastIP)
	}

	ticker := time.NewTicker(time.Duration(cfg.CheckIntervalSecs) * time.Second)
	defer ticker.Stop()

	for {
		select {
		case newInterval := <-tickerReset:
			ticker.Reset(newInterval)
			continue

		case <-ticker.C:
			current := holder.get()

			ip, err := getPublicIP(current.IPCheckURL)
			if err != nil {
				fmt.Println("Error getting public IP:", err)
				continue
			}
			if ip == lastIP {
				fmt.Println(time.Now(), "IP unchanged:", ip)
				continue
			}

			fmt.Println(time.Now(), "IP changed to", ip)
			if err := updateDDNS(current.UpdateURL); err != nil {
				fmt.Println("Failed to update DDNS:", err)
			} else {
				fmt.Println(time.Now(), "DDNS updated successfully")
				lastIP = ip
			}
		}
	}
}
