package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"gopkg.in/yaml.v3"
)

// Config represents the YAML configuration structure
type Config struct {
	CheckInterval int `yaml:"check_interval"`
	Cloudflare    struct {
		APIToken   string `yaml:"api_token"`
		ZoneID     string `yaml:"zone_id"`
		Subdomains []struct {
			Name    string `yaml:"name"`
			Proxied bool   `yaml:"proxied"`
		} `yaml:"subdomains"`
	} `yaml:"cloudflare"`
	Logging struct {
		File  string `yaml:"file"`
		Level string `yaml:"level"`
	} `yaml:"logging"`
	Retry struct {
		MaxAttempts  int `yaml:"max_attempts"`
		DelaySeconds int `yaml:"delay_seconds"`
	} `yaml:"retry"`
	Webhook struct {
		Enabled bool   `yaml:"enabled"`
		URL     string `yaml:"url"`
	} `yaml:"webhook"`
}

// CloudflareDNSRecord represents a DNS record from Cloudflare API
type CloudflareDNSRecord struct {
	ID      string `json:"id"`
	Type    string `json:"type"`
	Name    string `json:"name"`
	Content string `json:"content"`
	Proxied bool   `json:"proxied"`
	TTL     int    `json:"ttl"`
}

// CloudflareResponse is the generic API response structure
type CloudflareResponse[T any] struct {
	Success bool     `json:"success"`
	Errors  []string `json:"errors"`
	Result  T        `json:"result"`
}

// DNSUpdater manages the DDNS update process
type DNSUpdater struct {
	config     *Config
	logger     *slog.Logger
	httpClient *http.Client
	lastIP     string
}

func main() {
	configPath := flag.String("config", "config.yaml", "path to config file")
	flag.Parse()

	// Load configuration
	config, err := loadConfig(*configPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
		os.Exit(1)
	}

	// Setup logger using Go 1.21+ slog package
	logger := setupLogger(config)
	logger.Info("Starting DDNS Updater", "version", "1.0.0", "go", "1.23")

	// Create updater instance
	updater := &DNSUpdater{
		config: config,
		logger: logger,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}

	// Setup graceful shutdown
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

	go func() {
		<-sigChan
		logger.Info("Shutdown signal received, stopping...")
		cancel()
	}()

	// Run the update loop
	updater.run(ctx)
	logger.Info("DDNS Updater stopped gracefully")
}

// loadConfig reads and parses the YAML configuration file
func loadConfig(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("reading config file: %w", err)
	}

	var config Config
	if err := yaml.Unmarshal(data, &config); err != nil {
		return nil, fmt.Errorf("parsing config: %w", err)
	}

	// Validate configuration
	if config.Cloudflare.APIToken == "" {
		return nil, errors.New("cloudflare.api_token is required")
	}
	if config.Cloudflare.ZoneID == "" {
		return nil, errors.New("cloudflare.zone_id is required")
	}
	if config.CheckInterval <= 0 {
		return nil, errors.New("check_interval must be positive")
	}

	return &config, nil
}

// setupLogger creates a structured logger with file output
func setupLogger(config *Config) *slog.Logger {
	// Parse log level
	var level slog.Level
	switch strings.ToLower(config.Logging.Level) {
	case "debug":
		level = slog.LevelDebug
	case "info":
		level = slog.LevelInfo
	case "warn":
		level = slog.LevelWarn
	case "error":
		level = slog.LevelError
	default:
		level = slog.LevelInfo
	}

	// Open log file
	logFile, err := os.OpenFile(config.Logging.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to open log file: %v\n", err)
		os.Exit(1)
	}

	// Create multi-writer for both file and stdout
	multiWriter := io.MultiWriter(os.Stdout, logFile)

	// Use Go 1.21+ slog with JSON handler for structured logging
	handler := slog.NewJSONHandler(multiWriter, &slog.HandlerOptions{
		Level: level,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			// Customize timestamp format
			if a.Key == slog.TimeKey {
				return slog.String("time", a.Value.Time().Format(time.RFC3339))
			}
			return a
		},
	})

	return slog.New(handler)
}

// run is the main update loop
func (u *DNSUpdater) run(ctx context.Context) {
	ticker := time.NewTicker(time.Duration(u.config.CheckInterval) * time.Minute)
	defer ticker.Stop()

	// Run immediately on startup
	if err := u.updateDNS(ctx); err != nil {
		u.logger.Error("Initial DNS update failed", "error", err)
	}

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := u.updateDNS(ctx); err != nil {
				u.logger.Error("DNS update failed", "error", err)
			}
		}
	}
}

// updateDNS checks current IP and updates DNS records if needed
func (u *DNSUpdater) updateDNS(ctx context.Context) error {
	// Get current public IP
	currentIP, err := u.getCurrentIP(ctx)
	if err != nil {
		return fmt.Errorf("getting current IP: %w", err)
	}

	u.logger.Info("Current public IP", "ip", currentIP)

	// Check if IP has changed
	if currentIP == u.lastIP {
		u.logger.Debug("IP unchanged, skipping update")
		return nil
	}

	u.logger.Info("IP changed, updating DNS records", "old_ip", u.lastIP, "new_ip", currentIP)

	// Update each subdomain
	for _, subdomain := range u.config.Cloudflare.Subdomains {
		if err := u.updateSubdomain(ctx, subdomain.Name, currentIP, subdomain.Proxied); err != nil {
			u.logger.Error("Failed to update subdomain",
				"subdomain", subdomain.Name,
				"error", err,
			)
			continue
		}
		u.logger.Info("Successfully updated subdomain",
			"subdomain", subdomain.Name,
			"ip", currentIP,
		)
	}

	// Update last known IP
	u.lastIP = currentIP

	// Send webhook notification if enabled
	if u.config.Webhook.Enabled {
		go u.sendWebhook(currentIP)
	}

	return nil
}

