package asaModels

import (
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"

	"github.com/sirupsen/logrus"
)

var (
	aaaAuthenticationRE   = regexp.MustCompile(`(?i)aaa\s+authentication\s+(\S+)\s+(console)\s+(.*?)(\n|$)`)
	aaaAuthorizationRE    = regexp.MustCompile(`(?i)aaa\s+authorization\s+(\S+)\s+(.*?)(\n|$)`)
	aaaCommandRE          = regexp.MustCompile(`(?i)aaa\s+authorization\s+command\s+(.*?)(\n|$)`)
	aaaHostRE             = regexp.MustCompile(`aaa-server\s(\S+)\s\((\S+)\)\s(\S+)\s(\S+)(.|\n\s)*`)
	aaaServerRE           = regexp.MustCompile(`aaa-server\s(\S+)\sprotocol\s(\S+)(.|\n\s)*`)
	accountingModeRE      = regexp.MustCompile(`accounting-mode\s(\S+)`)
	accountingPortRE      = regexp.MustCompile(`accounting-port\s(\S+)`) // Radius Options:
	aclNetmaskConvertRE   = regexp.MustCompile(`acl-netmask-convert\s(\S+)`)
	adAgentModeRE         = regexp.MustCompile(`ad-agent-mode`)
	authenticationPortRE  = regexp.MustCompile(`authentication-port\s(\S+)`)
	groupSearchRE         = regexp.MustCompile(`group-search-timeout\s(\S+)`)
	intrmAcntngUpdtRE     = regexp.MustCompile(`interim-accounting-update`)
	keyRE                 = regexp.MustCompile(`key\s(\S+)`)
	ldapAttributeMapRE    = regexp.MustCompile(`ldap-attribute-map\s(\S+)`)
	ldapBaseDNRE          = regexp.MustCompile(`ldap-bas-dn\s(\S+)`)
	ldapGroupBaseDNRE     = regexp.MustCompile(`ldap-group-base-dn\s(\S+)`)
	ldapLoginDNRE         = regexp.MustCompile(`ldap-login-dn\s(\S+)`)
	ldapLoginPasswdRE     = regexp.MustCompile(`ldap-login-password\s(\S+)`)
	ldapNamingAttributeRE = regexp.MustCompile(`ldap-naming-attribute\s(\S+)`)
	ldapOverSSLRE         = regexp.MustCompile(`ldap-over-ssl`)
	ldapScopeRE           = regexp.MustCompile(`ldap_scope\s(\S+)`)
	maxFailedRE           = regexp.MustCompile(`max-failed-attempts\s(\S+)`)
	mergeDaclRE           = regexp.MustCompile(`merge-dacl\s(\S+)`)
	mschapv2CapableRE     = regexp.MustCompile(`mschapv2-capable`)
	proxyAuthMapRE        = regexp.MustCompile(`proxy-auth_map\s(\S+.*)`)
	radiusCommonPWRE      = regexp.MustCompile(`radius-common-pw\s(\S+)`)
	reactModeRE           = regexp.MustCompile(`reactivation-mode\s(timed|(depletion)\s(\S+)\s(\S+))`)
	retryInterval         = regexp.MustCompile(`retry-interval\s(\S+)`)
	saslMechanismRE       = regexp.MustCompile(`sasl_mechanism\s(\S+)`)
	serverPortRE          = regexp.MustCompile(`server-port\s(\S+)`)
	serverTypeRE          = regexp.MustCompile(`server-type\s(\S+)`) // LDAP Options
	timeOutRE             = regexp.MustCompile(`timeout\s(\S+)`)
)

type AAAServerGroups struct {
	Groups map[string]AAAServerGroup `json:"aaaServerGroups"`
}

type AAAServerGroup struct {
	Protocol string                     `json:"protocol"`
	Servers  map[string]AAAServerStatus `json:"servers"`
}

