/*
Basic functionality to receive uplink messages from TTN,
process them and send appropriate responses to devices.

Author: Olek
*/
package main

// go mod vendor -> for static libraries
// go mod tidy -> to download to /go/pkg/mod
// or just use docker
import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	api_user "server/app_comm/api/user"
	serder "server/serder"

	chirp_api "github.com/chirpstack/chirpstack/api/go/v4/api"
	"github.com/joho/godotenv"

	mqtt "github.com/eclipse/paho.mqtt.golang"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
	"golang.org/x/crypto/bcrypt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	MAX_CHAR_LEN     = 64 // Limits fields like: names, passwords...
	MAX_PING_RETRIES = 6
)

// FPort signifies meaning of each port's message
const (
	FNotif uint8 = iota + 1 // Basic notfication message
	FTask                   // Assigned task message
	FAuth                   // Authorization message: system, device, NFC, NFC Reader...
	FWarn                   // Warning message: emergency, help request...
)

// Internal Pagerino status table
type Status uint16

const (
	StatUnknown Status = iota
	StatOperational
	StatIssue           // Cannot operate until fixed, small issue
	StatTempUnavailable // Cannot operate for a period of time regardless of any action
	StatRepair          // Explicitly repaired and unavailable
	StatOffline         // Not powered on or not disconnected from system
	StatFatal           // Cannot operate until professionally repaired or replaced
	StatMAX             // Maximum value, no other meaning
)

// AppData holds data and connections needed througout the program
type AppData struct {
	ExitSig           chan os.Signal                 // Use on fatal error to graciously exit, otherwise potential leaks
	DBPool            *pgxpool.Pool                  // Execute queries on internal DB on this pool
	APIServer         *grpc.Server                   // Provide API for apps
	MQTTClient        mqtt.Client                    // Get Uplinks and downlinks
	ChirpGateClient   chirp_api.GatewayServiceClient // Check gateway stats and manage them
	ChirpDeviceCLient chirp_api.DeviceServiceClient  // Check device stats
	Timeout           time.Duration                  // General connection/request timeout
}

// Downlink reflects Chirpstack's downlink format
type Downlink struct {
	Confirmed bool   `json:"confirmed"` // false for no Ack
	FPort     uint8  `json:"fPort"`     // refer to FPort enum
	Data      string `json:"data"`      // Base64 payload
}

// Common errors
var (
	ErrNotFound error = errors.New("query returned empty")
	ErrTooLong  error = errors.New("given parameter(s) is too long")
	ErrTimedOut error = errors.New("operation did not complete before timeout")
	ErrModify   error = errors.New("database data manipulation error")
)

type RPCCredentials struct {
	Token string
}

func (cred *RPCCredentials) GetRequestMetadata(ctx context.Context, url ...string) (map[string]string, error) {
	return map[string]string{
		"authorization": "Bearer " + cred.Token,
	}, nil
}

// TODO: Change to true if want certs
func (cred *RPCCredentials) RequireTransportSecurity() bool {
	return false
}

// Get non-empty config value, otherwise terminate program
func FindEnv(name string) string {
	val := os.Getenv(name)
	if val == "" {
		log.Fatalln("Missing or empty configuration value for: " + name)
	}

	return val
}

// MakeTLSConfig loads client certificates for TLS and SSL
func MakeTLSConfig() (*tls.Config, error) {
	// Load CA certificate
	caCert, err := os.ReadFile("./certs/ca.pem")
	if err != nil {
		return nil, err
	}
	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)

	// Load client certificate
	cert, err := tls.LoadX509KeyPair("./certs/client.pem", "./certs/client.key")
	if err != nil {
		return nil, err
	}

	return &tls.Config{
		RootCAs:      caCertPool,
		Certificates: []tls.Certificate{cert},
		MinVersion:   tls.VersionTLS12,
	}, nil
}

func (data *AppData) printChirpStats() {
	ctx_tmout, cancel_ctx := context.WithTimeout(context.Background(), data.Timeout)
	defer cancel_ctx()

	resp_gate, err := data.ChirpGateClient.List(ctx_tmout, &chirp_api.ListGatewaysRequest{
		Limit:       10,
		Offset:      0,
		OrderByDesc: true,
	})
	if err != nil {
		log.Println("Failed to get Chirpstack's API response: " + err.Error())
		return
	}

	log.Println("Found the following gateways (first 10):")
	for idx, gate := range resp_gate.Result {
		log.Println(strconv.FormatInt(int64(idx), 10) + ": App[" +
			gate.TenantId + "]   GateID[" + gate.GatewayId + "]   Name[" +
			gate.Name + "]")
	}
	log.Println("[END]")

	resp_device, err := data.ChirpDeviceCLient.List(ctx_tmout, &chirp_api.ListDevicesRequest{
		Limit:         10,
		Offset:        0,
		OrderByDesc:   true,
		ApplicationId: FindEnv("APP_ID"),
	})
	if err != nil {
		log.Println("Failed to get Chirpstack's API response: " + err.Error())
		return
	}

	log.Println("Found the following devices (first 10):")
	for idx, dev := range resp_device.Result {
		log.Println(strconv.FormatInt(int64(idx), 10) + ": Name[" + dev.Name +
			"]   Description:[" + dev.Description +
			"]   DevEUI[" + dev.DevEui + "]",
		)
	}
	log.Println("[END]")
}

