package zones

import (
	"bytes"
	"fmt"
	"os"

	"darvaza.org/core"

	"git.jpi.io/amery/jpictl/pkg/wireguard"
)

// GetWireguardKeys reads a wgN.key/wgN.pub files
func (m *Machine) GetWireguardKeys(ring int) (wireguard.KeyPair, error) {
	var (
		data []byte
		err  error
		out  wireguard.KeyPair
	)

	data, err = m.ReadFile("wg%v.key", ring)
	if err != nil {
		// failed to read
		return out, err
	}

	out.PrivateKey, err = wireguard.PrivateKeyFromBase64(string(data))
	if err != nil {
		// bad key
		err = core.Wrapf(err, "wg%v.key", ring)
		return out, err
	}

	data, err = m.ReadFile("wg%v.pub", ring)
	switch {
	case os.IsNotExist(err):
		// no wgN.pub is fine
	case err != nil:
		// failed to read
		return out, err
	default:
		// good read
		out.PublicKey, err = wireguard.PublicKeyFromBase64(string(data))
		if err != nil {
			// bad key
			err = core.Wrapf(err, "wg%v.pub", ring)
			return out, err
		}
	}

	err = out.Validate()
	return out, err
}

func (m *Machine) tryReadWireguardKeys(ring int) error {
	kp, err := m.GetWireguardKeys(ring)
	switch {
	case os.IsNotExist(err):
		// ignore
		return nil
	case err != nil:
		// something went wrong
		return err
	default:
		// import keys
		ri := &RingInfo{
			Ring: ring,
			Keys: kp,
		}

		return m.applyRingInfo(ring, ri)
	}
}

// RemoveWireguardKeys deletes wgN.key and wgN.pub from
// the machine's config directory
func (m *Machine) RemoveWireguardKeys(ring int) error {
	var err error

	err = m.RemoveFile("wg%v.pub", ring)
	switch {
	case os.IsNotExist(err):
		// ignore
	case err != nil:
		return err
	}

	err = m.RemoveFile("wg%v.key", ring)
	if os.IsNotExist(err) {
		// ignore
		err = nil
	}

	return err
}

// GetWireguardConfig reads a wgN.conf file
func (m *Machine) GetWireguardConfig(ring int) (*wireguard.Config, error) {
	data, err := m.ReadFile("wg%v.conf", ring)
	if err != nil {
		return nil, err
	}

	r := bytes.NewReader(data)
	return wireguard.NewConfigFromReader(r)
}

func (m *Machine) tryApplyWireguardConfig(ring int) error {
	wg, err := m.GetWireguardConfig(ring)
	switch {
	case os.IsNotExist(err):
		return nil
	case err != nil:
		return err
	default:
		return m.applyWireguardConfig(ring, wg)
	}
}

func (m *Machine) applyWireguardConfig(ring int, wg *wireguard.Config) error {
	addr := wg.GetAddress()
	zoneID, nodeID, ok := Rings[ring].Decode(addr)
	if !ok {
		return fmt.Errorf("%s: invalid wg%v address: %s", m.Name, ring, addr)
	}

	if err := m.applyZoneNodeID(zoneID, nodeID); err != nil {
		err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
		return err
	}

	if err := m.applyWireguardInterfaceConfig(ring, wg.Interface); err != nil {
		err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
		return err
	}

	for _, peer := range wg.Peer {
		if err := m.applyWireguardPeerConfig(ring, peer); err != nil {
			err = core.Wrapf(err, "%s: wg%v:%s", m.Name, ring, addr)
			return err
		}
	}

	return nil
}

func (m *Machine) getRingInfo(ring int) (*RingInfo, bool) {
	for _, ri := range m.Rings {
		if ri.Ring == ring {
			return ri, ri.Enabled
		}
	}

	return nil, false
}

func (m *Machine) applyRingInfo(ring int, new *RingInfo) error {
	cur, _ := m.getRingInfo(ring)
	if cur == nil {
		// first, append
		m.Rings = append(m.Rings, new)
		return nil
	}

	// extra, merge
	return cur.Merge(new)
}

func (m *Machine) applyWireguardInterfaceConfig(ring int, data wireguard.InterfaceConfig) error {
	ri := &RingInfo{
		Ring:    ring,
		Enabled: true,
		Keys: wireguard.KeyPair{
			PrivateKey: data.PrivateKey,
		},
	}

	return m.applyRingInfo(ring, ri)
}

func (m *Machine) applyWireguardPeerConfig(ring int, pc wireguard.PeerConfig) error {
	peer, found := m.getPeerByName(pc.Endpoint.Name())
	switch {
	case !found:
		// unknown
	case ring == 1 && m.zone != peer.zone:
		// invalid zone
	default:
		// apply RingInfo
		ri := &RingInfo{
			Ring:    ring,
			Enabled: true,
			Keys: wireguard.KeyPair{
				PublicKey: pc.PublicKey,
			},
		}

		return peer.applyRingInfo(ring, ri)
	}

	return fmt.Errorf("%q: invalid peer endpoint", pc.Endpoint.Host)
}

func (m *Machine) applyZoneNodeID(zoneID, nodeID int) error {
	switch {
	case zoneID == 0:
		return fmt.Errorf("invalid %s", "zoneID")
	case nodeID == 0:
		return fmt.Errorf("invalid %s", "nodeID")
	case m.ID != nodeID:
		return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.ID, nodeID)
	case m.zone.ID != 0 && m.zone.ID != zoneID:
		return fmt.Errorf("invalid %s: %v ≠ %v", "zoneID", m.zone.ID, zoneID)
	case m.zone.ID == 0:
		m.zone.ID = zoneID
	}

	return nil
}

// RemoveWireguardConfig deletes wgN.conf from the machine's
// config directory.
func (m *Machine) RemoveWireguardConfig(ring int) error {
	err := m.RemoveFile("wg%v.conf", ring)
	if os.IsNotExist(err) {
		err = nil
	}

	return err
}

func (m *Machine) createRingInfo(ring int, enabled bool) (*RingInfo, error) {
	keys, err := wireguard.NewKeyPair()
	if err != nil {
		return nil, err
	}

	ri := &RingInfo{
		Ring:    ring,
		Enabled: enabled,
		Keys:    keys,
	}

	err = m.applyRingInfo(ring, ri)
	if err != nil {
		return nil, err
	}

	return ri, nil
}