package main

import (
	_ "crypto/md5"
	_ "gateway-softweng/docs"
	paho "github.com/eclipse/paho.mqtt.golang"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	influxdb2 "github.com/influxdata/influxdb-client-go/v2"
	"github.com/influxdata/influxdb-client-go/v2/api"
	"github.com/labstack/gommon/log"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"net/http"
	"os"
)

type Gateway struct {
	username  string
	password  string
	mqtt      paho.Client
	rest      *gin.Engine
	restGroup *gin.RouterGroup
	accounts  gin.Accounts
	influx    influxdb2.Client
	influxApi api.WriteAPIBlocking
}

// @Summary Ping test endpoint
// @Description get ping response
// @Tags ping
// @Accept json
// @Produce json
// @Success 200 {string} string "pong"
// @Router /ping [get]
func pingHandler(c *gin.Context) {
	c.String(http.StatusOK, "pong")
}

// getUsers initializes user account.
//
// It performs the following actions:
//   - Creates user accounts with usernames and password provided by environnement variable
//   - Stores the credentials for basic authentication.
func (gh *Gateway) getUser() {
	// Create username
	ok := false
	gh.username, ok = os.LookupEnv("REST_USERNAME")
	if !ok {
		log.Fatal("REST_USERNAME not set")
	}

	gh.password, ok = os.LookupEnv("REST_PASSWORD")
	if !ok {
		log.Fatal("REST_PASSWORD not set")
	}

	gh.accounts = gin.Accounts{}
	gh.accounts[gh.username] = gh.password

}

// createRestGateway sets up the REST API using the Gin framework.
// It defines the following routes:
//   - A public ping test endpoint for health checks.
//   - A Swagger documentation endpoint for API documentation.
//   - A secured group of endpoints for commands and data requests, protected by basic authentication.
//
// Behavior:
//   - The `/ping` endpoint responds with "pong" to test server availability.
//   - The `/swagger/*any` endpoint serves the Swagger UI for API documentation.
//   - The `/raclette` group includes:
//   - A POST route for publishing commands to MQTT.
//   - A GET route for requesting data from InfluxDB.
//
// Security:
//   - The `/raclette` group requires basic authentication using credentials generated for each user.
func (gh *Gateway) createRestGateway() {
	// Create a new Gin router
	gh.rest = gin.Default()

	// Configure CORS middleware to allow all origins
	config := cors.DefaultConfig()
	config.AllowAllOrigins = true
	config.AllowMethods = []string{"GET", "POST"}
	config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"}
	gh.rest.Use(cors.New(config))

	gh.rest.GET("/ping", pingHandler)

	// Swagger documentation route
	gh.rest.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

	// Create a new router group with basic authentication for /raclette
	gh.restGroup = gh.rest.Group("/raclette", gin.BasicAuth(gh.accounts))

	// Define the route for the publish command
	gh.restGroup.POST("", func(c *gin.Context) {
		ret := gh.publishCommand(c)
		if ret == nil {
			c.JSON(http.StatusOK, gin.H{"status": "ok"})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"error": ret.Error()})
		}
	})

	// Define the route for the publish command (GET)
	gh.restGroup.GET("", func(c *gin.Context) {
		ret := gh.requestInflux(c)
		if ret != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": ret.Error()})
		}
	})
}

