package forwardproxy

import (
	"errors"
	"net"
	"strings"
)

// ACLRule describes an ACL rule.
type ACLRule struct {
	Subjects []string `json:"subjects,omitempty"`
	Allow    bool     `json:"allow,omitempty"`
}

type aclDecision uint8

const (
	aclDecisionAllow = iota
	aclDecisionDeny
	aclDecisionNoMatch
)

type aclRule interface {
	tryMatch(ip net.IP, domain string) aclDecision
}

type aclIPRule struct {
	net   net.IPNet
	allow bool
}

func (a *aclIPRule) tryMatch(ip net.IP, domain string) aclDecision {
	if !a.net.Contains(ip) {
		return aclDecisionNoMatch
	}
	if a.allow {
		return aclDecisionAllow
	}
	return aclDecisionDeny
}

type aclDomainRule struct {
	domain            string
	subdomainsAllowed bool
	allow             bool
}

func (a *aclDomainRule) tryMatch(ip net.IP, domain string) aclDecision {
	domain = strings.TrimPrefix(domain, ".")

	if domain == a.domain ||
		a.subdomainsAllowed && strings.HasSuffix(domain, "."+a.domain) {
		if a.allow {
			return aclDecisionAllow
		}
		return aclDecisionDeny
	}
	return aclDecisionNoMatch
}

type aclAllRule struct {
	allow bool
}

func (a *aclAllRule) tryMatch(ip net.IP, domain string) aclDecision {
	if a.allow {
		return aclDecisionAllow
	}
	return aclDecisionDeny
}

func newACLRule(ruleSubject string, allow bool) (aclRule, error) {
	if ruleSubject == "all" {
		return &aclAllRule{allow: allow}, nil
	}
	_, ipNet, err := net.ParseCIDR(ruleSubject)
	if err != nil {
		ip := net.ParseIP(ruleSubject)
		// support specifying just an IP
		if ip.To4() != nil {
			_, ipNet, err = net.ParseCIDR(ruleSubject + "/32")
		} else if ip.To16() != nil {
			_, ipNet, err = net.ParseCIDR(ruleSubject + "/128")
		}
	}
	if err == nil {
		return &aclIPRule{net: *ipNet, allow: allow}, nil
	}

	subdomainsAllowed := false
	if strings.HasPrefix(ruleSubject, `*.`) {
		subdomainsAllowed = true
		ruleSubject = ruleSubject[2:]
	}
	err = isValidDomainLite(ruleSubject)
	if err != nil {
		return nil, errors.New(ruleSubject + " could not be parsed as either IP, IP network, or domain: " + err.Error())
	}
	return &aclDomainRule{domain: ruleSubject, subdomainsAllowed: subdomainsAllowed, allow: allow}, nil
}

// isValidDomainLite shamelessly rejects non-LDH names. returns nil if domains seems valid
func isValidDomainLite(domain string) error {
	for i := 0; i < len(domain); i++ {
		c := domain[i]
		if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || '0' <= c && c <= '9' ||
			c == '-' || c == '.' {
			continue
		}
		return errors.New("character " + string(c) + " is not allowed")
	}
	sections := strings.Split(domain, ".")
	for _, s := range sections {
		if len(s) == 0 {
			return errors.New("empty section between dots in domain name or trailing dot")
		}
		if len(s) > 63 {
			return errors.New("domain name section is too long")
		}
	}
	return nil
}
