package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"sync"
	"time"
)

type Cache struct {
	mu          sync.RWMutex
	filePath    string
	contentType string
	lastSuccess time.Time
	lastAttempt time.Time
	lastError   string
	hasData     bool
}

func (c *Cache) Update(contentType string, err error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	c.lastAttempt = time.Now()

	if err != nil {
		c.lastError = "fetch failed (check server logs for details)"
		return
	}

	c.contentType = contentType
	c.lastSuccess = time.Now()
	c.lastError = ""
	c.hasData = true
}

func (c *Cache) GetContent() (filePath string, contentType string, ok bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()

	if !c.hasData {
		return "", "", false
	}
	return c.filePath, c.contentType, true
}

func (c *Cache) Status() map[string]interface{} {
	c.mu.RLock()
	defer c.mu.RUnlock()

	var lastSuccessStr, lastAttemptStr, lastErrorStr string
	if !c.lastSuccess.IsZero() {
		lastSuccessStr = c.lastSuccess.UTC().Format(time.RFC3339)
	}
	if !c.lastAttempt.IsZero() {
		lastAttemptStr = c.lastAttempt.UTC().Format(time.RFC3339)
	}
	if c.lastError != "" {
		lastErrorStr = c.lastError
	}

	return map[string]interface{}{
		"has_data":        c.hasData,
		"last_success":    lastSuccessStr,
		"last_attempt":    lastAttemptStr,
		"last_error":      lastErrorStr,
		"content_present": c.hasData,
	}
}

func fetchOnce(url, destPath string) (string, error) {
	client := &http.Client{
		Timeout: 30 * time.Second,
	}

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

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

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	tmpPath := destPath + ".tmp"
	f, err := os.Create(tmpPath)
	if err != nil {
		return "", fmt.Errorf("creating temp file: %w", err)
	}

	_, copyErr := io.Copy(f, resp.Body)
	closeErr := f.Close()
	if copyErr != nil {
		_ = os.Remove(tmpPath)
		return "", fmt.Errorf("copying body: %w", copyErr)
	}
	if closeErr != nil {
		_ = os.Remove(tmpPath)
		return "", fmt.Errorf("closing temp file: %w", closeErr)
	}

	if err := os.Rename(tmpPath, destPath); err != nil {
		_ = os.Remove(tmpPath)
		return "", fmt.Errorf("renaming temp file: %w", err)
	}

	contentType := resp.Header.Get("Content-Type")
	if contentType == "" {
		contentType = "text/xml; charset=utf-8"
	}

	return contentType, nil
}

func startFetcher(ctx context.Context, cache *Cache, url string, okInterval, failInterval time.Duration) {
	go func() {
		for {
			contentType, err := fetchOnce(url, cache.filePath)
			if err != nil {
				log.Printf("error fetching %s: %v", url, err)
			}
			cache.Update(contentType, err)

			var nextInterval time.Duration
			if err != nil || !cache.hasData {
				nextInterval = failInterval
			} else {
				nextInterval = okInterval
			}

			select {
			case <-ctx.Done():
				return
			case <-time.After(nextInterval):
			}
		}
	}()
}

func parseEnvDurationHours(key string, defaultHours int) time.Duration {
	val := os.Getenv(key)
	if val == "" {
		return time.Duration(defaultHours) * time.Hour
	}
	hours, err := strconv.Atoi(val)
	if err != nil || hours <= 0 {
		log.Printf("Invalid value for %s=%q, using default %d hours", key, val, defaultHours)
		return time.Duration(defaultHours) * time.Hour
	}
	return time.Duration(hours) * time.Hour
}

func main() {
	url := os.Getenv("CACHE_URL")
	if url == "" {
		log.Fatal("CACHE_URL env var is required")
	}

	okInterval := parseEnvDurationHours("CACHE_OK_INTERVAL_HOURS", 8)
	failInterval := parseEnvDurationHours("CACHE_FAIL_INTERVAL_HOURS", 4)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	exePath, err := os.Executable()
	if err != nil {
		log.Fatalf("failed to get executable path: %v", err)
	}
	exeDir := filepath.Dir(exePath)
	cacheFilePath := filepath.Join(exeDir, "cache.xml")

	cache := &Cache{
		filePath: cacheFilePath,
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	startFetcher(ctx, cache, url, okInterval, failInterval)

	mux := http.NewServeMux()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		path, contentType, ok := cache.GetContent()
		if !ok {
			http.Error(w, "Cache not yet initialized; no successful fetch so far", http.StatusServiceUnavailable)
			return
		}

		if contentType != "" {
			w.Header().Set("Content-Type", contentType)
		}
		http.ServeFile(w, r, path)
	})

	mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
		status := cache.Status()

		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		enc := json.NewEncoder(w)
		enc.SetIndent("", "  ")
		if err := enc.Encode(status); err != nil {
			http.Error(w, "failed to encode status", http.StatusInternalServerError)
			return
		}
	})

	addr := ":" + port
	log.Printf("Starting server on %s, caching %s to %s (ok interval: %s, fail interval: %s)", addr, url, cacheFilePath, okInterval, failInterval)
	if err := http.ListenAndServe(addr, mux); err != nil {
		log.Fatalf("server error: %v", err)
	}
}