type AAAServerStatus struct {
	Port                   string `json:"port"`
	Status                 string `json:"status"`
	LastTransaction        string `json:"lastTransaction"`
	PendingRequests        int    `json:"pendingRequests"`
	AvgRoundTripTime       string `json:"avgRoundTripTime"`
	AuthenticationRequests int    `json:"authenticationRequests"`
	AuthorizationRequests  int    `json:"authorizationRequests"`
	AccountingRequests     int    `json:"accountingRequests"`
	Retransmissions        int    `json:"retransmissions"`
	Accepts                int    `json:"accepts"`
	Rejects                int    `json:"rejects"`
	Challenges             int    `json:"challenges"`
	MalformedResponses     int    `json:"malformedResponses"`
	BadAuthenticators      int    `json:"badAuthenticators"`
	Timeouts               int    `json:"timeouts"`
	UnrecognizedResponses  int    `json:"unrecognizedResponses"`
}

func (a *AAAServerGroups) Parse(raw string) error {
	a.Groups = make(map[string]AAAServerGroup)

	var curGrpName string
	var curSrvName string

	for i, line := range strings.Split(raw, "\n") {
		if line == "show aaa-server" {
			continue // skip echo of command we sent
		}

		if i == len(strings.Split(raw, "\n"))-1 {
			continue // skip the last line which is fw prompt
		}

		fields := strings.Fields(line)
		if len(fields) == 0 {
			continue // skip blank lines
		}
		if strings.HasPrefix(line, "Server Group:") {
			// New server entry begins here
			if _, exists := a.Groups[fields[2]]; !exists {
				newGroup := AAAServerGroup{
					Servers: make(map[string]AAAServerStatus),
				}
				a.Groups[fields[2]] = newGroup
				curGrpName = fields[2]
			}
			continue
		}
		if strings.HasPrefix(line, "Server Address:") {
			if _, exists := a.Groups[curGrpName].Servers[fields[2]]; !exists {
				a.Groups[curGrpName].Servers[fields[2]] = AAAServerStatus{}
				curSrvName = fields[2]
			}
			continue
		}
		if strings.HasPrefix(line, "Server Protocol:") {
			thisGroup, ok := a.Groups[curGrpName]
			if !ok {
				return errors.New("somehow we got to protocol line but group " + curGrpName + " doesn't exist")
			}
			thisGroup.Protocol = strings.Join(fields[2:], " ")
			a.Groups[curGrpName] = thisGroup
			continue
		}

		// From here on we're handling server details

		thisGroup, ok := a.Groups[curGrpName]
		if !ok {
			return errors.New("somehow we got here but group " + curGrpName + " doesn't exist")
		}
		thisServer, ok := thisGroup.Servers[curSrvName]
		if !ok {
			return errors.New("we got here but server " + curSrvName + " doesn't exist")
		}

		switch fields[0] {
		case "Server":
			switch fields[1] {
			case "port:":
				thisServer.Port = fields[2]
			case "status:":
				thisServer.Status = fields[2]
				if strings.HasSuffix(thisServer.Status, ",") {
					// delete trailing comma
					thisServer.Status = thisServer.Status[0 : len(thisServer.Status)-1]
				}
				thisServer.LastTransaction = strings.Join(fields[6:], " ")
			}
		case "Average":
			switch fields[1] {
			case "round":
				switch fields[2] {
				case "trip":
					switch fields[3] {
					case "time":
						thisServer.AvgRoundTripTime = fields[4]
					}
				}
			}
		case "Number":
			switch fields[1] {
			case "of":
				switch fields[2] {
				case "unrecognized":
					switch fields[3] {
					case "responses":
						i, err := strconv.Atoi(fields[4])
						if err != nil {
							return err
						}
						thisServer.UnrecognizedResponses = i
					}
				case "timeouts":
					i, err := strconv.Atoi(fields[3])
					if err != nil {
						return err
					}
					thisServer.Timeouts = i
				case "bad":
					switch fields[3] {
					case "authenticators":
						i, err := strconv.Atoi(fields[4])
						if err != nil {
							return err
						}
						thisServer.BadAuthenticators = i
					}
				case "malformed":
					switch fields[3] {
					case "responses":
						i, err := strconv.Atoi(fields[4])
						if err != nil {
							return err
						}
						thisServer.MalformedResponses = i
					}
				case "challenges":
					i, err := strconv.Atoi(fields[3])
					if err != nil {
						return err
					}
					thisServer.Challenges = i
				case "accepts":
					i, err := strconv.Atoi(fields[3])
					if err != nil {
						return err
					}
					thisServer.Accepts = i
				case "rejects":
					i, err := strconv.Atoi(fields[3])
					if err != nil {
						return err
					}
					thisServer.Rejects = i
				case "retransmissions":
					i, err := strconv.Atoi(fields[3])
					if err != nil {
						return err
					}
					thisServer.Retransmissions = i
				case "accounting":
					switch fields[3] {
					case "requests":
						i, err := strconv.Atoi(fields[4])
						if err != nil {
							return err
						}
						thisServer.AccountingRequests = i
					}
				case "authorization":
					switch fields[3] {
					case "requests":
						i, err := strconv.Atoi(fields[4])
						if err != nil {
							return err
						}
						thisServer.AuthorizationRequests = i
					}
				case "authentication":
					switch fields[3] {
					case "requests":
						i, err := strconv.Atoi(fields[4])
						if err != nil {
							return err
						}
						thisServer.AuthenticationRequests = i
					}
				case "pending":
					switch fields[3] {
					case "requests":
						i, err := strconv.Atoi(fields[4])
						if err != nil {
							return err
						}
						thisServer.PendingRequests = i
					}
				}
			}
		default:
			logrus.WithField("line", line).Warn("Unrecognized line in aaa-server output")
		}
		thisGroup.Servers[curSrvName] = thisServer
		a.Groups[curGrpName] = thisGroup
	}
	return nil
}

