// Copyright 2024 Northern.tech AS
//
//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at
//
//        http://www.apache.org/licenses/LICENSE-2.0
//
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS,
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//    See the License for the specific language governing permissions and
//    limitations under the License.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"

	"github.com/mendersoftware/mender-server/services/iot-manager/app"
	"github.com/mendersoftware/mender-server/services/iot-manager/client/iotcore"
	"github.com/mendersoftware/mender-server/services/iot-manager/client/iothub"
	"github.com/mendersoftware/mender-server/services/iot-manager/cmd"
	dconfig "github.com/mendersoftware/mender-server/services/iot-manager/config"
	"github.com/mendersoftware/mender-server/services/iot-manager/crypto"
	"github.com/mendersoftware/mender-server/services/iot-manager/model"
	"github.com/mendersoftware/mender-server/services/iot-manager/server"
	store "github.com/mendersoftware/mender-server/services/iot-manager/store/mongo"

	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"

	oas "github.com/mendersoftware/mender-server/pkg/api"
	openapi "github.com/mendersoftware/mender-server/pkg/api/client"
	"github.com/mendersoftware/mender-server/pkg/config"
	"github.com/mendersoftware/mender-server/pkg/log"
	"github.com/mendersoftware/mender-server/pkg/version"
)

var appVersion = version.Get()

const helpReencrypt = `Re-encrypt all client-side encrypted data in the database.
To run the command, the server MUST first be configured to use the old key as ` +
	"`aes_encryption_fallback_key`" + `.
Add the new key as the ` + "`aes_encryption_key`" + ` and run this command.`

func main() {
	doMain(os.Args)
}

func doMain(args []string) {
	var configPath string

	app := &cli.App{
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name: "config",
				Usage: "Configuration `FILE`. " +
					"Supports JSON, TOML, YAML and HCL " +
					"formatted configs.",
				Value:       "/etc/iot-manager/config.yaml",
				Destination: &configPath,
			},
			&cli.StringFlag{
				Name:  "log-level",
				Usage: "Log `LEVEL` to emit to standard error.",
				Value: "info",
			},
		},
		Commands: []cli.Command{
			{
				Name:   "server",
				Usage:  "Run the HTTP API server",
				Action: cmdServer,
				Flags: []cli.Flag{
					&cli.BoolFlag{
						Name:  "automigrate",
						Usage: "Run database migrations before starting.",
					},
				},
			},
			{
				Name:   "migrate",
				Usage:  "Run the migrations",
				Action: cmdMigrate,
			},
			{
				Name:   "re-encrypt",
				Usage:  helpReencrypt,
				Action: cmdReencrypt,
				Flags: []cli.Flag{
					cli.StringFlag{
						Name:  "to-key",
						Usage: "Override for the updated encryption key",
					},
					cli.StringFlag{
						Name:  "from-key",
						Usage: "Override for the from encryption key.",
					},
				},
			},
			{
				Name:   "sync-devices",
				Usage:  "Synchronize device state across IoT platforms.",
				Action: cmdSync,
				Flags: []cli.Flag{
					&cli.IntFlag{
						Name:  "batch-size",
						Usage: "Maximum number of devices to sync in a batch.",
						Value: 50,
					},
					&cli.BoolFlag{
						Name:  "fail-early",
						Usage: "Do not ignore non-fatal errors.",
					},
				},
			},
			{
				Name:  "version",
				Usage: "Show version information",
				Flags: []cli.Flag{
					cli.StringFlag{
						Name:  "output",
						Usage: "Output format <json|text>",
						Value: "text",
					},
				},
				Action: func(args *cli.Context) error {
					switch strings.ToLower(args.String("output")) {
					case "text":
						fmt.Print(appVersion)
					case "json":
						_ = json.NewEncoder(os.Stdout).Encode(appVersion)
					default:
						return fmt.Errorf("Unknown output format %q", args.String("output"))
					}
					return nil
				},
			},
		},
		Version: appVersion.Version,
	}
	app.Usage = "IoT Manager"
	app.Action = cmdServer

	app.Before = func(args *cli.Context) error {
		lvl, err := logrus.ParseLevel(args.String("log-level"))
		if err != nil {
			return err
		}
		log.Log.Level = lvl

		err = config.FromConfigFile(configPath, dconfig.Defaults)
		if err != nil {
			return cli.NewExitError(
				fmt.Sprintf("error loading configuration: %s", err),
				1)
		}

		// Enable setting config values by environment variables
		config.Config.SetEnvPrefix("IOT_MANAGER")
		config.Config.AutomaticEnv()
		config.Config.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))

		// Set encryption keys
		err = crypto.SetAESEncryptionKey(
			config.Config.GetString(dconfig.SettingAESEncryptionKey),
		)
		if err == nil {
			err = crypto.SetAESEncryptionFallbackKey(
				config.Config.GetString(dconfig.SettingAESEncryptionFallbackKey),
			)
		}
		model.SetTrustedHostnames(
			config.Config.GetStringSlice(dconfig.SettingDomainWhitelist),
		)

		store.SetEventExpiration(
			config.Config.GetInt64(dconfig.SettingEventExpirationTimeout),
		)

		return err
	}

	err := app.Run(args)
	if err != nil {
		logrus.Fatal(err)
	}
}