// createMQTTGateway initializes the MQTT client and sets up subscriptions.
//
// It subscribes to the following topics:
//   - "+/+/+/update": For device state updates.
//
// The OnConnect callback ensures that subscriptions are re-established after reconnecting.
//
// Returns:
//   - error: An error if the MQTT client fails to connect or subscribe to topics.
func (gh *Gateway) createMQTTGateway() error {

	// Get env variables and set default values if not set
	MQTT_URL, ok := os.LookupEnv("MQTT_URL")
	if !ok {
		log.Error("MQTT_URL not set, using default value: mqtt://mqtt.mse.kb28.ch:1883")
		MQTT_URL = "mqtt://mqtt.mse.kb28.ch:1883"
	}

	CLIENT_ID, ok := os.LookupEnv("CLIENT_ID")
	if !ok {
		log.Error("CLIENT_ID not set, using default value: Gateway-SoftwEng")
		CLIENT_ID = "Gateway-SoftwEng"
	}

	// Get env variables for MQTT credentials, panic if not set
	MQTT_USERNAME, ok := os.LookupEnv("MQTT_USERNAME")
	if !ok {
		log.Fatal("MQTT_USERNAME not set")
	}
	MQTT_PASSWORD, ok := os.LookupEnv("MQTT_PASSWORD")
	if !ok {
		log.Fatal("MQTT_PASSWORD not set")
	}

	// Create MQTT client
	option := paho.NewClientOptions()
	option.AddBroker(MQTT_URL)
	option.SetClientID(CLIENT_ID)
	option.SetUsername(MQTT_USERNAME)
	option.SetPassword(MQTT_PASSWORD)
	option.SetAutoReconnect(true)
	option.OnConnect = func(client paho.Client) {

		// Subscribe to update
		token := gh.mqtt.Subscribe("+/+/+/update", 0, gh.processUpdate)
		if token.Wait() && token.Error() != nil {
			log.Error(token.Error())
			client.Disconnect(1000) // Disconnect from the broker after 1000ms
		}

	}
	gh.mqtt = paho.NewClient(option)
	token := gh.mqtt.Connect()
	if token.Wait() && token.Error() != nil {
		return token.Error()
	}
	return nil
}

// createInfluxGateway initializes the InfluxDB client.
func (gh *Gateway) createInfluxGateway() {

	// Get env variables and set default values if not set
	INFLUXDB_ORG, ok := os.LookupEnv("INFLUXDB_ORG")
	if !ok {
		log.Error("INFLUXDB_ORG not set, using default value: raclette")
		INFLUXDB_ORG = "raclette"
	}

	INFLUXDB_BUCKET, ok := os.LookupEnv("INFLUXDB_BUCKET")
	if !ok {
		log.Error("INFLUXDB_BUCKET not set, using default value: raclette")
		INFLUXDB_BUCKET = "raclette"
	}

	// Get env variables and set default values if not set
	INFLUXDB_URL, ok := os.LookupEnv("INFLUXDB_URL")
	if !ok {
		log.Error("INFLUXDB_URL not set, using default value: http://influx.mse.kb28.ch")
		INFLUXDB_URL = "http://influx.mse.kb28.ch"
	}

	// Get env variables for InfluxDB credentials, panic if not set
	INFLUXDB_TOKEN, ok := os.LookupEnv("INFLUXDB_TOKEN")
	if !ok {
		log.Fatal("INFLUXDB_TOKEN not set")
	}

	// Initialize the InfluxDB client
	gh.influx = influxdb2.NewClient(INFLUXDB_URL, INFLUXDB_TOKEN)
	gh.influxApi = gh.influx.WriteAPIBlocking(INFLUXDB_ORG, INFLUXDB_BUCKET)
	gh.influxApi.EnableBatching()
}

// NewGateway initializes the Gateway instance by setting up the necessary components.
//
// It performs the following actions:
//   - Initializes the InfluxDB client using the provided token.
//   - Creates user account with unique credentials and InfluxDB write APIs.
//   - Sets up the REST API server with routes for health checks, Swagger documentation, and secured endpoints.
//   - Connects to the MQTT broker and subscribes to relevant topics.
//
// Returns:
//   - *Gateway: A pointer to the initialized Gateway instance.
//   - error: An error if any of the initialization steps fail.
func NewGateway() (*Gateway, error) {
	var gh = Gateway{}
	var err error

	// Initialize the InfluxDB client
	gh.createInfluxGateway()

	// Generate user accounts and credentials
	gh.getUser()

	// Set up the REST API server
	gh.createRestGateway()

	// Initialize the MQTT client and subscriptions
	err = gh.createMQTTGateway()
	if err != nil {
		return nil, err
	}

	// Start the HTTP server
	err = gh.rest.Run(":8080")
	if err != nil {
		return nil, err
	}

	return &gh, nil
}
