// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package kubespan

import (
	"errors"
	"fmt"
	"os"

	"github.com/hashicorp/go-multierror"
	"github.com/vishvananda/netlink"
	"golang.org/x/sys/unix"
)

// RulesManager manages routing rules outside of controllers/resources scope.
//
// TODO: this might be refactored later to support routing rules in the native network resources.
type RulesManager interface {
	Install() error
	Cleanup() error
}

// NewRulesManager initializes new RulesManager.
func NewRulesManager(targetTable, internalMark, markMask int) RulesManager {
	return &rulesManager{
		TargetTable:  targetTable,
		InternalMark: internalMark,
		MarkMask:     markMask,
	}
}

type rulesManager struct {
	TargetTable  int
	InternalMark int
	MarkMask     int
}

// Install routing rules.
func (m *rulesManager) Install() error {
	nc, err := netlink.NewHandle()
	if err != nil {
		return fmt.Errorf("failed to get netlink handle: %w", err)
	}

	defer nc.Close()

	if err := nc.RuleAdd(&netlink.Rule{
		Priority:          nextRuleNumber(nc, unix.AF_INET),
		Family:            unix.AF_INET,
		Table:             m.TargetTable,
		Mark:              m.InternalMark,
		Mask:              m.MarkMask,
		Goto:              -1,
		Flow:              -1,
		SuppressIfgroup:   -1,
		SuppressPrefixlen: -1,
	}); err != nil {
		if !errors.Is(err, os.ErrExist) {
			return fmt.Errorf("failed to add IPv4 table-mark rule: %w", err)
		}
	}

	if err := nc.RuleAdd(&netlink.Rule{
		Priority:          nextRuleNumber(nc, unix.AF_INET6),
		Family:            unix.AF_INET6,
		Table:             m.TargetTable,
		Mark:              m.InternalMark,
		Mask:              m.MarkMask,
		Goto:              -1,
		Flow:              -1,
		SuppressIfgroup:   -1,
		SuppressPrefixlen: -1,
	}); err != nil {
		if !errors.Is(err, os.ErrExist) {
			return fmt.Errorf("failed to add IPv6 table-mark rule: %w", err)
		}
	}

	return nil
}

func (m *rulesManager) deleteRulesFamily(nc *netlink.Handle, family int) error {
	var merr *multierror.Error

	list, err := nc.RuleList(family)
	if err != nil {
		merr = multierror.Append(merr, fmt.Errorf("failed to get route rules: %w", err))
	}

	for _, r := range list {
		if r.Table == m.TargetTable &&
			r.Mark == m.InternalMark {
			thisRule := r

			if err := nc.RuleDel(&thisRule); err != nil {
				if !errors.Is(err, os.ErrNotExist) {
					merr = multierror.Append(merr, err)
				}
			}

			break
		}
	}

	return merr.ErrorOrNil()
}

// Cleanup the installed routing rules.
func (m *rulesManager) Cleanup() error {
	var merr *multierror.Error

	nc, err := netlink.NewHandle()
	if err != nil {
		return fmt.Errorf("failed to get netlink handle: %w", err)
	}

	defer nc.Close()

	if err = m.deleteRulesFamily(nc, unix.AF_INET); err != nil {
		merr = multierror.Append(merr, fmt.Errorf("failed to delete all IPv4 route rules: %w", err))
	}

	if err = m.deleteRulesFamily(nc, unix.AF_INET6); err != nil {
		merr = multierror.Append(merr, fmt.Errorf("failed to delete all IPv6 route rules: %w", err))
	}

	return merr.ErrorOrNil()
}

func nextRuleNumber(nc *netlink.Handle, family int) int {
	list, err := nc.RuleList(family)
	if err != nil {
		return 0
	}

	for i := 32500; i > 0; i-- {
		var found bool

		for _, r := range list {
			if r.Priority == i {
				found = true

				break
			}
		}

		if !found {
			return i
		}
	}

	return 0
}