func cmdServer(args *cli.Context) error {
	mgoConfig := store.NewConfig().SetAutomigrate(args.Bool("automigrate"))
	dataStore, err := store.SetupDataStore(mgoConfig)
	if err != nil {
		return err
	}
	defer dataStore.Close()
	return server.InitAndRun(config.Config, dataStore)
}

func cmdMigrate(args *cli.Context) error {
	mgoConfig := store.NewConfig().SetAutomigrate(true)
	dataStore, err := store.SetupDataStore(mgoConfig)
	if err != nil {
		return err
	}
	return dataStore.Close()
}

func cmdReencrypt(args *cli.Context) error {
	mgoConfig := store.NewConfig().SetAutomigrate(args.Bool("automigrate"))
	dataStore, err := store.SetupDataStore(mgoConfig)
	if err != nil {
		return err
	}
	defer dataStore.Close()
	toKey := config.Config.GetString(dconfig.SettingAESEncryptionKey)
	if args.IsSet("to-key") {
		toKey = args.String("to-key")
		err := crypto.SetAESEncryptionKey(toKey)
		if err != nil {
			return fmt.Errorf("error configuring new AES encryption key: %w", err)
		}
	}
	if toKey == "" {
		return fmt.Errorf("--to-key or aes_encryption_key key must be non-empty")
	}

	fromKey := config.Config.GetString(dconfig.SettingAESEncryptionFallbackKey)
	if args.IsSet("from-key") {
		fromKey = args.String("from-key")
		err := crypto.SetAESEncryptionFallbackKey(args.String("from-key"))
		if err != nil {
			return fmt.Errorf("error configuring AES encryption key: %w", err)
		}
	}
	if fromKey == "" {
		return fmt.Errorf("--from-key or aes_encryption_fallback_key key must be non-empty")
	}
	return cmd.Reencrypt(dataStore)
}

func cmdSync(args *cli.Context) error {
	if bs := args.Int("batch-size"); bs <= 0 {
		return cli.NewExitError(
			"invalid flag 'batch-size': must be a positive integer", 1,
		)
	} else if bs > 500 {
		// This is the max page size from deviceauth
		return cli.NewExitError(
			"invalid flag 'batch-size': must be less than 500", 1,
		)
	}
	httpClient := new(http.Client)
	ctx := context.Background()

	wfCfg, err := oas.NewDefaultClientConfigurationFromURL(
		config.Config.GetString(dconfig.SettingWorkflowsURL),
	)
	if err != nil {
		return fmt.Errorf("failed to initialize workflows client: %w", err)
	}
	wf := openapi.NewAPIClient(wfCfg).WorkflowsOtherAPI
	daCfg, err := oas.NewDefaultClientConfigurationFromURL(
		config.Config.GetString(dconfig.SettingDeviceauthURL),
	)
	if err != nil {
		return fmt.Errorf("failed to initialize deviceauth client: %w", err)
	}
	devauth := openapi.NewAPIClient(daCfg).DeviceAuthenticationInternalAPIAPI
	hub := iothub.NewClient(iothub.NewOptions().SetClient(httpClient))
	core := iotcore.NewClient()
	mgoConfig := store.NewConfig()

	ds, err := store.SetupDataStore(mgoConfig)
	if err != nil {
		return err
	}
	defer ds.Close()
	app := app.New(ds, wf, devauth).WithIoTHub(hub).WithIoTCore(core)
	app = app.WithWebhooksTimeout(config.Config.GetUint(dconfig.SettingWebhooksTimeoutSeconds))
	return app.SyncDevices(ctx, args.Int("batch-size"), args.Bool("fail-early"))
}
