package openvpn

import (
	"bytes"
	"fmt"
	"strconv"
)

var eventSep = []byte(":")
var fieldSep = []byte(",")
var byteCountEventKW = []byte("BYTECOUNT")
var byteCountCliEventKW = []byte("BYTECOUNT_CLI")
var clientEventKW = []byte("CLIENT")
var echoEventKW = []byte("ECHO")
var fatalEventKW = []byte("FATAL")
var holdEventKW = []byte("HOLD")
var infoEventKW = []byte("INFO")
var logEventKW = []byte("LOG")
var needOkEventKW = []byte("NEED-OK")
var needStrEventKW = []byte("NEED-STR")
var passwordEventKW = []byte("PASSWORD")
var stateEventKW = []byte("STATE")

type Event interface {
	String() string
}

// UnknownEvent represents an event of a type that this package doesn't
// know about.
//
// Future versions of this library may learn about new event types, so a
// caller should exercise caution when making use of events of this type
// to access unsupported behavior. Backward-compatibility is *not*
// guaranteed for events of this type.
type UnknownEvent struct {
	keyword []byte
	body    []byte
}

func (e *UnknownEvent) Type() string {
	return string(e.keyword)
}

func (e *UnknownEvent) Body() string {
	return string(e.body)
}

func (e *UnknownEvent) String() string {
	return fmt.Sprintf("%s: %s", e.keyword, e.body)
}

// MalformedEvent represents a message from the OpenVPN process that is
// presented as an event but does not comply with the expected event syntax.
//
// Events of this type should never be seen but robust callers will accept
// and ignore them, possibly generating some kind of debugging message.
//
// One reason for potentially seeing events of this type is when the target
// program is actually not an OpenVPN process at all, but in fact this client
// has been connected to a different sort of server by mistake.
type MalformedEvent struct {
	raw []byte
}

func (e *MalformedEvent) String() string {
	return fmt.Sprintf("Malformed Event %q", e.raw)
}

// HoldEvent is a notification that the OpenVPN process is in a management
// hold and will not continue connecting until the hold is released, e.g.
// by calling client.HoldRelease()
type HoldEvent struct {
	body []byte
}

func (e *HoldEvent) String() string {
	return string(e.body)
}

// StateEvent is a notification of a change of connection state. It can be
// used, for example, to detect if the OpenVPN connection has been interrupted
// and the OpenVPN process is attempting to reconnect.
type StateEvent struct {
	body []byte

	// bodyParts is populated only on first request, giving us the
	// separate comma-separated elements of the message. Not all
	// fields are populated for all states.
	bodyParts [][]byte
}

func (e *StateEvent) RawTimestamp() string {
	parts := e.parts()
	return string(parts[0])
}

func (e *StateEvent) NewState() string {
	parts := e.parts()
	return string(parts[1])
}

func (e *StateEvent) Description() string {
	parts := e.parts()
	return string(parts[2])
}

// LocalTunnelAddr returns the IP address of the local interface within
// the tunnel, as a string that can be parsed using net.ParseIP.
//
// This field is only populated for events whose NewState returns
// either ASSIGN_IP or CONNECTED.
func (e *StateEvent) LocalTunnelAddr() string {
	parts := e.parts()
	return string(parts[3])
}

// RemoteAddr returns the non-tunnel IP address of the remote
// system that has connected to the local OpenVPN process.
//
// This field is only populated for events whose NewState returns
// CONNECTED.
func (e *StateEvent) RemoteAddr() string {
	parts := e.parts()
	return string(parts[4])
}

func (e *StateEvent) String() string {
	newState := e.NewState()
	switch newState {
	case "ASSIGN_IP":
		return fmt.Sprintf("%s: %s", newState, e.LocalTunnelAddr())
	case "CONNECTED":
		return fmt.Sprintf("%s: %s", newState, e.RemoteAddr())
	default:
		desc := e.Description()
		if desc != "" {
			return fmt.Sprintf("%s: %s", newState, desc)
		} else {
			return newState
		}
	}
}

