// Copyright AGNTCY Contributors (https://github.com/agntcy)
// SPDX-License-Identifier: Apache-2.0

package http

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"github.com/agntcy/telemetry-hub/api-layer/docs"
	"github.com/agntcy/telemetry-hub/api-layer/pkg/common"
	"github.com/agntcy/telemetry-hub/api-layer/pkg/services/clickhouse/models"
	services "github.com/agntcy/telemetry-hub/api-layer/pkg/services/interfaces"
	"github.com/rs/cors"
	httpSwagger "github.com/swaggo/http-swagger"

	// docs is generated by Swag CLI, you have to import it.

	_ "github.com/agntcy/telemetry-hub/api-layer/docs"
	"github.com/agntcy/telemetry-hub/api-layer/pkg/logger"
	"github.com/gorilla/mux"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

type HttpServer struct {
	Port            int
	DataService     services.DataService
	SignalsChannel  chan os.Signal
	BaseUrl         string
	AllowOrigins    string
	httpServer      *http.Server
	keepAliveMetric prometheus.Counter
}

type SimpleMessage struct {
	Message string `json:"message"`
}

type SessionID models.SessionUniqueID
type Trace models.OtelTraces
type CreateMetric models.MetricCreateRequest
type Metric models.MetricResponse

// EchoResponse represents a response for the echo endpoint.
type EchoResponse map[string]interface{}

// @title API-Layer API
// @version 1.0
// @description This serivce is a part of the API-Layer project. It helps to understand if the service is alive and running.
// @BasePath /

