package grs

import (
	"errors"
	"fmt"
	"log"
	"net"
	"net/url"
	"regexp"
	"strings"
	"time"

	napping "gopkg.in/jmcvetta/napping.v3"
)

const (
	// GrsURL contains the RIPE REST API URL
	GrsURL = "http://rest.db.ripe.net/search.json"
)

// GRS contains whois data from the ripe API
type GRS struct {
	//ID           bson.ObjectId `bson:"_id,omitempty" json:"id"`
	ID           string       `bson:"_id" json:"_id"`
	Source       string       `bson:"source" json:"source"`
	IpFrom       string       `bson:"ip_from" json:"ip_from"`
	IpTo         string       `bson:"ip_to" json:"ip_to"`
	IpFromRaw    []byte       `bson:"ip_from_raw" json:"ip_from_raw"`
	IpToRaw      []byte       `bson:"ip_to_raw" json:"ip_to_raw"`
	Description  string       `bson:"description" json:"description"`
	Country      string       `bson:"country" json:"country"`
	Status       string       `bson:"status" json:"status"`
	Cidr         string       `bson:"cidr" json:"cidr"`
	NetMaskBits  int          `bson:"netmask_bits" json:"netmask_bits"`
	Name         string       `bson:"name" json:"name"`
	Org          string       `bson:"org" json:"org"`
	ASN          string       `bson:"asn" json:"asn"`
	CreatedAt    time.Time    `bson:"created_at" json:"created_at"`
	ModifiedAt   time.Time    `bson:"modified_at" json:"modified_at"`
	Organization Organization `bson:"organization" json:"organization"`
}

// GRSRaw contains the raw GRS response from the ripe API server
type GRSRaw struct {
	Service       Name          `json:"service"`
	Parameters    Parameters    `json:"parameters"`
	Objects       Objects       `json:"objects"`
	Terms         Terms         `json:"terms-and-conditions"`
	ErrorMessages ErrorMessages `json:"errormessages"`
}

// Name contains the GRS name
type Name struct {
	Name string `json:"name"`
}

// Parameters contains all API query parameters
type Parameters struct {
	InverseLookup map[string]interface{} `json:"inverse-lookup"`
	TypeFilters   map[string]interface{} `json:"type-filters"`
	Flags         map[string]interface{} `json:"flags"`
	QueryStrings  QueryString            `json:"query-strings"`
	Sources       Sources                `json:"sources"`
}

// QueryString contains a list of query-strings for API requests
type QueryString struct {
	QueryString []Value `json:"query-string"`
}

// Value contains a GRS value
type Value struct {
	Value string `json:"value"`
}

type Sources struct {
	Ids []Id `json:"source"`
}

type AutNum struct {
	Source       string
	ID           string
	Name         string
	Org          string
	AdminContact string
	TechContact  string
	Status       string
	Notify       []string
	Maintainer   []string
	Created      time.Time
	Modified     time.Time
	Organization Organization
}

type Organization struct {
	// ID           string
	Name         string
	Type         string
	Address      []string
	Phone        string
	Fax          string
	Email        string
	Organization string
	AbuseEmail   string
	Source       string
}

type Id struct {
	ID string `json:"id"`
}

type Objects struct {
	Objects []Object `json:"object"`
}

type Object struct {
	Type       string     `json:"type"`
	Link       Link       `json:"link"`
	Source     Id         `json:"source"`
	PrimaryKey Attributes `json:"primary-key"`
	Attributes Attributes `json:"attributes"`
	Tags       Tags       `json:"tags"`
}

type Link struct {
	Type string `json:"type"`
	Href string `json:"href"`
}

type Attributes struct {
	Attributes []Attribute `json:"attribute"`
}

type Attribute struct {
	Link           Link   `json:"link"`
	Name           string `json:"name"`
	Value          string `json:"value"`
	ReferencedType string `json:"referenced-type"`
}

type Tags struct {
	Tags []Tag `json:"tag"`
}

type Tag struct {
	ID   string `json:"id"`
	Data string `json:"data"`
}

type Terms struct {
	Type string `json:"type"`
	Href string `json:"href"`
}

type ErrorMessages struct {
	ErrorMessage []ErrorMessage `json:"errormessage"`
}

type ErrorMessage struct {
	Severity string `json:"severity"`
	Text     string `json:"text"`
}