type (
	AAAGroups struct {
		Groups []*AAAGroup `json:"aaa_server_groups"`
	}

	AAAGroup struct {
		Name     string           `json:"name"`
		Protocol string           `json:"protocol"`
		Options  *AAAGroupOptions `json:"options"`
		Hosts    []*AAAHost       `json:"hosts"`
	}

	AAAHost struct {
		Interface string          `json:"interface"`
		Address   string          `json:"address"`
		Options   *AAAHostOptions `json:"options"`
	}

	AAAHostOptions struct {
		// General Options
		Timeout *int `json:"timeout"`
		// Key is the shared secret key
		Key                *string `json:"key"`
		GroupSearchTimeout *int    `json:"group_search_timeout,omitempty"`
		ProxyAuthMap       *string `json:"proxy_auth_map,omitempty"`
		ServerPort         *int    `json:"server_port,omitempty"`
		// ldap options
		ServerType          *string `json:"server_type,omitempty"`
		LdapAttributeMap    *string `json:"ldap_attribute_map,omitempty"`
		LdapBaseDn          *string `json:"ldap_base_dn,omitempty"`
		LdapGroupBaseDn     *string `json:"ldap_group_base_dn,omitempty"`
		LdapLoginDn         *string `json:"ldap_login_dn,omitempty"`
		LdapLoginPassword   *string `json:"ldap_login_password,omitempty"`
		LdapNamingAttribute *string `json:"ldap_naming_attribute,omitempty"`
		LdapOverSSL         bool    `json:"ldap_over_ssl,omitempty"`
		LdapScope           *string `json:"ldap_scope,omitempty"`
		SaslMechanism       *string `json:"sasl_mechanism,omitempty"`
		// Radius Options
		AccountingPort     *int    `json:"accounting_port,omitempty"`
		ACLNetmaskConvert  *string `json:"acl_netmask_convert,omitempty"`
		AuthenticationPort *int    `json:"authentication_port,omitempty"`
		Mschapv2Capable    bool    `json:"mschapv2_capable,omitempty"`
		RadiusCommonPw     *string `json:"radius_common_pw,omitempty"`
		RetryInterval      *int    `json:"retry_interval,omitempty"`
	}

	AAAGroupOptions struct {
		// General Options
		ReactivationMode  *ReactMode `json:"reactivation_mode,omitempty"`
		MaxFailedAttempts *int       `json:"max_failed_attempts,omitempty"`
		AccountingMode    *string    `json:"accounting_mode,omitempty"`
		// Radius Options
		MergeDacl       *string `json:"merge_dacl,omitempty"`
		ADAgentMode     bool    `json:"ad_agent_mode,omitempty"`
		IntrmAcntngUpdt bool    `json:"interim_accounting_update,omitempty"`
	}

	ReactMode struct {
		Mode     string `json:"mode"`
		Deadtime int    `json:"deadtime,omitempty"`
	}

	AAAConfig struct {
		Authentication []*AAAType `json:"authentication"`
		Authorization  []*AAAType `json:"authorization"`
		CommandAuth    []string   `json:"command_authorization"`
	}

	AAAType struct {
		Name    *string  `json:"name"`
		Type    *string  `json:"type,omitempty"`
		Options []string `json:"options"`
	}
)