// @Summary      Get sessions
// @Description  Get sessions by start and end time
// @Tags         APIs
// @Accept       json
// @Produce      json
// @Param        start_time query string true "Start time in ISO 8601 UTC format (e.g. 2023-06-25T15:04:05Z)" example("2023-06-25T15:04:05Z")
// @Param        end_time query string true "End time in ISO 8601 UTC format (e.g. 2023-06-25T15:04:05Z)" example("2023-06-25T18:04:05Z")
// @Success		 200 {array} models.SessionsResponse "list of session IDs"
// @Failure      400 {object} string "Bad request"
// @Failure      500 {object} string "Internal server error"
// @Router       /traces/sessions [get]
func (hs *HttpServer) Sessions(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	startTime := r.URL.Query().Get(common.START_TIME)
	startTimeParsed, err := common.ParseTime(startTime)
	if err != nil {
		http.Error(w, fmt.Sprintf("Invalid start_time: %v", err), http.StatusBadRequest)
		return
	}

	endTime := r.URL.Query().Get(common.END_TIME)
	endTimeParsed, err := common.ParseTime(endTime)
	if err != nil {
		http.Error(w, fmt.Sprintf("Invalid end_time: %v", err), http.StatusBadRequest)
		return
	}

	includePrompts := r.URL.Query().Get(common.INCLUDE_PROMPTS)
	var sessionIDs []models.SessionUniqueID
    if includePrompts == "true" {
        sessionIDs, err = hs.DataService.GetSessionIDSWithPrompts(startTimeParsed, endTimeParsed)
    } else {
        sessionIDs, err = hs.DataService.GetSessionIDSUnique(startTimeParsed, endTimeParsed)
    }
	if err != nil {
		http.Error(w, fmt.Sprintf("Error fetching sessions: %v", err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")

	response := models.SessionsResponse{
		Data:  sessionIDs,
		Total: len(sessionIDs),
	}
	if err := json.NewEncoder(w).Encode(response); err != nil {
		http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
		return
	}
}

// @Summary      Get traces by session ID
// @Description  Get traces by session ID
// @Tags         APIs
// @Accept       json
// @Produce      json
// @Param        session_id path string true "Session ID" example("session_abc123")
// @Success      200 {array} Trace "List of traces for the session" example([{"trace_id": "trace_def456", "span_name": "ml_inference", "timestamp": "2023-06-25T15:30:00Z"}, {"trace_id": "trace_ghi789", "span_name": "data_processing", "timestamp": "2023-06-25T15:31:00Z"}])
// @Failure      400 {object} string "Bad request"
// @Failure      500 {object} string "Internal server error"
// @Router       /traces/session/{session_id} [get]
func (hs *HttpServer) Traces(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	vars := mux.Vars(r)
	sessionID := vars[common.SESSION_ID]
	if sessionID == "" {
		http.Error(w, "Session ID is required", http.StatusBadRequest)
		return
	}

	traces, err := hs.DataService.GetTracesBySessionID(sessionID)
	if err != nil {
		http.Error(w, fmt.Sprintf("Error fetching traces for session ID %s: %v", sessionID, err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(traces)

}

// @Summary      Write session metrics
// @Description  Write session metrics to the server
// @Tags         APIs
// @Accept       json
// @Produce      json
// @Param        metric body CreateMetric true "Metric to write" example({"span_id": "span_abc123", "trace_id": "trace_def456", "session_id": "session_ghi789", "metrics": {"accuracy": "0.95", "latency_ms": "120", "error_count": "3"}, "app_name": "ml-service", "app_id": "app-001"})
// @Success      201 {object} Metric "Metric created successfully"
// @Failure      400 {object} string "Bad request"
// @Failure      500 {object} string "Internal server error"
// @Router       /metrics/session [post]
func (hs *HttpServer) WriteMetricsSession(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	hs.saveMetrics(w, r, common.METRIC_SCOPE_SESSION)

}

// @Summary      Write span metrics
// @Description  Write span metrics to the server
// @Tags         APIs
// @Accept       json
// @Produce      json
// @Param        metric body CreateMetric true "Metric to write" example({"span_id": "span_xyz789", "trace_id": "trace_uvw123", "session_id": "session_rst456", "metrics": {"response_time": "200", "cache_hit": "true", "error_type": "timeout"}, "app_name": "api-gateway", "app_id": "app-002"})
// @Success      201 {object} Metric "Metric created successfully"
// @Failure      400 {object} string "Bad request"
// @Failure      500 {object} string "Internal server error"
// @Router       /metrics/span [post]
func (hs *HttpServer) WriteMetricsSpan(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	hs.saveMetrics(w, r, common.METRIC_SCOPE_SPAN)

}

// @Summary      Get metrics by session ID
// @Description  Get metrics by session ID
// @Tags         APIs
// @Accept       json
// @Produce      json
// @Param        session_id path string true "Session ID" example("session_abc123")
// @Success      200 {array} Metric "List of metrics for the session" example([{"id": "metric_001", "span_id": "span_abc123", "trace_id": "trace_def456", "session_id": "session_abc123", "timestamp": "2023-06-25T15:30:00Z", "metrics": {"accuracy": "0.95", "latency_ms": "120"}, "app_name": "ml-service", "app_id": "app-001"}])
// @Failure      400 {object} string "Bad request"
// @Failure      500 {object} string "Internal server error"
// @Router       /metrics/session/{session_id} [get]
func (hs *HttpServer) GetMetricsSession(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	vars := mux.Vars(r)
	sessionID := vars[common.SESSION_ID]
	if sessionID == "" {
		http.Error(w, "Session ID is required", http.StatusBadRequest)
		return
	}

	metrics, err := hs.DataService.GetMetricsBySessionIdAndScope(sessionID, common.METRIC_SCOPE_SESSION)
	if err != nil {
		http.Error(w, fmt.Sprintf("Error fetching metrics for session ID %s: %v", sessionID, err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(metrics)
}

// @Summary      Get metrics by span ID
// @Description  Get metrics by span ID
// @Tags         APIs
// @Accept       json
// @Produce      json
// @Param        span_id path string true "Span ID" example("span")
// @Success      200 {array} Metric "List of metrics for the span" example([{"id": "metric_001", "span_id": "span_abc123", "trace_id": "trace_def456", "session_id": "session_abc123", "timestamp": "2023-06-25T15:30:00Z", "metrics": {"accuracy": "0.95", "latency_ms": "120"}, "app_name": "ml-service", "app_id": "app-001"}])
// @Failure      400 {object} string "Bad request"
// @Failure      500 {object} string "Internal server error"
// @Router       /metrics/span/{span_id} [get]
func (hs *HttpServer) GetMetricsSpan(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	vars := mux.Vars(r)
	spanID := vars[common.SPAN_ID]
	if spanID == "" {
		http.Error(w, "Span ID is required", http.StatusBadRequest)
		return
	}

	metrics, err := hs.DataService.GetMetricsBySpanIdAndScope(spanID, common.METRIC_SCOPE_SPAN)
	if err != nil {
		http.Error(w, fmt.Sprintf("Error fetching metrics for span ID %s: %v", spanID, err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(metrics)
}

func KeepAlive(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	response := SimpleMessage{Message: "I'm alive!"}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func PrometeusMetrics(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	promhttp.Handler().ServeHTTP(w, r)
}

func (hs *HttpServer) saveMetrics(w http.ResponseWriter, r *http.Request, metricScope string) {

	var metricRequest models.MetricCreateRequest
	if err := json.NewDecoder(r.Body).Decode(&metricRequest); err != nil {
		http.Error(w, fmt.Sprintf("Error decoding request body: %v", err), http.StatusBadRequest)
		return
	}

	// Convert request to metric model
	metric := metricRequest.ToMetric()
	metric.Scope = &metricScope

	createdMetric, err := hs.DataService.AddMetric(*metric)
	if err != nil {
		http.Error(w, fmt.Sprintf("Error writing metric: %v", err), http.StatusInternalServerError)
		return
	}

	// Return the created metric with generated ID and timestamp
	response := createdMetric.ToResponse()
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(response)
}

func createNewCounterVec(metricName string, metricHelp string) prometheus.Counter {
	requests := prometheus.NewCounter(prometheus.CounterOpts{
		Name: metricName,
		Help: metricHelp,
	})
	requests.Inc()

	prometheus.MustRegister(requests)
	return requests
}

func (hs *HttpServer) logMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		var bodyBytes []byte
		if r.Body != nil {
			bodyBytes, _ = io.ReadAll(r.Body)
		}

		// Restore the io.ReadCloser to its original state
		r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))

		logger.Zap.Info("Started",
			logger.String("Method", r.Method),
			logger.String("Path", r.URL.Path),
			logger.String("Remote Address", r.RemoteAddr),
		)

		next.ServeHTTP(w, r)

		logger.Zap.Info("Completed",
			logger.String("Method", r.Method),
			logger.String("Path", r.URL.Path),
			logger.String("Remote Address", r.RemoteAddr),
			logger.Duration("Duration [s]", time.Since(start)),
		)

	})
}

func (hs *HttpServer) Run(ctx context.Context, wg *sync.WaitGroup) error {
	defer wg.Done()

	hs.startServer()

	logger.Zap.Info("Server is running on port", logger.Int("port", hs.Port))
	signal.Notify(hs.SignalsChannel, os.Interrupt, syscall.SIGTERM)

LOOP:
	for {
		select {
		case signal := <-hs.SignalsChannel:
			logger.Zap.Info(
				"Received signal",
				logger.String("signal", signal.String()),
			)
			break LOOP
		case <-ctx.Done():
			logger.Zap.Info(
				"Context done, terminating process grpc",
				logger.Error(ctx.Err()))
			// Call shutdown with background context
			return hs.Stop(context.Background())
		}
	}

	// stop monitoring service
	return hs.Stop(ctx)
}

func (hs *HttpServer) Stop(_ context.Context) error {
	logger.Zap.Info("Stopping Http server")
	hs.httpServer.Shutdown(context.Background())
	return nil
}

func (hs *HttpServer) startServer() {
	go func() {
		logger.Zap.Info("Starting HTTP server")

		docs.SwaggerInfo.Host = hs.BaseUrl
		hs.keepAliveMetric = createNewCounterVec("keep_alive_request", "Keep Alive Requeste, it has to be always 1")
		mux := mux.NewRouter()
		mux.Use(hs.logMiddleware)
		mux.HandleFunc("/keepAlive", KeepAlive).Methods(http.MethodGet)

		mux.HandleFunc(
			"/metrics",
			PrometeusMetrics,
		).Methods(http.MethodGet)

		mux.HandleFunc(
			"/traces/sessions",
			hs.Sessions,
		).Methods(http.MethodGet)

		mux.HandleFunc("/metrics/session", hs.WriteMetricsSession).Methods(http.MethodPost)
		mux.HandleFunc("/metrics/span", hs.WriteMetricsSpan).Methods(http.MethodPost)

		mux.HandleFunc("/metrics/session/{session_id}", hs.GetMetricsSession).Methods(http.MethodGet)
		mux.HandleFunc("/metrics/span/{span_id}", hs.GetMetricsSpan).Methods(http.MethodGet)

		mux.HandleFunc("/traces/session/{session_id}", hs.Traces)
		mux.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler)
		logger.Zap.Info("Server is running on port", logger.Int("port", hs.Port))
		c := cors.New(cors.Options{
			AllowedOrigins:   []string{"http://localhost:8080"},
			AllowCredentials: true,
		})
		hs.httpServer = &http.Server{
			Addr:    fmt.Sprintf(":%d", hs.Port),
			Handler: c.Handler(mux),
		}

		if err := hs.httpServer.ListenAndServe(); err != nil {
			log.Fatal(err)
		}
	}()
}
