package main

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

	"github.com/jackc/pgx/v5"
)

type defconState struct {
	mu    sync.RWMutex
	Level int `json:"level"`
}

func (d *defconState) get() int {
	d.mu.RLock()
	defer d.mu.RUnlock()
	return d.Level
}

func (d *defconState) set(level int) {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.Level = level
}

var defcon = defconState{Level: 5}
var marketOrder = []string{
	"USD/JPY",
	"OIL/USD",
	"XAU/USD",
	"USD/EUR",
	"USD/CHF",
	"USD/GBP",
}

var marketAliases = map[string]string{
	"yen":     "USD/JPY",
	"oil":     "OIL/USD",
	"gold":    "XAU/USD",
	"usd_eur": "USD/EUR",
	"usd_chf": "USD/CHF",
	"usd_gbp": "USD/GBP",
	"EUR/USD": "USD/EUR",
	"EUR/GBP": "USD/GBP",
}

var uniMainzNet = mustParseCIDR("134.93.0.0/16")

func mustParseCIDR(cidr string) *net.IPNet {
	_, block, err := net.ParseCIDR(cidr)
	if err != nil {
		log.Fatalf("invalid CIDR %q: %v", cidr, err)
	}
	return block
}

func clientIP(r *http.Request) net.IP {
	var candidates []string

	if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
		parts := strings.Split(fwd, ",")
		for _, p := range parts {
			if trimmed := strings.TrimSpace(p); trimmed != "" {
				candidates = append(candidates, trimmed)
			}
		}
	}

	if real := strings.TrimSpace(r.Header.Get("X-Real-IP")); real != "" {
		candidates = append(candidates, real)
	}

	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err == nil && host != "" {
		candidates = append(candidates, host)
	} else if r.RemoteAddr != "" {
		candidates = append(candidates, r.RemoteAddr)
	}

	for _, raw := range candidates {
		ip := net.ParseIP(raw)
		if ip == nil {
			continue
		}
		if ip4 := ip.To4(); ip4 != nil {
			return ip4
		}
		return ip
	}
	return nil
}

func writeJSON(w http.ResponseWriter, status int, payload interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(payload)
}

func buildDSN() string {
	if url := os.Getenv("DATABASE_URL"); url != "" {
		return url
	}
	host := envOr("POSTGRES_HOST", "database")
	port := envOr("POSTGRES_PORT", "5432")
	user := envOr("POSTGRES_USER", "app")
	pass := envOr("POSTGRES_PASSWORD", "appsecret")
	name := envOr("POSTGRES_DB", "appdb")

	return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, pass, host, port, name)
}

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func canonicalInstrumentName(name string) (string, bool) {
	if canon, ok := marketAliases[name]; ok {
		name = canon
	}
	for _, target := range marketOrder {
		if target == name {
			return name, true
		}
	}
	return "", false
}

func fetchLatestQuotes(ctx context.Context, conn *pgx.Conn, requested []string) (map[string]float64, error) {
	lookup := append([]string{}, requested...)
	for alias := range marketAliases {
		lookup = append(lookup, alias)
	}

	rows, err := conn.Query(ctx, `
		SELECT instrument, bid
		FROM market_quotes
		WHERE instrument = ANY($1)
		ORDER BY quoted_at DESC
	`, lookup)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	result := map[string]float64{}
	for rows.Next() {
		var instrument string
		var bid float64
		if err := rows.Scan(&instrument, &bid); err != nil {
			return nil, err
		}
		canon, ok := canonicalInstrumentName(instrument)
		if !ok {
			continue
		}
		if _, exists := result[canon]; !exists {
			result[canon] = bid
		}
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}
	return result, nil
}

func withConn(r *http.Request, fn func(conn *pgx.Conn) error) error {
	ctx := r.Context()
	conn, err := pgx.Connect(ctx, buildDSN())
	if err != nil {
		return err
	}
	defer conn.Close(ctx)
	return fn(conn)
}

func serveFile(path string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, filepath.Clean(path))
	}
}

