package main

import (
	"authentication/db"
	"authentication/docs"
	"authentication/helper"
	"authentication/models"
	"authentication/redisclient"
	"database/sql"
	"fmt"
	"log"
	"net"

	"authentication/routes"
	"net/http"
	"os"
	"time"

	"github.com/getsentry/sentry-go"
	"github.com/gorilla/mux"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/rs/cors"
)

// @swagger: "2.0"
// @title UESS Authentication Microservice
// @version 1.0

// @description This is the API for Authentication Microservice for UESS. It doesn't support OAS 3.0 and is only for documentation purposes. The library used doesn't support @server annotation.
// @contact.name Darrel Israel
// @contact.email d.israel.psa@gmail.com

// @BasePath /

// @securityDefinitions.apikey BearerToken
// @in header
// @name Authorization

var (
	dbOpenConnections = prometheus.NewGauge(prometheus.GaugeOpts{
		Name: "db_open_connections",
		Help: "Number of open database connections",
	})
	dbInUseConnections = prometheus.NewGauge(prometheus.GaugeOpts{
		Name: "db_in_use_connections",
		Help: "Number of in-use database connections",
	})
	dbIdleConnections = prometheus.NewGauge(prometheus.GaugeOpts{
		Name: "db_idle_connections",
		Help: "Number of idle database connections",
	})
)

var (
	httpRequestsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "http_requests_total",
			Help: "Total number of HTTP requests",
		},
		[]string{"path", "method"},
	)
	httpRequestDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "http_request_duration_seconds",
			Help:    "Duration of HTTP requests in seconds",
			Buckets: prometheus.DefBuckets,
		},
		[]string{"path", "method"},
	)
	httpRequestSize = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "http_request_size_bytes",
			Help:    "Size of HTTP requests in bytes",
			Buckets: prometheus.ExponentialBuckets(100, 10, 8),
		},
		[]string{"path", "method"},
	)
	httpResponseSize = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "http_response_size_bytes",
			Help:    "Size of HTTP responses in bytes",
			Buckets: prometheus.ExponentialBuckets(100, 10, 8),
		},
		[]string{"path", "method"},
	)
)

func init() {
	prometheus.MustRegister(httpRequestsTotal)
	prometheus.MustRegister(httpRequestDuration)
	prometheus.MustRegister(httpRequestSize)
	prometheus.MustRegister(httpResponseSize)
	prometheus.MustRegister(dbOpenConnections)
	prometheus.MustRegister(dbInUseConnections)
	prometheus.MustRegister(dbIdleConnections)
}

func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		if r.URL.Path != metricsPath {
			helper.LogInfo(fmt.Sprintf("INFO: Started %s %s", r.Method, r.URL.Path))
		}

		httpRequestsTotal.WithLabelValues(r.URL.Path, r.Method).Inc()

		requestSize := float64(r.ContentLength)
		if requestSize < 0 {
			requestSize = 0
		}
		httpRequestSize.WithLabelValues(r.URL.Path, r.Method).Observe(requestSize)

		rw := &models.ResponseWriter{ResponseWriter: w}
		next.ServeHTTP(rw, r)

		duration := time.Since(start).Seconds()
		httpRequestDuration.WithLabelValues(r.URL.Path, r.Method).Observe(duration)
		httpResponseSize.WithLabelValues(r.URL.Path, r.Method).Observe(float64(rw.Size))

		// Log completion for non-metrics endpoints
		if r.URL.Path != metricsPath {
			helper.LogInfo(fmt.Sprintf("INFO: Completed %s %s in %.3f seconds", r.Method, r.URL.Path, duration))
		}
	})
}

func collectDBMetrics(database *sql.DB) {
	for {
		stats := database.Stats()
		dbOpenConnections.Set(float64(stats.OpenConnections))
		dbInUseConnections.Set(float64(stats.InUse))
		dbIdleConnections.Set(float64(stats.Idle))
		time.Sleep(10 * time.Second) // Adjust the interval as needed
	}
}

func allowOnlyGrafana(next http.Handler, allowedIP string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
		if err != nil {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}
		if remoteIP == allowedIP {
			next.ServeHTTP(w, r)
			return
		}
		http.Error(w, "Forbidden", http.StatusForbidden)
	})
}

func main() {
	// Initialize Sentry
	goEnv := os.Getenv("GO_ENV")
	if goEnv == "" {
		log.Fatal("GO_ENV is not set in main. Please set the GO_ENV environment variable.")
	}

	DSN := os.Getenv("DSN")
	if DSN == "" {
		log.Fatal("Sentry DSN is not set. Please set the DSN environment variable.")
	}

	err := sentry.Init(sentry.ClientOptions{
		Dsn:              os.Getenv("DSN"),
		TracesSampleRate: 1.0,
		Environment:      goEnv,
	})
	if err != nil {
		log.Fatalf("sentry.Init: %s", err)
	}
	defer sentry.Flush(2 * time.Second)

	docs.SwaggerInfo.Host = "localhost:8080"
	docs.SwaggerInfo.Schemes = []string{"http"}

	helper.LogInfo("INFO: Initializing database connection...")
	var database *sql.DB
	for {
		database, err = db.InitDB()
		if err == nil {
			break
		}
		helper.LogError(fmt.Errorf("ERROR: error initializing database: %v", err), "database initialization error")
		time.Sleep(2 * time.Second)
	}

	go collectDBMetrics(database)
	router := mux.NewRouter()
	routes.SetupRoutes(router, database)
	helper.LogInfo("INFO: Database initialized successfully.")

	allowedIP := os.Getenv("ALLOWED_IP")
	helper.LogInfo("INFO: Setting up routes...")
	router.Handle(metricsPath, allowOnlyGrafana(promhttp.Handler(), allowedIP))
	router.Use(loggingMiddleware)

	c := cors.New(cors.Options{
		AllowedOrigins:   []string{"http://localhost:4173", "http://localhost:5173"}, // Your frontend URL
		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
		AllowedHeaders:   []string{"*"}, // Allow all headers temporarily
		AllowCredentials: true,          // Critical for withCredentials requests
		MaxAge:           86400,         // Cache preflight results
	})

	handler := c.Handler(router)

	redisclient.Init()

	helper.LogInfo("INFO: Connected to Redis successfully!")

	helper.LogInfo("WARNING: Ensure Redis is secured to prevent unauthorized access. Use a strong password and bind Redis to localhost or a secure network.")

	helper.LogInfo("INFO: Authentication Microservice is running on http://localhost:8080")
	server := &http.Server{
		Addr:         ":8080",
		Handler:      handler,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 300 * time.Second,
		IdleTimeout:  60 * time.Second,
	}
	log.Fatal(server.ListenAndServe())
}

