// Copyright 2019 the Kilo 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.

package encapsulation

import (
	"fmt"
	"net"
	"sync"

	"github.com/squat/kilo/pkg/iptables"
	"github.com/vishvananda/netlink"
)

const flannelDeviceName = "flannel.1"

type flannel struct {
	iface    int
	strategy Strategy
	ch       chan netlink.LinkUpdate
	done     chan struct{}
	// mu guards updates to the iface field.
	mu sync.Mutex
}

// NewFlannel returns an encapsulator that uses Flannel.
func NewFlannel(strategy Strategy) Encapsulator {
	return &flannel{
		ch:       make(chan netlink.LinkUpdate),
		done:     make(chan struct{}),
		strategy: strategy,
	}
}

// CleanUp is a no-op.
func (f *flannel) CleanUp() error {
	close(f.done)
	return nil
}

// Gw returns the correct gateway IP associated with the given node.
func (f *flannel) Gw(_, _, _ net.IP, subnet *net.IPNet) net.IP {
	return subnet.IP
}

// CNICompatibilityIP is a no-op for Flannel.
func (f *flannel) CNICompatibilityIP() *net.IPNet {
	return nil
}

// Index returns the index of the Flannel interface.
func (f *flannel) Index() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.iface
}

// Init finds the Flannel interface index.
func (f *flannel) Init(_ int) error {
	if err := netlink.LinkSubscribe(f.ch, f.done); err != nil {
		return fmt.Errorf("failed to subscribe to updates to %s: %v", flannelDeviceName, err)
	}
	go func() {
		var lu netlink.LinkUpdate
		for {
			select {
			case lu = <-f.ch:
				if lu.Attrs().Name == flannelDeviceName {
					f.mu.Lock()
					f.iface = lu.Attrs().Index
					f.mu.Unlock()
				}
			case <-f.done:
				return
			}
		}
	}()
	i, err := netlink.LinkByName(flannelDeviceName)
	if _, ok := err.(netlink.LinkNotFoundError); ok {
		return nil
	}
	if err != nil {
		return fmt.Errorf("failed to query for Flannel interface: %v", err)
	}
	f.mu.Lock()
	f.iface = i.Attrs().Index
	f.mu.Unlock()
	return nil
}

// Rules is a no-op.
func (f *flannel) Rules(_ []*net.IPNet) iptables.RuleSet {
	return iptables.RuleSet{}
}

// Set is a no-op.
func (f *flannel) Set(_ *net.IPNet) error {
	return nil
}

// Strategy returns the configured strategy for encapsulation.
func (f *flannel) Strategy() Strategy {
	return f.strategy
}
