package api

import (
	"context"
	"log"
	"net/http"
	"strings"
	"sync"
	"time"

	db "github.com/cynfo/rmm-backend/db"
)

type contextKey string

const UserContextKey contextKey = "user"

// APIKeyContextKey fuer Key-Infos im Request-Context
const APIKeyContextKey contextKey = "apikey"

// APIKeyCache cached DB-basierte API-Keys mit periodischem Refresh
type APIKeyCache struct {
	db       *db.Database
	mu       sync.RWMutex
	keys     map[string]*db.APIKey // key -> APIKey mit Permissions + CustomerID
	lastLoad time.Time
	ttl      time.Duration
}

func NewAPIKeyCache(database *db.Database) *APIKeyCache {
	c := &APIKeyCache{
		db:   database,
		keys: make(map[string]*db.APIKey),
		ttl:  30 * time.Second,
	}
	c.refresh()
	return c
}

func (c *APIKeyCache) refresh() {
	dbKeys, err := c.db.ListAPIKeys()
	if err != nil {
		log.Printf("API-Key Cache Refresh fehlgeschlagen: %v", err)
		return
	}
	newKeys := make(map[string]*db.APIKey, len(dbKeys))
	for i := range dbKeys {
		k := dbKeys[i]
		newKeys[k.Key] = &k
	}
	c.mu.Lock()
	c.keys = newKeys
	c.lastLoad = time.Now()
	c.mu.Unlock()
}

func (c *APIKeyCache) Get(key string) *db.APIKey {
	c.mu.RLock()
	expired := time.Since(c.lastLoad) > c.ttl
	info := c.keys[key]
	c.mu.RUnlock()

	if expired {
		c.refresh()
		c.mu.RLock()
		info = c.keys[key]
		c.mu.RUnlock()
	}

	if info != nil {
		go c.db.UpdateAPIKeyLastUsed(key)
	}
	return info
}

// IsValid prueft nur ob Key existiert (Abwaertskompatibilitaet)
func (c *APIKeyCache) IsValid(key string) bool {
	return c.Get(key) != nil
}

// CombinedAuthWithCache - API-Key (aus DB-Cache) ODER JWT Token
// Setzt APIKey-Info im Context fuer nachgelagerte Permission-Checks.
func CombinedAuthWithCache(cache *APIKeyCache, auth *AuthHandler, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Try API key first (Header oder Query-Parameter)
		key := r.Header.Get("X-API-Key")
		if key == "" {
			key = r.URL.Query().Get("api_key")
		}
		if key != "" {
			if info := cache.Get(key); info != nil {
				// Permission-Check
				if !hasAPIKeyPermission(info, r) {
					http.Error(w, `{"error":"forbidden: unzureichende Berechtigungen"}`, http.StatusForbidden)
					return
				}
				ctx := context.WithValue(r.Context(), APIKeyContextKey, info)
				next.ServeHTTP(w, r.WithContext(ctx))
				return
			}
		}

		// Try JWT — Authorization Header oder ?token= Query-Parameter (fuer WebSocket)
		var jwtToken string
		authHeader := r.Header.Get("Authorization")
		if strings.HasPrefix(authHeader, "Bearer ") {
			jwtToken = strings.TrimPrefix(authHeader, "Bearer ")
		} else if t := r.URL.Query().Get("token"); t != "" {
			jwtToken = t
		}
		if jwtToken != "" {
			claims, err := auth.ValidateToken(jwtToken)
			if err == nil {
				ctx := context.WithValue(r.Context(), UserContextKey, claims)
				next.ServeHTTP(w, r.WithContext(ctx))
				return
			}
		}

		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
	})
}

