package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strings"
    "sync/atomic"
    "time"

    "github.com/joho/godotenv"
)

type ctxKey string

const ctxReqID ctxKey = "reqID"

// atomic counter for simple request IDs
var reqIDCounter uint64

// log levels
const (
    levelDebug = iota
    levelInfo
    levelWarn
    levelError
)

var currentLogLevel = levelInfo

func parseLogLevel(s string) int {
    switch strings.ToLower(s) {
    case "debug":
        return levelDebug
    case "info", "":
        return levelInfo
    case "warn", "warning":
        return levelWarn
    case "error", "err":
        return levelError
    default:
        return levelInfo
    }
}

func logf(level int, format string, args ...any) {
    if level < currentLogLevel {
        return
    }
    prefix := "INFO"
    switch level {
    case levelDebug:
        prefix = "DEBUG"
    case levelInfo:
        prefix = "INFO"
    case levelWarn:
        prefix = "WARN"
    case levelError:
        prefix = "ERROR"
    }
    // Prepend the prefix as the first argument
    log.Printf("[%s] "+format, append([]any{prefix}, args...)...)
}

func main() {
    _ = godotenv.Load(".env") // Load .env file

    valhallaURL := os.Getenv("VALHALLA_URL")
    if valhallaURL == "" {
        valhallaURL = "http://valhalla:8002/route"
    }

    nominatimBase := os.Getenv("NOMINATIM_URL")
    if nominatimBase == "" {
        nominatimBase = "https://nominatim.openstreetmap.org"
    }
    nominatimUA := os.Getenv("NOMINATIM_USER_AGENT")
    if nominatimUA == "" {
        nominatimUA = "FreeMoto/1.0 (+https://fm.ztsw.de/)"
    }

    currentLogLevel = parseLogLevel(os.Getenv("LOG_LEVEL"))

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

    http.Handle("/static/", withLogging(http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))))
    http.Handle("/healthz", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("ok"))
    })))
    http.Handle("/route", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        proxyToValhalla(w, r, valhallaURL)
    })))
    // Nominatim proxy endpoints
    http.Handle("/geocode", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        proxyToNominatimGET(w, r, nominatimBase+"/search", nominatimUA)
    })))
    http.Handle("/reverse", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        proxyToNominatimGET(w, r, nominatimBase+"/reverse", nominatimUA)
    })))
    http.Handle("/", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/" {
            http.ServeFile(w, r, "./static/index.html")
            return
        }
        http.ServeFile(w, r, "./static/"+r.URL.Path[1:])
    })))
    logf(levelInfo, "Listening on :%s", port)
    http.ListenAndServe(":"+port, nil)
}

func proxyToValhalla(w http.ResponseWriter, r *http.Request, valhallaURL string) {
    // Create outbound request with caller context and preserve headers
    req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, valhallaURL, r.Body)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    req.Header = r.Header.Clone()

    client := http.Client{Timeout: 15 * time.Second}
    start := time.Now()
    resp, err := client.Do(req)
    if err != nil {
        if rid, ok := r.Context().Value(ctxReqID).(string); ok {
            logf(levelError, "rid=%s upstream=valhalla error=%v", rid, err)
        } else {
            logf(levelError, "upstream=valhalla error=%v", err)
        }
        http.Error(w, err.Error(), http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()
    // Propagate relevant headers and status code
    for key, values := range resp.Header {
        // Optionally filter headers; here we forward common ones
        if key == "Content-Type" || key == "Content-Encoding" || key == "Cache-Control" || key == "Vary" {
            for _, v := range values {
                w.Header().Add(key, v)
            }
        }
    }
    w.WriteHeader(resp.StatusCode)
    _, _ = io.Copy(w, resp.Body)

    if rid, ok := r.Context().Value(ctxReqID).(string); ok {
        logf(levelDebug, "rid=%s upstream=valhalla status=%d dur=%s", rid, resp.StatusCode, time.Since(start))
    } else {
        logf(levelDebug, "upstream=valhalla status=%d dur=%s", resp.StatusCode, time.Since(start))
    }
}

// proxyToNominatimGET proxies GET requests to Nominatim endpoints (search/reverse),
// sets a descriptive User-Agent, and enforces a timeout.
func proxyToNominatimGET(w http.ResponseWriter, r *http.Request, target string, userAgent string) {
    if r.Method != http.MethodGet {
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        return
    }
    // Preserve original query parameters
    url := target
    if rq := r.URL.RawQuery; rq != "" {
        url = url + "?" + rq
    }

    req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, url, nil)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // Set a proper User-Agent per Nominatim usage policy
    req.Header.Set("User-Agent", userAgent)
    req.Header.Set("Accept", "application/json")

    client := http.Client{Timeout: 10 * time.Second}
    start := time.Now()
    resp, err := client.Do(req)
    if err != nil {
        if rid, ok := r.Context().Value(ctxReqID).(string); ok {
            logf(levelError, "rid=%s upstream=nominatim url=%s error=%v", rid, url, err)
        } else {
            logf(levelError, "upstream=nominatim url=%s error=%v", url, err)
        }
        http.Error(w, err.Error(), http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()

    // Forward selected headers and status code
    if ct := resp.Header.Get("Content-Type"); ct != "" {
        w.Header().Set("Content-Type", ct)
    }
    w.WriteHeader(resp.StatusCode)
    _, _ = io.Copy(w, resp.Body)

    if rid, ok := r.Context().Value(ctxReqID).(string); ok {
        logf(levelDebug, "rid=%s upstream=nominatim status=%d dur=%s", rid, resp.StatusCode, time.Since(start))
    } else {
        logf(levelDebug, "upstream=nominatim status=%d dur=%s", resp.StatusCode, time.Since(start))
    }
}

// withLogging wraps handlers to add request ID, status, duration logging
func withLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        rid := fmt.Sprintf("%x", atomic.AddUint64(&reqIDCounter, 1))
        ctx := context.WithValue(r.Context(), ctxReqID, rid)
        r = r.WithContext(ctx)

        lrw := &loggingResponseWriter{ResponseWriter: w, status: 200}
        start := time.Now()
        next.ServeHTTP(lrw, r)
        dur := time.Since(start)

        logf(levelInfo, "rid=%s method=%s path=%s status=%d bytes=%d dur=%s ua=\"%s\" ip=%s",
            rid, r.Method, r.URL.Path, lrw.status, lrw.bytes, dur, r.UserAgent(), clientIP(r))
    })
}

type loggingResponseWriter struct {
    http.ResponseWriter
    status int
    bytes  int
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
    lrw.status = code
    lrw.ResponseWriter.WriteHeader(code)
}

func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
    n, err := lrw.ResponseWriter.Write(b)
    lrw.bytes += n
    return n, err
}

func clientIP(r *http.Request) string {
    // honor X-Forwarded-For if present (first IP)
    if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
        return xff
    }
    return r.RemoteAddr
}