// VerifyCredentials returns no error if password matches in DB
func VerifyCredentials(username string, password string, data *AppData) error {
	if len(username) > MAX_CHAR_LEN || len(password) > MAX_CHAR_LEN {
		return ErrTooLong
	}

	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}

	ctx, close_ctx := context.WithTimeout(context.Background(), data.Timeout)
	defer close_ctx()

	row := data.DBPool.QueryRow(ctx, "SELECT password FROM User WHERE name = $1;")

	var pass string
	if err = row.Scan(&pass); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return fmt.Errorf("%w for: User.name", ErrNotFound)
		} else {
			return err
		}
	}

	err = bcrypt.CompareHashAndPassword(hash, []byte(pass))

	return err
}

// SendDownlink sends downlink message to MQTT broker
func SendDownlink(data *AppData, topic string, msg *Downlink) error {
	jsonData, _ := json.Marshal(msg)
	publish_token := data.MQTTClient.Publish(topic, 0, false, jsonData)
	if ok := publish_token.WaitTimeout(data.Timeout); !ok {
		return fmt.Errorf("%w for: Downlink", ErrTimedOut)
	}

	return nil
}

// makeMQTTHandler returns a callback function that hanldes uplink messages
// from MQTT broker.
//
// Does most of the application logic.
func makeMQTTHandler(data *AppData) mqtt.MessageHandler {
	return func(client mqtt.Client, msg mqtt.Message) {
		var uplink Uplink
		// Parse
		err := json.Unmarshal(msg.Payload(), &uplink)
		if err != nil {
			log.Println("Failed to parse uplink message: " + err.Error())
			return
		}

		// Decode to original + saves space
		decoded, err := base64.StdEncoding.DecodeString(uplink.Data)
		if err != nil {
			log.Println("Failed to decode uplink payload: " + err.Error())
			return
		}

		// Print
		log.Println("Uplink from DeviceEUI [" + uplink.DevEUI + "]")

		switch uplink.FPort {
		case FNotif:
			log.Println("Uplink is responding to a normal message or notification")
		case FTask:
			log.Println("Uplink is responding to a task")
		case FAuth:
			log.Println("Uplink is responding to an authentication message")
		case FWarn:
			log.Println("Uplink is responding to a warning")
		default:
			log.Println("Uplink is responding in an unrecognized channel")
		}

		//data.printChirpStats()

		// Query timeout
		ctx, close := context.WithTimeout(context.Background(), data.Timeout)
		defer close()

		// Get device IDs
		row := data.DBPool.QueryRow(ctx,
			"SELECT id FROM \"Devices\" WHERE euid = '$1';",
			uplink.DevEUI,
		)

		var dev_id int32
		if err = row.Scan(&dev_id); err != nil {
			if err == pgx.ErrNoRows {
				log.Println(
					"Adding new device to DB with DeviceEUI[" + uplink.DevEUI +
						"] and Name[" + uplink.DeviceName + "]",
				)
				data.DBPool.Exec(ctx,
					"INSERT INTO \"Devices\" (euid, name) VALUES ($1, $2)",
					uplink.DevEUI, uplink.DeviceName,
				)

			} else {
				log.Println(err.Error() + ": Device.euid = " + uplink.DevEUI)
				return
			}
			return
		}

		// Add Message to DB
		_, err = data.DBPool.Exec(ctx,
			"INSERT INTO \"Messages\" (sender_id, receiver_id, payload) VALUES ($1, $2, $3);",
			dev_id, nil, decoded,
		)

		if err != nil {
			log.Println(ErrModify.Error() + ": " + err.Error())
			return
		}

		// Get decoded custom payload
		pager_msg, err := serder.Deserialize(decoded)
		if err != nil {
			log.Println("Deserialization error: " + err.Error())
			return
		}

		log.Println(
			"Received Pagerino payload for AppID[" +
				strconv.FormatUint(uint64(pager_msg.AppID), 10) + "]:",
		)
		for _, field := range pager_msg.Fields {
			log.Println(field)
		}
	}
}

