// Copyright 2017 flannel authors
//
// 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.
//go:build !windows
// +build !windows

package ipsec

import (
	"context"
	"fmt"
	"net"
	"os"
	"os/exec"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/bronze1man/goStrongswanVici"
	"github.com/flannel-io/flannel/pkg/lease"
	log "k8s.io/klog/v2"
)

type Uri struct {
	network, address string
}

type CharonIKEDaemon struct {
	viciUri     Uri
	espProposal string
	ctx         context.Context
}

func NewCharonIKEDaemon(ctx context.Context, wg *sync.WaitGroup, espProposal string) (*CharonIKEDaemon, error) {
	charon := &CharonIKEDaemon{ctx: ctx, espProposal: espProposal}

	addr := strings.Split("unix:///var/run/charon.vici", "://")
	charon.viciUri = Uri{addr[0], addr[1]}

	execPath, err := findExecPath()
	if err != nil {
		log.Errorf("Charon daemon not found: %v", err)
		return nil, err
	}

	cmd, err := charon.run(execPath)
	if err != nil {
		log.Errorf("Error starting charon daemon: %v", err)
		return nil, err
	} else {
		log.Info("Charon daemon started")
	}
	wg.Add(1)
	go func() {
		<-ctx.Done()
		err := cmd.Process.Signal(syscall.SIGTERM)
		if err != nil {
			log.Errorf("Error while processing the signal: %v", err)
		}
		err = cmd.Wait()
		if err != nil {
			log.Errorf("Error while waiting for process to exit: %v", err)
		}
		log.Infof("Stopped charon daemon")
		wg.Done()
	}()
	return charon, nil
}

func (charon *CharonIKEDaemon) getClient(wait bool) (client *goStrongswanVici.ClientConn, err error) {
	for {
		socket_conn, err := net.Dial(charon.viciUri.network, charon.viciUri.address)
		if err == nil {
			return goStrongswanVici.NewClientConn(socket_conn), nil
		} else {
			if wait {
				select {
				case <-charon.ctx.Done():
					log.Error("Cancel waiting for charon")
					return nil, err
				default:
					log.Errorf("ClientConnection failed: %v", err)
				}

				log.Info("Retrying in a second ...")
				time.Sleep(time.Second)
			} else {
				return nil, err
			}
		}
	}
}

func (charon *CharonIKEDaemon) run(execPath string) (cmd *exec.Cmd, err error) {
	cmd = &exec.Cmd{
		Path: execPath,
		SysProcAttr: &syscall.SysProcAttr{
			Pdeathsig: syscall.SIGTERM,
		},
		Stdout: os.Stdout,
		Stderr: os.Stderr,
	}
	err = cmd.Start()
	return
}

func (charon *CharonIKEDaemon) LoadSharedKey(remotePublicIP, password string) error {
	var err error
	var client *goStrongswanVici.ClientConn

	client, err = charon.getClient(true)
	if err != nil {
		log.Errorf("Failed to acquire Vici client: %v", err)
		return err
	}

	defer client.Close()

	sharedKey := &goStrongswanVici.Key{
		Typ:    "IKE",
		Data:   password,
		Owners: []string{remotePublicIP},
	}

	for {
		err = client.LoadShared(sharedKey)
		if err != nil {
			log.Errorf("Failed to load my key. Retrying. %v", err)
			time.Sleep(time.Second)
		} else {
			break
		}
	}

	log.Infof("Loaded shared key for: %v", remotePublicIP)
	return nil
}

func (charon *CharonIKEDaemon) LoadConnection(localLease, remoteLease *lease.Lease,
	reqID, encap string) error {
	var err error
	var client *goStrongswanVici.ClientConn

	client, err = charon.getClient(true)
	if err != nil {
		log.Errorf("Failed to acquire Vici client: %s", err)
		return err
	}
	defer client.Close()

	childConfMap := make(map[string]goStrongswanVici.ChildSAConf)
	childSAConf := goStrongswanVici.ChildSAConf{
		Local_ts:      []string{localLease.Subnet.String()},
		Remote_ts:     []string{remoteLease.Subnet.String()},
		ESPProposals:  []string{charon.espProposal},
		StartAction:   "start",
		CloseAction:   "trap",
		DpdAction:     "restart",
		Mode:          "tunnel",
		ReqID:         reqID,
		RekeyTime:     "1h",
		InstallPolicy: "no",
	}

	childSAConfName := formatChildSAConfName(localLease, remoteLease)

	childConfMap[childSAConfName] = childSAConf

	localAuthConf := goStrongswanVici.AuthConf{
		AuthMethod: "psk",
	}
	remoteAuthConf := goStrongswanVici.AuthConf{
		AuthMethod: "psk",
	}

	ikeConf := goStrongswanVici.IKEConf{
		LocalAddrs:  []string{localLease.Attrs.PublicIP.String()},
		RemoteAddrs: []string{remoteLease.Attrs.PublicIP.String()},
		Proposals:   []string{"aes256-sha256-modp4096"},
		Version:     "2",
		KeyingTries: "0", //continues to retry
		LocalAuth:   localAuthConf,
		RemoteAuth:  remoteAuthConf,
		Children:    childConfMap,
		Encap:       encap,
	}
	ikeConfMap := make(map[string]goStrongswanVici.IKEConf)

	connectionName := formatConnectionName(localLease, remoteLease)
	ikeConfMap[connectionName] = ikeConf

	err = client.LoadConn(&ikeConfMap)
	if err != nil {
		return err
	}

	log.Infof("Loaded connection: %v", connectionName)
	return nil
}

func (charon *CharonIKEDaemon) UnloadCharonConnection(localLease,
	remoteLease *lease.Lease) error {
	client, err := charon.getClient(false)
	if err != nil {
		log.Errorf("Failed to acquire Vici client: %s", err)
		return err
	}
	defer client.Close()

	connectionName := formatConnectionName(localLease, remoteLease)
	unloadConnRequest := &goStrongswanVici.UnloadConnRequest{
		Name: connectionName,
	}

	err = client.UnloadConn(unloadConnRequest)
	if err != nil {
		return err
	}

	log.Infof("Unloaded connection: %v", connectionName)
	return nil
}

func formatConnectionName(localLease, remoteLease *lease.Lease) string {
	return fmt.Sprintf("%s-%s-%s-%s", localLease.Attrs.PublicIP,
		localLease.Subnet, remoteLease.Subnet, remoteLease.Attrs.PublicIP)
}

func formatChildSAConfName(localLease, remoteLease *lease.Lease) string {
	return fmt.Sprintf("%s-%s", localLease.Subnet, remoteLease.Subnet)
}

func findExecPath() (string, error) {
	// try well known charon paths
	paths := []string{
		"charon",                         // PATH
		"/usr/lib/strongswan/charon",     // alpine, arch, flannel container
		"/usr/lib/ipsec/charon",          // debian/ubuntu
		"/usr/libexec/strongswan/charon", // centos/rhel
		"/usr/libexec/ipsec/charon",      // opensuse/sles
	}
	for _, path := range paths {
		path, err := exec.LookPath(path)
		if err != nil {
			log.Warningf("No valid charon executable found at path %s: %v", path, err)
			continue
		}
		return path, nil
	}

	err := fmt.Errorf("No valid charon executable found at paths %v", paths)
	return "", err
}