func (a AAAGroups) Exists(g string) bool {
	if a.Groups == nil {
		return false
	}
	for _, o := range a.Groups {
		if o.Name == g {
			return true
		}
	}
	return false
}

func (a AAAGroups) Index(n string) (int, error) {
	if a.Groups == nil {
		return -1, errors.New("No groups in configuration")
	}
	for i, e := range a.Groups {
		if e.Name == n {
			return i, nil
		}
	}
	return -1, errors.New("Group not found")
}

// Parse takes the raw lines from the firewall and passses them to the appropriate sub-parser
func (a *AAAGroups) Parse(s string) {
	// Parse our Groups
	for _, v := range aaaServerRE.FindAllStringSubmatch(s, -1) {
		// 0 = complete match (multiline)
		// 1 = name
		// 2 = protocol
		var server AAAGroup
		server.ParseGroup(&v[0], &v[1], &v[2])
		a.Groups = append(a.Groups, &server)
	}

	// Parse the hosts in the groups
	for _, v := range aaaHostRE.FindAllStringSubmatch(s, -1) {
		// 0 = complete match (multiline)
		// 1 = name
		// 2 = interface
		// 3 = "host"
		// 4 = address
		for _, group := range a.Groups {
			if v[1] == group.Name {
				var newHost AAAHost
				newHost.ParseHost(&v[0], &v[1], &v[2], &v[4], &group.Protocol)
				group.Hosts = append(group.Hosts, &newHost)
			}
		}
	}
}

// ParseACSKeys takes input from a more:system-running, parses it, and add's the key to the configuration
func (a *AAAGroups) ParseACSKeys(s string) error {
	res := regexp.MustCompile(`aaa-server (\S+) \((\S+)\) host (\S+)\n timeout (\d+)\n key (\S+)`).FindAllStringSubmatch(s, -1)
	for _, host := range res {
		found := false
		for _, group := range a.Groups {
			for _, existingHost := range group.Hosts {
				if existingHost.Address == host[3] && group.Name == host[1] {
					found = true
					existingHost.Options.Key = &host[5]
				}
			}
		}
		if !found {
			return fmt.Errorf("Unable to find existing entry for aaa-server %s-%s", host[1], host[3])
		}
	}
	return nil
}

// ParseHost takes in raw lines from the firewall and parses it in to the AAAGroup structure
func (a *AAAGroup) ParseGroup(raw *string, name *string, protocol *string) {
	var o AAAGroupOptions
	a.Name = *name
	a.Protocol = *protocol
	// Start with no host options
	optionsPresent := false
	for i, l := range strings.Split(*raw, "\n") {
		if i == 0 {
			// Skip first line
			continue
		}
		search := mergeDaclRE.FindStringSubmatch(l)
		if len(search) > 0 {
			// merge-dacl found
			optionsPresent = true
			o.MergeDacl = &search[1]
		}
		search = reactModeRE.FindStringSubmatch(l)
		if len(search) > 0 {
			// reactivation-mode found
			optionsPresent = true
			var r ReactMode
			if search[2] == "depletion" {
				r.Mode = search[2]
				in, _ := strconv.Atoi(search[4])
				r.Deadtime = in
			} else {
				r.Mode = search[1]
			}
			o.ReactivationMode = &r
		}
		search = maxFailedRE.FindStringSubmatch(l)
		if len(search) > 0 {
			// max-failed-attempts found
			optionsPresent = true
			in, _ := strconv.Atoi(search[1])
			o.MaxFailedAttempts = &in
		}
		search = accountingModeRE.FindStringSubmatch(l)
		if len(search) > 0 {
			// accounting-mode found
			optionsPresent = true
			o.AccountingMode = &search[1]
		}

		if adAgentModeRE.MatchString(l) {
			// ad-agent-mode found
			optionsPresent = true
			o.ADAgentMode = true
		}

		if intrmAcntngUpdtRE.MatchString(l) {
			// interim-accounting-update found
			optionsPresent = true
			o.IntrmAcntngUpdt = true
		}
	}
	if optionsPresent {
		a.Options = &o
	}

}