func main() {
	// = Quit signal
	quit_signal := make(chan os.Signal, 1)
	signal.Notify(quit_signal, syscall.SIGINT, syscall.SIGTERM)

	Data := AppData{
		ExitSig: quit_signal,
	}

	godotenv.Load("./.env")

	// === Logger settings
	log.SetFlags(log.Ldate | log.Ltime | log.Lmsgprefix | log.Lshortfile)
	log.SetPrefix(FindEnv("LOG_PREFIX") + " ")

	tmout, err := time.ParseDuration(FindEnv("TIMEOUT"))
	if err != nil {
		log.Println(`Format misconfiguration for TIMEOUT: 
Expected: [number]{h|m|s|ms|us|ns}...`)
		return
	}
	Data.Timeout = tmout

	// === PostgreSQL database
	ctx_tmout, cancel_ctx := context.WithTimeout(context.Background(), Data.Timeout)
	defer cancel_ctx()

	db_pool, err := pgxpool.New(ctx_tmout,
		"postgres://"+FindEnv("DB_USER")+":"+FindEnv("DB_PASSWORD")+"@"+
			FindEnv("DB_HOST")+":5432/"+FindEnv("DB_NAME"),
	)
	if err != nil {
		log.Println("DB connection pool creation failed: " + err.Error())
		return
	}
	defer db_pool.Close()
	log.Println("Created database connection.")

	Data.DBPool = db_pool

	// Check responsiveness
	failed := true
	for count := 0; count < MAX_PING_RETRIES; count++ {
		if err = db_pool.Ping(ctx_tmout); err != nil {
			time.Sleep(200 * time.Millisecond)
		} else {
			failed = false
			break
		}
	}
	if failed {
		log.Println("Database is not responding: " + err.Error())
		return
	}

	// === MQTT broker
	tls_config, err := MakeTLSConfig()
	if err != nil {
		log.Println("Error creating TLS configuration: " + err.Error())
		return
	}
	opts := mqtt.NewClientOptions().
		AddBroker(FindEnv("MQTT_ADDRESS")).
		SetClientID(FindEnv("MQTT_CLIENT_ID")).
		SetTLSConfig(tls_config).
		SetKeepAlive(30 * time.Second)

	client := mqtt.NewClient(opts)
	connect_token := client.Connect()
	if err := connect_token.Error(); err != nil {
		log.Println("Failed to connect to MQTT broker: " + err.Error())
		return
	}

	if ok := connect_token.WaitTimeout(Data.Timeout); !ok {

		log.Println("Connection to MQTT broker timed out")
		return
	}

	log.Println("Created MQTT broker connection at: " + FindEnv("MQTT_ADDRESS"))
	defer client.Disconnect(150)

	Data.MQTTClient = client

	// === API Server
	api_port := FindEnv("SERVER_API_PORT")
	net_listen, err := net.Listen("tcp", ":"+api_port)
	if err != nil {
		log.Println("Failed to listen on port " + api_port + ": " + err.Error())
		return
	}

	grpc_server := grpc.NewServer()
	// User service
	api_user.RegisterUserServiceServer(
		grpc_server,
		&UserServiceServer{App: &Data},
	)
	Data.APIServer = grpc_server

	// Serve gRPC
	go func() {
		if err := grpc_server.Serve(net_listen); err != nil {
			log.Println("Failed to serve gRPC API: " + err.Error())
			return
		}
	}()
	log.Println("gRPC API serving on port: " + api_port)

	// === Chirpstack's gRPC API
	dialOpts := []grpc.DialOption{
		//grpc.WithBlock(),
		grpc.WithPerRPCCredentials(&RPCCredentials{FindEnv("CHIRP_API_KEY")}),
		grpc.WithTransportCredentials(insecure.NewCredentials()), // remove this when using TLS
	}

	grpc_conn, err := grpc.Dial(FindEnv("CHIRP_ADDRESS"), dialOpts...)
	if err != nil {
		log.Println("Failed to create a gRPC connection to Chirpstack's API: " + err.Error())
		return
	}

	Data.ChirpGateClient = chirp_api.NewGatewayServiceClient(grpc_conn)
	Data.ChirpDeviceCLient = chirp_api.NewDeviceServiceClient(grpc_conn)
	defer grpc_conn.Close()

	log.Println("Created Chirpstack's API connection at: " + FindEnv("CHIRP_ADDRESS"))

	Data.printChirpStats()

	// === Listen for Uplinks
	// Subscribe to topics
	topic := "application/+/device/+/event/+"
	qos, err := strconv.Atoi(FindEnv("MQTT_QOS"))
	if err != nil {
		log.Println("Format misconfiguration for MQTT_QOS: " + err.Error())
		return
	}

	client.Subscribe(topic, byte(qos), makeMQTTHandler(&Data))
	log.Println("Subscribed to uplink data on: " + topic + ".")

	// === Simulate uplinks
	go func() {
		for {
			time.Sleep(time.Second * 3)

			uplink, err := MakePayload()
			if err != nil {
				log.Println("Error while creating uplink: " + err.Error())
				break
			}

			payload, err := json.Marshal(*uplink)
			if err != nil {
				log.Println("Failed to marshal uplink json: " + err.Error())
				break
			}

			client.Publish(
				"application/"+FindEnv("APP_ID")+"/device/"+DeviceEUI+"/event/up",
				0,
				false,
				payload,
			)
		}

		log.Println("Stopping uplink loop...")
	}()

	// === Shutdown
	<-Data.ExitSig // Continue here on force quit
	log.Println("The application is shutting down.")
}