// LookupAutNum looks up the Autonomous System Number (ASN) using the RIPE API and returns an AutNum object
func LookupAutNum(id string) (AutNum, error) {
	asID := id
	asID = strings.TrimPrefix(asID, "MNT-")
	asID = strings.TrimPrefix(asID, "AS")
	asID = fmt.Sprintf("AS%s", id)

	args := url.Values{}
	args.Set("query-string", id)
	args.Add("type-filter", "aut-num")
	args.Add("source", "ripe-grs")
	args.Add("source", "arin-grs")
	args.Add("source", "apnic-grs")
	args.Add("source", "lacnic-grs")
	args.Add("source", "afrinic-grs")

	var data GRSRaw

	resp, err := napping.Get(GrsURL, &args, &data, nil)

	if err != nil {
		return AutNum{}, errors.New(fmt.Sprint("Failed to load data from ", resp.Url, ": ", err))
	}

	if resp.Status() != 200 {
		return AutNum{}, errors.New(fmt.Sprint("Failed to load data from GRS server: ", resp.Status()))
	}

	if len(data.ErrorMessages.ErrorMessage) > 0 {
		return AutNum{}, errors.New(fmt.Sprint("No GRS data for ", id, ": ", data.ErrorMessages.ErrorMessage[0].Text))
	}
	if err != nil {
		return AutNum{}, err
	}

	var aut AutNum

	var autnum *Object
	var org *Object

	ymdRegexp := regexp.MustCompile(`^\d+$`)

	for i, e := range data.Objects.Objects {
		switch e.Type {
		case "aut-num":
			autnum = &data.Objects.Objects[i]
		case "organisation":
			org = &data.Objects.Objects[i]
		}
	}

	if autnum != nil {
		for _, e := range autnum.Attributes.Attributes {
			switch e.Name {
			case "aut-num":
				aut.ID = e.Value
			case "as-name":
				aut.Name = e.Value
			case "org":
				aut.Org = e.Value
			case "admin-c":
				aut.AdminContact = e.Value
			case "tech-c":
				aut.TechContact = e.Value
			case "status":
				aut.Status = e.Value
			case "mnt-by":
				aut.Maintainer = append(aut.Maintainer, e.Value)
			case "source":
				aut.Source = e.Value
			case "created":
				if ymdRegexp.MatchString(e.Value) {
					t, err := time.Parse("20060102", e.Value)
					if err != nil {
						log.Printf("%s: %s\n", e.Value, err)
						break
					}
					aut.Created = t
					break
				}

				t, err := time.Parse(time.RFC3339, e.Value)
				if err != nil {
					log.Printf("%s: %s\n", e.Value, err)
					break
				}
				aut.Created = t
			case "last-modified":
				if ymdRegexp.MatchString(e.Value) {
					t, err := time.Parse("20060102", e.Value)
					if err != nil {
						log.Printf("%s: %s\n", e.Value, err)
						break
					}
					aut.Modified = t
					break
				}

				t, err := time.Parse(time.RFC3339, e.Value)
				if err != nil {
					log.Printf("%s: %s\n", e.Value, err)
					break
				}
				aut.Modified = t
			}
		}
	}

	if org != nil {
		o := Organization{}

		for _, e := range org.Attributes.Attributes {
			switch e.Name {
			case "organisation":
				o.Organization = e.Value
			case "org-name":
				o.Name = e.Value
			case "org-type":
				o.Type = e.Value
			case "address":
				o.Address = append(o.Address, e.Value)
			case "phone":
				o.Phone = e.Value
			case "abuse-mailbox":
				o.AbuseEmail = e.Value
			case "e-mail":
				o.Email = e.Value
			}
		}

		aut.Organization = o
	}

	return aut, nil
}

