package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"os/exec"
	"strconv"
	"strings"
)

var ErrorAlreadyExists = errors.New("already exists")
var ErrorNotFound = errors.New("not found")

type client struct {
	config string
}

func newClient(config string) *client {
	return &client{
		config: fmt.Sprintf("--config=%s", config),
	}
}

func (c *client) run(cc ...string) (string, error) {
	// TODO(giolekva): make expiration configurable, and auto-refresh
	cc = append(cc, c.config)
	cmd := exec.Command("headscale", cc...)
	out, err := cmd.Output()
	if err != nil {
		return "", err
	}
	return string(out), nil

}

func (c *client) createUser(name string) error {
	cmd := exec.Command("headscale", c.config, "users", "create", name)
	out, err := cmd.Output()
	outStr := string(out)
	if err != nil && strings.Contains(outStr, "User already exists") {
		return ErrorAlreadyExists
	}
	return err
}

func (c *client) createPreAuthKey(user string) (string, error) {
	// TODO(giolekva): make expiration configurable, and auto-refresh
	cmd := exec.Command("headscale", c.config, "--user", user, "preauthkeys", "create", "--reusable", "--expiration", "365d")
	out, err := cmd.Output()
	fmt.Println(string(out))
	if err != nil {
		return "", err
	}
	return extractLastLine(string(out))
}

func (c *client) expirePreAuthKey(user, authKey string) error {
	cmd := exec.Command("headscale", c.config, "--user", user, "preauthkeys", "expire", authKey)
	out, err := cmd.Output()
	fmt.Println(string(out))
	if err != nil {
		return err
	}
	return nil
}

func (c *client) expireUserNode(user, node string) error {
	id, err := c.getNodeId(user, node)
	if err != nil {
		return err
	}
	cmd := exec.Command("headscale", c.config, "node", "expire", "--identifier", id)
	out, err := cmd.Output()
	fmt.Println(string(out))
	if err != nil {
		return err
	}
	return nil
}

func (c *client) removeUserNode(user, node string) error {
	id, err := c.getNodeId(user, node)
	if err != nil {
		return err
	}
	cmd := exec.Command("headscale", c.config, "node", "delete", "--identifier", id, "--force")
	out, err := cmd.Output()
	fmt.Println(string(out))
	if err != nil {
		return err
	}
	return nil
}

func (c *client) enableRoute(id string) error {
	cmd := exec.Command("headscale", c.config, "routes", "enable", "-r", id)
	out, err := cmd.Output()
	fmt.Println(string(out))
	return err
}

type timeInfo struct {
	Seconds int `json:"seconds"`
	Nanos   int `json:"nanos"`
}

type nodeInfo struct {
	Id          int      `json:"id"`
	Name        string   `json:"name"`
	GivenName   string   `json:"given_name"`
	IPAddresses []net.IP `json:"ip_addresses"`
	LastSeen    timeInfo `json:"last_seen"`
}

func (c *client) getUserNodes(user string) ([]nodeInfo, error) {
	cmd := exec.Command("headscale", c.config, "--user", user, "node", "list", "-o", "json")
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}
	var nodes []nodeInfo
	if err := json.NewDecoder(bytes.NewReader(out)).Decode(&nodes); err != nil {
		return nil, err
	}
	return nodes, nil
}

func (c *client) getNodeId(user, node string) (string, error) {
	nodes, err := c.getUserNodes(user)
	if err != nil {
		return "", err
	}
	for _, n := range nodes {
		if n.Name == node {
			return strconv.Itoa(n.Id), nil
		}
	}
	return "", ErrorNotFound
}

func (c *client) getNodeAddresses(user, node string) ([]net.IP, error) {
	cmd := exec.Command("headscale", c.config, "--user", user, "node", "list", "-o", "json")
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}
	var nodes []nodeInfo
	if err := json.NewDecoder(bytes.NewReader(out)).Decode(&nodes); err != nil {
		return nil, err
	}
	for _, n := range nodes {
		if n.Name == node {
			return n.IPAddresses, nil
		}
	}
	return nil, ErrorNotFound
}

func extractLastLine(s string) (string, error) {
	items := strings.Split(s, "\n")
	for i := len(items) - 1; i >= 0; i-- {
		t := strings.TrimSpace(items[i])
		if t != "" {
			return t, nil
		}
	}
	return "", fmt.Errorf("All lines are empty")
}