// getCurrentIP fetches the current public IPv4 address
func (u *DNSUpdater) getCurrentIP(ctx context.Context) (string, error) {
	services := []string{
		"https://api.ipify.org",
		"https://checkip.amazonaws.com",
		"https://icanhazip.com",
	}

	var lastErr error
	for _, service := range services {
		req, err := http.NewRequestWithContext(ctx, "GET", service, nil)
		if err != nil {
			lastErr = err
			continue
		}

		resp, err := u.httpClient.Do(req)
		if err != nil {
			lastErr = err
			u.logger.Debug("IP service failed", "service", service, "error", err)
			continue
		}

		body, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			lastErr = err
			continue
		}

		if resp.StatusCode == http.StatusOK {
			ip := strings.TrimSpace(string(body))
			if ip != "" {
				return ip, nil
			}
		}
	}

	return "", fmt.Errorf("all IP services failed: %w", lastErr)
}

// updateSubdomain updates a single DNS record with retry logic
func (u *DNSUpdater) updateSubdomain(ctx context.Context, subdomain, ip string, proxied bool) error {
	var lastErr error

	for attempt := range u.config.Retry.MaxAttempts {
		err := u.updateSubdomainAttempt(ctx, subdomain, ip, proxied)
		if err == nil {
			return nil
		}

		lastErr = err
		u.logger.Warn("Update attempt failed",
			"subdomain", subdomain,
			"attempt", attempt+1,
			"max_attempts", u.config.Retry.MaxAttempts,
			"error", err,
		)

		if attempt < u.config.Retry.MaxAttempts-1 {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-time.After(time.Duration(u.config.Retry.DelaySeconds) * time.Second):
			}
		}
	}

	return fmt.Errorf("failed after %d attempts: %w", u.config.Retry.MaxAttempts, lastErr)
}

// updateSubdomainAttempt performs a single update attempt
func (u *DNSUpdater) updateSubdomainAttempt(ctx context.Context, subdomain, ip string, proxied bool) error {
	// First, get the current DNS record
	recordID, currentIP, err := u.getDNSRecord(ctx, subdomain)
	if err != nil {
		return fmt.Errorf("getting DNS record: %w", err)
	}

	// Check if update is needed
	if currentIP == ip {
		u.logger.Debug("DNS record already up to date", "subdomain", subdomain, "ip", ip)
		return nil
	}

	// Update the DNS record
	url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s",
		u.config.Cloudflare.ZoneID, recordID)

	payload := map[string]any{
		"type":    "A",
		"name":    subdomain,
		"content": ip,
		"proxied": proxied,
		"ttl":     300,
	}

	jsonData, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshaling payload: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "PATCH", url, bytes.NewReader(jsonData))
	if err != nil {
		return fmt.Errorf("creating request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+u.config.Cloudflare.APIToken)
	req.Header.Set("Content-Type", "application/json")

	resp, err := u.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("executing request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("reading response: %w", err)
	}

	var cfResp CloudflareResponse[CloudflareDNSRecord]
	if err := json.Unmarshal(body, &cfResp); err != nil {
		return fmt.Errorf("parsing response: %w", err)
	}

	if !cfResp.Success {
		return fmt.Errorf("cloudflare API error: %v", cfResp.Errors)
	}

	return nil
}

// getDNSRecord retrieves the record ID and current IP for a subdomain
func (u *DNSUpdater) getDNSRecord(ctx context.Context, subdomain string) (recordID, currentIP string, err error) {
	url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=A&name=%s",
		u.config.Cloudflare.ZoneID, subdomain)

	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return "", "", fmt.Errorf("creating request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+u.config.Cloudflare.APIToken)
	req.Header.Set("Content-Type", "application/json")

	resp, err := u.httpClient.Do(req)
	if err != nil {
		return "", "", fmt.Errorf("executing request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", "", fmt.Errorf("reading response: %w", err)
	}

	var cfResp CloudflareResponse[[]CloudflareDNSRecord]
	if err := json.Unmarshal(body, &cfResp); err != nil {
		return "", "", fmt.Errorf("parsing response: %w", err)
	}

	if !cfResp.Success {
		return "", "", fmt.Errorf("cloudflare API error: %v", cfResp.Errors)
	}

	if len(cfResp.Result) == 0 {
		return "", "", fmt.Errorf("no DNS record found for %s", subdomain)
	}

	record := cfResp.Result[0]
	return record.ID, record.Content, nil
}

// sendWebhook sends a notification to the configured webhook (placeholder)
func (u *DNSUpdater) sendWebhook(newIP string) {
	if u.config.Webhook.URL == "" {
		return
	}

	payload := map[string]string{
		"event":  "ip_changed",
		"new_ip": newIP,
		"time":   time.Now().Format(time.RFC3339),
	}

	jsonData, _ := json.Marshal(payload)
	req, err := http.NewRequest("POST", u.config.Webhook.URL, bytes.NewReader(jsonData))
	if err != nil {
		u.logger.Error("Failed to create webhook request", "error", err)
		return
	}

	req.Header.Set("Content-Type", "application/json")

	resp, err := u.httpClient.Do(req)
	if err != nil {
		u.logger.Error("Failed to send webhook", "error", err)
		return
	}
	defer resp.Body.Close()

	u.logger.Info("Webhook sent", "status", resp.StatusCode)
}
