package main

import (
	"flag"
	"log"
	"time"

	"github.com/aws/aws-sdk-go/aws/session"
	"go.uber.org/zap"

	"github.com/filtr/infrastructure/go/openvpn-route-updater/internal/config"
	"github.com/filtr/infrastructure/go/openvpn-route-updater/internal/dnsresolver"
	"github.com/filtr/infrastructure/go/openvpn-route-updater/internal/routeupdater"
	"github.com/filtr/infrastructure/go/openvpn-route-updater/internal/utility"
	"github.com/filtr/infrastructure/go/openvpn-route-updater/zapcloudwatch"
)

func main() {
	var (
		verbose      bool
		dryrun       bool
		cwLogGroup   string
		cwAwsProfile string
		configName   string
		sacli        string
	)
	flag.BoolVar(&verbose, "verbose", false, "Enable verbose logging")
	flag.BoolVar(&dryrun, "dryrun", false, "Do not apply proposed changes")
	flag.StringVar(&configName, "config", "config.yaml", "Path to the file with the configuration")
	flag.StringVar(&sacli, "sacli", "/usr/local/openvpn_as/scripts/sacli", "Path to OpenVPN sacli")
	flag.StringVar(&cwLogGroup, "cwLogGroup", "", "Cloudwatch log group name")
	flag.StringVar(&cwAwsProfile, "cwAwsProfile", "", "AWS profile for sending logs to cloudwatch")
	flag.Parse()

	var logger *zap.Logger
	var err error
	zapConfig := zap.NewProductionConfig()
	if verbose {
		zapConfig.Level.SetLevel(zap.DebugLevel)
	}
	if cwLogGroup != "" {
		cwSess, err := session.NewSessionWithOptions(session.Options{
			Profile:           cwAwsProfile,
			SharedConfigState: session.SharedConfigEnable,
		})
		if err != nil {
			log.Fatalf("Unable to create AWS client with profile %s", cwAwsProfile)
		}
		logStream := time.Now().Format("20060102150405")
		log.Printf("log group %s log stream %s", cwLogGroup, logStream)
		hook, err := zapcloudwatch.NewCloudwatchHook(cwLogGroup, logStream, cwSess).Hook()
		if err != nil {
			log.Fatalf("Unable to create hook %s", err.Error())
		}
		logger, err = zapConfig.Build(zap.Hooks(hook))
		if err != nil {
			log.Fatalf("failed to init logger: %s", err)
		}
	} else {
		logger, err = zapConfig.Build()
		if err != nil {
			log.Fatalf("failed to init logger: %s", err)
		}
	}
	slog := logger.Sugar()

	// Parse config
	MainConfig, err := config.ReadConfig(configName)
	if err != nil {
		slog.Fatalf("Failed to parse config: %s", err)
	}
	MainCM := &config.MainConfigManager{Config: *MainConfig}
	MainCM.SetLogger(slog)
	MainCM.InitializeManager()
	slog.Debug(MainConfig.AdminGroupConfigs)

	// Initialize openvpncli
	openvpncli := routeupdater.NewCli(slog, sacli)
	// add cli verification ... err = openvpncli.Verify()

	// Add flag to restart server to apply made changes
	restartServer := false
	Groups := append(MainCM.Config.Groups, MainCM.Config.AdminGroupConfigs...)

	// For each group - resolve all required route53 records and singleRecords
	// Get list of ipaddresses to add to routes
	// Compare them with current routes
	// If there are any changes push newly created routes instead of old ones
	// Only public routes are affected
	for _, group := range Groups {
		groupManager := &config.GroupConfigManager{Group: group}
		groupManager.SetLogger(slog)
		slog.Infof("Group %v", group)
		records, err := dnsresolver.GetRoute53RecordsByGroupConfig(slog, groupManager)
		if err != nil {
			slog.Fatal(err)
		}
		slog.Debug(records)

		route53IPsRegistry := dnsresolver.ResolveRoute53Records(slog, records)
		slog.Debug(route53IPsRegistry)

		customIPsRegistry := dnsresolver.ResolveCustomRecords(slog, group.SingleRecords)
		slog.Debug(customIPsRegistry)

		mainRegistry := dnsresolver.MergeRegistries(route53IPsRegistry, customIPsRegistry)
		err = mainRegistry.RemovePrivateAddressesIPv4(slog)
		if err != nil {
			slog.Fatal(err)
		}
		slog.Debug(mainRegistry)
		filteredIPs := mainRegistry.GetIPList()
		slog.Debug(filteredIPs)
		slog.Infof("Length of filtered IP list - %d", len(filteredIPs))

		OVPNGroup, err := routeupdater.NewGroup(slog, group.Name, openvpncli)
		if err != nil {
			slog.Fatal(err)
		}
		OVPNGroupManager := &routeupdater.OpenVPNGroupManager{Group: OVPNGroup}
		OVPNGroupManager.SetLogger(slog)
		// Reindex routes, apply them, fail if further reindex is needed
		slog.Debug(OVPNGroup)
		if OVPNGroupManager.ReindexRoutes() && !dryrun {
			OVPNGroupManager.ApplyPrivateRoutes(openvpncli)
			OVPNGroupManager.ApplyPublicRoutes(openvpncli)
			OVPNGroupManager.Refresh(openvpncli)
			if OVPNGroupManager.ReindexRoutes() {
				slog.Info(OVPNGroup)
				slog.Fatalf("Routes reindex failed for the group %v", group.Name)
			}
		}
		slog.Debug(OVPNGroup)
		// Check filteredIPs diff with OVPNGroup public routes
		// if there is difference, trigger OVPNGroup route update and than trigger apply.
		newPublicRoutes := utility.Unique(routeupdater.IPListToRouteValues(filteredIPs))
		oldPublicRoutes := utility.Unique(OVPNGroupManager.GetPublicRouteValues())

		// In case there are less new routes than it was previously, you can't simply apply new routes
		// because the excessive routes will still remain in this case.
		// Adding a flag which will trigger force remove of old public routes
		var forceRemovePublicRoutes bool
		if len(oldPublicRoutes) > len(newPublicRoutes) {
			forceRemovePublicRoutes = true
		}
		// Check whether there are routes which needed to be cleaned up.
		// If amount of routes to delete exceedes amount of routes to add,
		// you have to force remove excessive routes .
		//

		routesToRemove := utility.Diff(oldPublicRoutes, newPublicRoutes)
		// Format routes to actual list of IP addresses
		if len(routesToRemove) > 0 {
			slog.Info("Remove routes for IPs list below")
			removeIpList, err := routeupdater.RoutesToIPList(routesToRemove)
			if err != nil {
				slog.Fatal(err)
			}
			slog.Info(removeIpList)
		} else {
			slog.Info("No routes to remove")
		}

		routesToAdd := utility.Diff(newPublicRoutes, oldPublicRoutes)
		if len(routesToAdd) > 0 {
			// Get Registry values to check the list of added IPs and
			// for what dns records are tthey added.
			slog.Info("Adding routes for Registry IP list below")
			addIpList, err := routeupdater.RoutesToIPList(routesToAdd)
			if err != nil {
				slog.Fatal(err)
			}
			var strAddIpList []string
			for _, ip := range addIpList {
				strAddIpList = append(strAddIpList, ip.To4().String())
			}
			infoRegistry := dnsresolver.CopyRegistry(mainRegistry, strAddIpList)
			slog.Info(infoRegistry.ResolvedIPs)
		} else {
			slog.Info("No routes to add")
		}

		if !dryrun {
			if len(routesToAdd) > 0 || len(routesToRemove) > 0 {
				// Force remove public routes if flag has been set
				if forceRemovePublicRoutes {
					slog.Info("Force remove flag has been set, removing old public routes")
					err = OVPNGroupManager.DeletePublicRoutesCli(openvpncli)
					if err != nil {
						slog.Fatal(err)
					}
				}
				err = OVPNGroupManager.SetPublicRoutes(newPublicRoutes)
				if err != nil {
					slog.Fatal(err)
				}
				slog.Debug(OVPNGroup)
				slog.Infof("Applying new public routes, number of routes %v", len(OVPNGroupManager.Group.PublicRoutes))
				err = OVPNGroupManager.ApplyPublicRoutes(openvpncli)
				if err != nil {
					slog.Fatal(err)
				}
				restartServer = true
			} else {
				slog.Info("No changes in public routes, skipping update.")
			}
		} else {
			if len(routesToAdd) > 0 || len(routesToRemove) > 0 {
				err = OVPNGroupManager.SetPublicRoutes(newPublicRoutes)
				if err != nil {
					slog.Fatal(err)
				}
				slog.Debug(OVPNGroup)
				slog.Info("Applying new public routes")
			} else {
				slog.Info("Dryrun mode enabled, skipping update.")
			}
		}
	}
	if restartServer && !dryrun {
		slog.Info("Restarting server to apply changes")
		response, err := openvpncli.RestartServer()
		if err != nil {
			slog.Fatal(err)
		}

		slog.Info(string(response))
	}
	return
}