func (e *StateEvent) parts() [][]byte {
	if e.bodyParts == nil {
		// State messages currently have only five segments, but
		// we'll ask for 5 so any additional fields that might show
		// up in newer versions will gather in an element we're
		// not actually using.
		e.bodyParts = bytes.SplitN(e.body, fieldSep, 6)

		// Prevent crash if the server has sent us a malformed
		// status message. This should never actually happen if
		// the server is behaving itself.
		if len(e.bodyParts) < 5 {
			expanded := make([][]byte, 5)
			copy(expanded, e.bodyParts)
			e.bodyParts = expanded
		}
	}
	return e.bodyParts
}

// EchoEvent is emitted by an OpenVPN process running in client mode when
// an "echo" command is pushed to it by the server it has connected to.
//
// The format of the echo message is free-form, since this message type is
// intended to pass application-specific data from the server-side config
// into whatever client is consuming the management prototcol.
//
// This event is emitted only if the management client has turned on events
// of this type using client.SetEchoEvents(true)
type EchoEvent struct {
	body []byte
}

func (e *EchoEvent) RawTimestamp() string {
	sepIndex := bytes.Index(e.body, fieldSep)
	if sepIndex == -1 {
		return ""
	}
	return string(e.body[:sepIndex])
}

func (e *EchoEvent) Message() string {
	sepIndex := bytes.Index(e.body, fieldSep)
	if sepIndex == -1 {
		return ""
	}
	return string(e.body[sepIndex+1:])
}

func (e *EchoEvent) String() string {
	return fmt.Sprintf("ECHO: %s", e.Message())
}

// ByteCountEvent represents a periodic snapshot of data transfer in bytes
// on a VPN connection.
//
// For OpenVPN *servers*, events are emitted for each client and the method
// ClientId identifies thet client.
//
// For other OpenVPN modes, events are emitted only once per interval for the
// single connection managed by the target process, and ClientId returns
// the empty string.
type ByteCountEvent struct {
	hasClient bool
	body      []byte

	// populated on first call to parts()
	bodyParts [][]byte
}

func (e *ByteCountEvent) ClientId() string {
	if !e.hasClient {
		return ""
	}

	return string(e.parts()[0])
}

func (e *ByteCountEvent) BytesIn() int {
	index := 0
	if e.hasClient {
		index = 1
	}
	str := string(e.parts()[index])
	val, _ := strconv.Atoi(str)
	// Ignore error, since this should never happen if OpenVPN is
	// behaving itself.
	return val
}

func (e *ByteCountEvent) BytesOut() int {
	index := 1
	if e.hasClient {
		index = 2
	}
	str := string(e.parts()[index])
	val, _ := strconv.Atoi(str)
	// Ignore error, since this should never happen if OpenVPN is
	// behaving itself.
	return val
}

func (e *ByteCountEvent) String() string {
	if e.hasClient {
		return fmt.Sprintf("Client %s: %d in, %d out", e.ClientId(), e.BytesIn(), e.BytesOut())
	} else {
		return fmt.Sprintf("%d in, %d out", e.BytesIn(), e.BytesOut())
	}
}

func (e *ByteCountEvent) parts() [][]byte {
	if e.bodyParts == nil {
		e.bodyParts = bytes.SplitN(e.body, fieldSep, 4)

		wantCount := 2
		if e.hasClient {
			wantCount = 3
		}

		// Prevent crash if the server has sent us a malformed
		// message. This should never actually happen if the
		// server is behaving itself.
		if len(e.bodyParts) < wantCount {
			expanded := make([][]byte, wantCount)
			copy(expanded, e.bodyParts)
			e.bodyParts = expanded
		}
	}
	return e.bodyParts
}

func upgradeEvent(raw []byte) Event {
	splitIdx := bytes.Index(raw, eventSep)
	if splitIdx == -1 {
		// Should never happen, but we'll handle it robustly if it does.
		return &MalformedEvent{raw}
	}

	keyword := raw[:splitIdx]
	body := raw[splitIdx+1:]

	switch {
	case bytes.Equal(keyword, stateEventKW):
		return &StateEvent{body: body}
	case bytes.Equal(keyword, holdEventKW):
		return &HoldEvent{body}
	case bytes.Equal(keyword, echoEventKW):
		return &EchoEvent{body}
	case bytes.Equal(keyword, byteCountEventKW):
		return &ByteCountEvent{hasClient: false, body: body}
	case bytes.Equal(keyword, byteCountCliEventKW):
		return &ByteCountEvent{hasClient: true, body: body}
	default:
		return &UnknownEvent{keyword, body}
	}
}