// hasAPIKeyPermission prueft ob ein API-Key fuer den aktuellen Request berechtigt ist
func hasAPIKeyPermission(key *db.APIKey, r *http.Request) bool {
	perm := key.Permissions
	method := r.Method
	path := r.URL.Path

	switch perm {
	case "agent":
		// Agent-Endpunkte: Registrierung, WS-Connect, Heartbeat, OTA-Update
		return path == "/api/v1/agent/ws" ||
			path == "/api/v1/agent/heartbeat" ||
			path == "/api/v1/agent/register" ||
			path == "/api/v1/firmware" ||
			path == "/api/v1/firmware/download" ||
			path == "/api/v1/installer/download" ||
			strings.HasPrefix(path, "/api/v1/agents/")

	case "read":
		// Nur GET-Requests, keine destruktiven Aktionen
		if method != http.MethodGet {
			return false
		}
		// Customer-Scope pruefen (falls gesetzt)
		return checkCustomerScope(key, r)

	case "write":
		// Alles ausser Terminal (Terminal ist JWT-only)
		// Customer-Scope pruefen
		return checkCustomerScope(key, r)

	case "admin":
		// Vollen Zugriff (kein Terminal — das prueft JWTOnlyAuth separat)
		return true

	default:
		return false
	}
}

// checkCustomerScope prueft ob ein customer-scoped Key nur auf seinen Kunden zugreift
func checkCustomerScope(key *db.APIKey, r *http.Request) bool {
	if key.CustomerID == nil {
		return true // Kein Scope = globaler Zugriff
	}
	// Customer-ID aus dem Pfad extrahieren (z.B. /api/v1/agents/{id} -> Agent-Customer pruefen)
	// Fuer jetzt: customer-scoped Keys werden im Handler weiter eingeschraenkt (via GetAPIKeyFromContext)
	// Der eigentliche Scope-Check passiert in den Handlern die GetAPIKeyFromContext nutzen
	return true
}

// GetAPIKeyFromContext gibt den API-Key aus dem Context zurueck (nil wenn JWT-Auth)
func GetAPIKeyFromContext(r *http.Request) *db.APIKey {
	if v := r.Context().Value(APIKeyContextKey); v != nil {
		if k, ok := v.(*db.APIKey); ok {
			return k
		}
	}
	return nil
}

// CombinedAuth - Abwaertskompatibel mit statischen Keys (Fallback)
func CombinedAuth(validKeys []string, auth *AuthHandler, next http.Handler) http.Handler {
	keySet := make(map[string]bool, len(validKeys))
	for _, k := range validKeys {
		keySet[k] = true
	}

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		key := r.Header.Get("X-API-Key")
		if key == "" {
			key = r.URL.Query().Get("api_key")
		}
		if key != "" && keySet[key] {
			next.ServeHTTP(w, r)
			return
		}

		authHeader := r.Header.Get("Authorization")
		if strings.HasPrefix(authHeader, "Bearer ") {
			token := strings.TrimPrefix(authHeader, "Bearer ")
			claims, err := auth.ValidateToken(token)
			if err == nil {
				ctx := context.WithValue(r.Context(), UserContextKey, claims)
				next.ServeHTTP(w, r.WithContext(ctx))
				return
			}
		}

		http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
	})
}

// JWTOnlyAuth - Nur JWT akzeptieren, kein API-Key (z.B. fuer Terminal-Zugriff)
// Erfordert echten Login mit User/Pass (+2FA), kein Maschinentoken reicht.
func JWTOnlyAuth(auth *AuthHandler, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var jwtToken string

		// Authorization Header
		authHeader := r.Header.Get("Authorization")
		if strings.HasPrefix(authHeader, "Bearer ") {
			jwtToken = strings.TrimPrefix(authHeader, "Bearer ")
		}
		// ?token= Query-Parameter (fuer WebSocket, Browser kann keinen Header setzen)
		if jwtToken == "" {
			jwtToken = r.URL.Query().Get("token")
		}

		if jwtToken == "" {
			http.Error(w, `{"error":"unauthorized: JWT erforderlich"}`, http.StatusUnauthorized)
			return
		}

		claims, err := auth.ValidateToken(jwtToken)
		if err != nil {
			http.Error(w, `{"error":"unauthorized: ungültiger Token"}`, http.StatusUnauthorized)
			return
		}

		ctx := context.WithValue(r.Context(), UserContextKey, claims)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// CORS Middleware
func CORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
		if r.Method == "OPTIONS" {
			w.WriteHeader(http.StatusOK)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// Logging - Request-Logging Middleware
func Logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
	})
}