// ParseHost takes in raw lines from the firewall and parses it in to the AAAHost structure
func (h *AAAHost) ParseHost(raw *string, name *string, int *string, address *string, prot *string) error {
	// Not doing a more:system-running, so can't get the key... As of 1/12/2016 there's no need for it either
	h.Address = *address
	h.Interface = *int
	var options AAAHostOptions
	o := false
	search := timeOutRE.FindStringSubmatch(*raw)
	if search != nil {
		conv, err := strconv.Atoi(search[1])
		if err != nil {
			return err
		}
		o = true
		options.Timeout = &conv
	}
	search = groupSearchRE.FindStringSubmatch(*raw)
	if search != nil {
		// Valid 1-300
		conv, err := strconv.Atoi(search[1])
		if err != nil {
			return err
		}
		o = true
		options.GroupSearchTimeout = &conv
	}
	search = proxyAuthMapRE.FindStringSubmatch(*raw)
	if search != nil {
		o = true
		options.ProxyAuthMap = &search[1]
	}
	search = serverPortRE.FindStringSubmatch(*raw)
	if search != nil {
		conv, err := strconv.Atoi(search[1])
		if err != nil {
			return err
		}
		o = true
		options.ServerPort = &conv
	}
	search = keyRE.FindStringSubmatch(*raw)
	if search != nil {
		o = true
		options.Key = &search[1]
	}
	if *prot == "ldap" {
		search = serverTypeRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.ServerType = &search[1]
		}
		search = ldapAttributeMapRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.LdapAttributeMap = &search[1]
		}
		search = ldapBaseDNRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.LdapBaseDn = &search[1]
		}
		search = ldapGroupBaseDNRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.LdapGroupBaseDn = &search[1]
		}
		search = ldapLoginDNRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.LdapLoginDn = &search[1]
		}
		search = ldapLoginPasswdRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.LdapLoginPassword = &search[1]
		}
		search = ldapNamingAttributeRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.LdapNamingAttribute = &search[1]
		}
		if ldapOverSSLRE.MatchString(*raw) {
			o = true
			options.LdapOverSSL = true
		}
		search = ldapScopeRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.LdapScope = &search[1]
		}
		search = saslMechanismRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.SaslMechanism = &search[1]
		}
	}
	if *prot == "radius" {
		search = accountingPortRE.FindStringSubmatch(*raw)
		if search != nil {
			conv, err := strconv.Atoi(search[1])
			if err != nil {
				return err
			}
			o = true
			options.AccountingPort = &conv
		}
		search = aclNetmaskConvertRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.ACLNetmaskConvert = &search[1]
		}
		search = authenticationPortRE.FindStringSubmatch(*raw)
		if search != nil {
			conv, err := strconv.Atoi(search[1])
			if err != nil {
				return err
			}
			o = true
			options.AuthenticationPort = &conv
		}
		if mschapv2CapableRE.MatchString(*raw) {
			o = true
			options.Mschapv2Capable = true
		}
		search = radiusCommonPWRE.FindStringSubmatch(*raw)
		if search != nil {
			o = true
			options.RadiusCommonPw = &search[1]
		}
		search = retryInterval.FindStringSubmatch(*raw)
		if search != nil {
			conv, err := strconv.Atoi(search[1])
			if err != nil {
				return err
			}
			o = true
			options.RetryInterval = &conv
		}
	}
	if o {
		h.Options = &options
	}
	return nil
}

func (a *AAAConfig) Parse(s string) {
	for _, i := range aaaAuthenticationRE.FindAllStringSubmatch(s, -1) {
		// sp := strings.Split(i[3], " ")
		a.Authentication = append(a.Authentication, &AAAType{
			Name:    &i[1],
			Type:    &i[2],
			Options: strings.Split(i[3], " "),
		})
	}

	for _, i := range aaaAuthorizationRE.FindAllStringSubmatch(s, -1) {
		if i[1] != "command" {
			i[2] = strings.TrimSpace(i[2])
			a.Authorization = append(a.Authorization, &AAAType{
				Name:    &i[1],
				Options: strings.Split(i[2], " "),
			})
		}
	}

	for _, i := range aaaCommandRE.FindAllStringSubmatch(s, -1) {
		i[1] = strings.TrimSpace(i[1])
		a.CommandAuth = strings.Split(i[1], " ")
	}

}