func main() {
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))

	http.HandleFunc("/", serveFile("web/template/index.html"))
	http.HandleFunc("/lite", serveFile("web/template/index-lite.html"))
	http.HandleFunc("/blog", serveFile("web/template/blog.html"))
	http.HandleFunc("/mandelbrot", serveFile("web/template/mandelbrot.html"))
	http.HandleFunc("/media/gong", serveFile("web/static/media/gong.mp4"))
	http.HandleFunc("/media/cantina", serveFile("web/static/media/cantina.mp3"))

	http.HandleFunc("/api/artikeltext", func(w http.ResponseWriter, r *http.Request) {
		if err := withConn(r, func(conn *pgx.Conn) error {

			rows, err := conn.Query(r.Context(), `
				SELECT article_id, title, link, summary, image, published_at
				FROM (
					SELECT DISTINCT ON (COALESCE(article_id, link, title))
						article_id,
						title,
						link,
						summary,
						image,
						published_at,
						COALESCE(published_at, created_at) AS sort_ts
					FROM articles
					WHERE image IS NOT NULL
					ORDER BY COALESCE(article_id, link, title), COALESCE(published_at, created_at) DESC
				) deduped
				ORDER BY sort_ts DESC
				LIMIT 20
			`)
			if err != nil {
				return err
			}
			defer rows.Close()

			// 🔑 Header einmal setzen
			w.Header().Set("Content-Type", "application/x-ndjson; charset=utf-8")
			w.Header().Set("Cache-Control", "no-cache")
			w.Header().Set("Connection", "keep-alive")
			// hilft bei manchen Proxies:
			w.Header().Set("X-Accel-Buffering", "no")
			w.WriteHeader(http.StatusOK)

			// Kickstart: sofort 1 Byte senden, damit Streaming „anfängt“

			enc := json.NewEncoder(w)

			flusher, ok := w.(http.Flusher)

			if !ok {
				return fmt.Errorf("streaming not supported")
			}

			w.Write([]byte("\n"))
			flusher.Flush()

			seen := make(map[string]struct{})
			rowCount := 0

			for rows.Next() {
				rowCount++
				var articleID *string
				var title, link, summary string
				var image *string
				var published *time.Time

				if err := rows.Scan(&articleID, &title, &link, &summary, &image, &published); err != nil {
					return err
				}

				id := ""
				if articleID != nil {
					id = strings.TrimSpace(*articleID)
				}
				if id == "" {
					if link != "" {
						id = link
					} else if title != "" {
						id = title
					} else {
						id = fmt.Sprintf("row:%d", rowCount)
					}
				}
				if _, exists := seen[id]; exists {
					continue
				}
				seen[id] = struct{}{}

				item := map[string]interface{}{
					"id":          id,
					"title":       title,
					"link":        link,
					"text":        summary,
					"image":       image,
					"publishedAt": published,
				}

				// 👇 EIN Artikel = EIN JSON
				if err := enc.Encode(item); err != nil {
					return err
				}

				flusher.Flush() // 🚀 sofort senden
			}

			return rows.Err()

		}); err != nil {
			log.Printf("artikeltext query failed: %v", err)
			http.Error(w, "stream error", http.StatusInternalServerError)
		}
	})

	http.HandleFunc("/api/images", func(w http.ResponseWriter, r *http.Request) {
		if err := withConn(r, func(conn *pgx.Conn) error {
			rows, err := conn.Query(r.Context(), `
				SELECT title, image, published_at
				FROM articles
				ORDER BY COALESCE(published_at, created_at) DESC
				LIMIT 10000
			`)
			if err != nil {
				return err
			}
			defer rows.Close()

			w.Header().Set("Content-Type", "application/x-ndjson")
			w.WriteHeader(http.StatusOK)

			enc := json.NewEncoder(w)

			flusher, ok := w.(http.Flusher)
			if !ok {
				return fmt.Errorf("streaming not supported")
			}

			for rows.Next() {
				var title string
				var image *string
				var published *time.Time
				if err := rows.Scan(&title, &image, &published); err != nil {
					return err
				}
				item := map[string]interface{}{
					"title":       title,
					"image":       image,
					"publishedAt": published,
				}

				if err := enc.Encode(item); err != nil {
					return err
				}

				flusher.Flush()
			}

			if rows.Err() != nil {
				return rows.Err()
			}
			return rows.Err()

		}); err != nil {
			// Fallback: keine News statt Fehler, damit die Seite weiterlädt.
			log.Printf("Image query failed, returning empty list: %v", err)
			writeJSON(w, http.StatusOK, []map[string]interface{}{})
		}
	})

	http.HandleFunc("/api/rmv", func(w http.ResponseWriter, r *http.Request) {
		if err := withConn(r, func(conn *pgx.Conn) error {
			rows, err := conn.Query(r.Context(), `
				SELECT trip_index, leg_index, linie, abfahrt, ankunft, von, nach
				FROM rmv_data
				ORDER BY trip_index, leg_index
			`)
			if err != nil {
				return err
			}
			defer rows.Close()

			type leg struct {
				Linie   string `json:"linie"`
				Abfahrt string `json:"abfahrt"`
				Ankunft string `json:"ankunft"`
				Von     string `json:"von"`
				Nach    string `json:"nach"`
			}
			byTrip := map[int][]leg{}
			var maxTrip int
			for rows.Next() {
				var tripIdx, legIdx int
				var l leg
				if err := rows.Scan(&tripIdx, &legIdx, &l.Linie, &l.Abfahrt, &l.Ankunft, &l.Von, &l.Nach); err != nil {
					return err
				}
				byTrip[tripIdx] = append(byTrip[tripIdx], l)
				if tripIdx > maxTrip {
					maxTrip = tripIdx
				}
			}
			if rows.Err() != nil {
				return rows.Err()
			}

			var trips [][]leg
			for i := 0; i <= maxTrip; i++ {
				if legs, ok := byTrip[i]; ok {
					trips = append(trips, legs)
				}
			}

			writeJSON(w, http.StatusOK, map[string]interface{}{"abfahrten": trips})
			return nil
		}); err != nil {
			log.Printf("rmv query failed: %v", err)
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db error"})
		}
	})

	http.HandleFunc("/api/market", func(w http.ResponseWriter, r *http.Request) {
		if err := withConn(r, func(conn *pgx.Conn) error {
			result, err := fetchLatestQuotes(r.Context(), conn, marketOrder)
			if err != nil {
				return err
			}

			type quote struct {
				Instrument string  `json:"instrument"`
				Bid        float64 `json:"bid"`
			}
			var ordered []quote
			for _, inst := range marketOrder {
				if bid, ok := result[inst]; ok {
					ordered = append(ordered, quote{Instrument: inst, Bid: bid})
				}
			}
			writeJSON(w, http.StatusOK, ordered)
			return nil
		}); err != nil {
			log.Printf("market query failed: %v", err)
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "db error"})
		}
	})

	http.HandleFunc("/api/defcon", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			writeJSON(w, http.StatusOK, map[string]int{"level": defcon.get()})
		case http.MethodPost:
			var body struct {
				Level int `json:"level"`
			}
			if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
				writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
				return
			}
			if body.Level < 1 || body.Level > 5 {
				writeJSON(w, http.StatusBadRequest, map[string]string{"error": "level must be 1-5"})
				return
			}
			defcon.set(body.Level)
			writeJSON(w, http.StatusOK, map[string]int{"level": body.Level})
		default:
			w.WriteHeader(http.StatusMethodNotAllowed)
		}
	})

	http.HandleFunc("/api/bunker-status", func(w http.ResponseWriter, r *http.Request) {
		ip := clientIP(r)
		var ipString string
		if ip != nil {
			ipString = ip.String()
		}
		inMainz := ip != nil && uniMainzNet.Contains(ip)
		writeJSON(w, http.StatusOK, map[string]interface{}{
			"online":    inMainz,
			"ts":        time.Now().UTC(),
			"client_ip": ipString,
			"network":   uniMainzNet.String(),
		})
	})

	http.HandleFunc("/api/countdowns", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, filepath.Clean("web/static/media/countdowns.json"))
	})

	log.Println("Server läuft auf http://0.0.0.0:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
