// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Methods for configuring a Cisco Switch 3500-series switch for team VLANs.

package network

import (
	"bufio"
	"bytes"
	"fmt"
	"github.com/Team254/cheesy-arena-lite/model"
	"net"
	"regexp"
	"strconv"
	"sync"
)

const switchTelnetPort = 23

const (
	red1Vlan  = 10
	red2Vlan  = 20
	red3Vlan  = 30
	blue1Vlan = 40
	blue2Vlan = 50
	blue3Vlan = 60
)

type Switch struct {
	address  string
	port     int
	password string
	mutex    sync.Mutex
}

var ServerIpAddress = "10.0.100.5" // The DS will try to connect to this address only.

func NewSwitch(address, password string) *Switch {
	return &Switch{address: address, port: switchTelnetPort, password: password}
}

// Sets up wired networks for the given set of teams.
func (sw *Switch) ConfigureTeamEthernet(teams [6]*model.Team) error {
	// Make sure multiple configurations aren't being set at the same time.
	sw.mutex.Lock()
	defer sw.mutex.Unlock()

	// Determine what new team VLANs are needed and build the commands to set them up.
	oldTeamVlans, err := sw.getTeamVlans()
	if err != nil {
		return err
	}
	addTeamVlansCommand := ""
	replaceTeamVlan := func(team *model.Team, vlan int) {
		if team == nil {
			return
		}
		if oldTeamVlans[team.Id] == vlan {
			delete(oldTeamVlans, team.Id)
		} else {
			addTeamVlansCommand += fmt.Sprintf(
				"ip dhcp excluded-address 10.%d.%d.1 10.%d.%d.100\n"+
					"no ip dhcp pool dhcp%d\n"+
					"ip dhcp pool dhcp%d\n"+
					"network 10.%d.%d.0 255.255.255.0\n"+
					"default-router 10.%d.%d.61\n"+
					"lease 7\n"+
					"no access-list 1%d\n"+
					"access-list 1%d permit ip 10.%d.%d.0 0.0.0.255 host %s\n"+
					"access-list 1%d permit udp any eq bootpc any eq bootps\n"+
					"interface Vlan%d\nip address 10.%d.%d.61 255.255.255.0\n",
				team.Id/100, team.Id%100, team.Id/100, team.Id%100, vlan, vlan, team.Id/100, team.Id%100, team.Id/100,
				team.Id%100, vlan, vlan, team.Id/100, team.Id%100, ServerIpAddress, vlan, vlan, team.Id/100,
				team.Id%100)
		}
	}
	replaceTeamVlan(teams[0], red1Vlan)
	replaceTeamVlan(teams[1], red2Vlan)
	replaceTeamVlan(teams[2], red3Vlan)
	replaceTeamVlan(teams[3], blue1Vlan)
	replaceTeamVlan(teams[4], blue2Vlan)
	replaceTeamVlan(teams[5], blue3Vlan)

	// Build the command to remove the team VLANs that are no longer needed.
	removeTeamVlansCommand := ""
	for _, vlan := range oldTeamVlans {
		removeTeamVlansCommand += fmt.Sprintf("interface Vlan%d\nno ip address\nno access-list 1%d\n", vlan, vlan)
	}

	// Build and run the overall command to do everything in a single telnet session.
	command := removeTeamVlansCommand + addTeamVlansCommand
	if len(command) > 0 {
		_, err = sw.runConfigCommand(removeTeamVlansCommand + addTeamVlansCommand)
		if err != nil {
			return err
		}
	}

	return nil
}

// Returns a map of currently-configured teams to VLANs.
func (sw *Switch) getTeamVlans() (map[int]int, error) {
	// Get the entire config dump.
	config, err := sw.runCommand("show running-config\n")
	if err != nil {
		return nil, err
	}

	// Parse out the team IDs and VLANs from the config dump.
	re := regexp.MustCompile("(?s)interface Vlan(\\d\\d)\\s+ip address 10\\.(\\d+)\\.(\\d+)\\.61")
	teamVlanMatches := re.FindAllStringSubmatch(config, -1)
	if teamVlanMatches == nil {
		// There are probably no teams currently configured.
		return nil, nil
	}

	// Build the map of team to VLAN.
	teamVlans := make(map[int]int)
	for _, match := range teamVlanMatches {
		team100s, _ := strconv.Atoi(match[2])
		team1s, _ := strconv.Atoi(match[3])
		team := int(team100s)*100 + team1s
		vlan, _ := strconv.Atoi(match[1])
		teamVlans[team] = vlan
	}
	return teamVlans, nil
}

// Logs into the switch via Telnet and runs the given command in user exec mode. Reads the output and
// returns it as a string.
func (sw *Switch) runCommand(command string) (string, error) {
	// Open a Telnet connection to the switch.
	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", sw.address, sw.port))
	if err != nil {
		return "", err
	}
	defer conn.Close()

	// Login to the AP, send the command, and log out all at once.
	writer := bufio.NewWriter(conn)
	_, err = writer.WriteString(fmt.Sprintf("%s\nenable\n%s\nterminal length 0\n%sexit\n", sw.password, sw.password,
		command))
	if err != nil {
		return "", err
	}
	err = writer.Flush()
	if err != nil {
		return "", err
	}

	// Read the response.
	var reader bytes.Buffer
	_, err = reader.ReadFrom(conn)
	if err != nil {
		return "", err
	}
	return reader.String(), nil
}

// Logs into the switch via Telnet and runs the given command in global configuration mode. Reads the output
// and returns it as a string.
func (sw *Switch) runConfigCommand(command string) (string, error) {
	return sw.runCommand(fmt.Sprintf("config terminal\n%send\ncopy running-config startup-config\n\n", command))
}