// Lookup loads GRS data from the RIPE REST API and returns a GRS object
func Lookup(ip string) (GRS, error) {
	// var data map[string]interface{}
	var data GRSRaw

	args := url.Values{}
	args.Set("query-string", ip)
	args.Add("source", "ripe-grs")
	args.Add("source", "arin-grs")
	args.Add("source", "apnic-grs")
	args.Add("source", "lacnic-grs")
	args.Add("source", "afrinic-grs")

	resp, err := napping.Get(GrsURL, &args, &data, nil)

	if err != nil {
		return GRS{}, errors.New(fmt.Sprint("Failed to load data from ", resp.Url, ": ", err))
	}

	if resp.Status() != 200 {
		return GRS{}, errors.New(fmt.Sprint("Failed to load data from GRS server: ", resp.Status()))
	}

	if len(data.ErrorMessages.ErrorMessage) > 0 {
		return GRS{}, errors.New(fmt.Sprint("No GRS data for ", ip, ": ", data.ErrorMessages.ErrorMessage[0].Text))
	}

	grs := GRS{}
	var grs_inetnum *Object
	var grs_route *Object
	var grs_org *Object
	var inetnum string

	ymdRegexp := regexp.MustCompile(`^\d+$`)

	for i, e := range data.Objects.Objects {
		switch e.Type {
		case "inetnum":
			grs_inetnum = &data.Objects.Objects[i]
		case "inet6num":
			grs_inetnum = &data.Objects.Objects[i]
		case "route":
			grs_route = &data.Objects.Objects[i]
		case "organisation":
			grs_org = &data.Objects.Objects[i]
		}
	}

	// Inetnum fields
	//
	if grs_inetnum != nil {
		for _, e := range grs_inetnum.Attributes.Attributes {
			switch e.Name {
			case "inetnum":
				inetnum = e.Value
			case "inet6num":
				inetnum = e.Value
			case "netname":
				grs.Name = e.Value
			case "descr":
				grs.Description = e.Value
			case "country":
				grs.Country = e.Value
			case "status":
				grs.Status = e.Value
			case "org":
				grs.Org = e.Value
			case "source":
				grs.Source = e.Value
			case "created":
				if ymdRegexp.MatchString(e.Value) {
					t, err := time.Parse("20060102", e.Value)
					if err != nil {
						log.Printf("%s: %s\n", e.Value, err)
						break
					}
					grs.CreatedAt = t
					break
				}

				t, err := time.Parse(time.RFC3339, e.Value)
				if err != nil {
					log.Printf("%s: %s\n", e.Value, err)
					break
				}
				grs.CreatedAt = t
			case "last-modified":
				if ymdRegexp.MatchString(e.Value) {
					t, err := time.Parse("20060102", e.Value)
					if err != nil {
						log.Printf("%s: %s\n", e.Value, err)
						break
					}
					grs.ModifiedAt = t
					break
				}

				t, err := time.Parse(time.RFC3339, e.Value)
				if err != nil {
					log.Printf("%s: %s\n", e.Value, err)
					break
				}
				grs.ModifiedAt = t
			}
		}
	}

	// Route fields
	//
	if grs_route != nil {
		for _, e := range grs_route.Attributes.Attributes {
			switch e.Name {
			//case "route":
			//	grs.Cidr = e.Value
			case "descr":
				grs.Description = e.Value
			case "origin":
				grs.ASN = e.Value
			}
		}
	}

	var ipFrom net.IP
	var ipTo net.IP

	if inetnum != "" {

		_, network, err := net.ParseCIDR(inetnum)

		if err == nil {
			ipFrom, ipTo = networkRange(network)
			grs.IpFrom = ipFrom.String()
			grs.IpTo = ipTo.String()
			if grs.Cidr == "" {
				grs.Cidr = network.String()

				_, numBits := network.Mask.Size()
				grs.NetMaskBits = numBits
			}
		} else {
			reg := regexp.MustCompile(`^\s*(\d+\.\d+\.\d+\.\d+)\s*-\s*(\d+\.\d+\.\d+\.\d+)\s*$`)
			matches := reg.FindAllStringSubmatch(inetnum, -1)

			if matches != nil && matches[0] != nil && matches[0][1] != "" && matches[0][2] != "" {
				grs.IpFrom = matches[0][1]
				grs.IpTo = matches[0][2]

				ipFrom = net.ParseIP(grs.IpFrom)
				ipTo = net.ParseIP(grs.IpTo)

				_, masklen := cidr(ipFrom, ipTo)

				grs.NetMaskBits = masklen
				grs.Cidr = fmt.Sprintf("%s/%d", grs.IpFrom, masklen)

			}

		}

		grs.IpFromRaw = ipFrom.To16()
		grs.IpToRaw = ipTo.To16()
	}

	if grs_org != nil {
		org := Organization{}
		for _, e := range grs_org.Attributes.Attributes {
			switch e.Name {
			case "organisation":
				org.Organization = e.Value
			case "org-name":
				org.Name = e.Value
			case "org-type":
				org.Type = e.Value
			case "address":
				org.Address = append(org.Address, e.Value)
			case "e-mail":
				org.Email = e.Value
			case "abuse-mailbox":
				org.AbuseEmail = e.Value
			case "phone":
				org.Phone = e.Value
			case "fax":
				org.Fax = e.Value
			case "source":
				org.Source = e.Value
			}
		}
		grs.Organization = org
	}

	return grs, nil
}

// Calculates the first and last IP addresses in an IPNet
func networkRange(network *net.IPNet) (net.IP, net.IP) {
	netIP := network.IP.To16()
	firstIP := netIP.Mask(network.Mask)
	lastIP := net.ParseIP("::").To16()
	for i := 0; i < len(lastIP); i++ {
		lastIP[i] = netIP[i] | ^network.Mask[i]
	}
	return firstIP, lastIP
}

func cidr(first, last net.IP) (net.IPMask, int) {

	// Is the address IPv4?
	a := first.To4()
	b := last.To4()
	if a == nil || b == nil {
		a = first
		b = last
	}

	l := len(a)
	m := make([]byte, l)

	for i := 0; i < l; i++ {
		msk := a[i] ^ b[i]
		m[i] = ^msk
	}

	ipmask := net.IPMask(m)
	masklen, _ := ipmask.Size()

	return ipmask, masklen
}
