/*
 * Copyright 2014-2020 Li Kexian
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Go module for domain whois information parsing
 * https://www.likexian.com/
 */

package whoisparser

import (
	"errors"
	"regexp"
	"strings"

	"github.com/likexian/gokit/assert"
	"github.com/likexian/gokit/xslice"
	"golang.org/x/net/idna"
)

// Domain info error and replacer variables
var (
	ErrDomainNotFound    = errors.New("Domain is not found.")
	ErrDomainInvalidData = errors.New("Domain whois data invalid.")
	ErrDomainLimitExceed = errors.New("Domain query limit exceeded.")
	ErrPremiumDomain     = errors.New("The domain is not registered, but available at a premium price.")
	ErrBlockedDomain     = errors.New("The domain name is blocked due to a DPML brand name block.")
)

// Version returns package version
func Version() string {
	return "1.15.1"
}

// Author returns package author
func Author() string {
	return "[Li Kexian](https://www.likexian.com/)"
}

// License returns package license
func License() string {
	return "Licensed under the Apache License 2.0"
}

// Parse returns parsed whois info
func Parse(text string) (whoisInfo WhoisInfo, err error) {
	name, extension := searchDomain(text)
	if name == "" {
		err = ErrDomainInvalidData
		if IsNotFound(text) {
			err = ErrDomainNotFound
		} else if IsPremiumDomain(text) {
			err = ErrPremiumDomain
		} else if IsBlockedDomain(text) {
			err = ErrBlockedDomain
		} else if IsLimitExceeded(text) {
			err = ErrDomainLimitExceed
		}
		return
	}

	domain := &Domain{}
	registrar := &Contact{}
	registrant := &Contact{}
	administrative := &Contact{}
	technical := &Contact{}
	billing := &Contact{}

	domain.Name, _ = idna.ToASCII(name)
	domain.Extension, _ = idna.ToASCII(extension)

	whoisText, _ := Prepare(text, domain.Extension)
	whoisLines := strings.Split(whoisText, "\n")
	for i := 0; i < len(whoisLines); i++ {
		line := strings.TrimSpace(whoisLines[i])
		if len(line) < 5 || !strings.Contains(line, ":") {
			continue
		}

		fChar := line[:1]
		if assert.IsContains([]string{"-", "*", "%", ">", ";"}, fChar) {
			continue
		}

		if line[len(line)-1:] == ":" {
			i += 1
			for ; i < len(whoisLines); i++ {
				thisLine := strings.TrimSpace(whoisLines[i])
				if strings.Contains(thisLine, ":") {
					break
				}
				line += thisLine + ","
			}
			line = strings.Trim(line, ",")
			i -= 1
		}

		lines := strings.SplitN(line, ":", 2)
		name := strings.TrimSpace(lines[0])
		value := strings.TrimSpace(lines[1])
		value = strings.TrimSpace(strings.Trim(value, ":"))

		if value == "" {
			continue
		}

		keyName := FindKeyName(name)
		switch keyName {
		case "domain_id":
			domain.ID = value
		case "domain_name":
			if domain.Domain == "" {
				domain.Domain = strings.ToLower(value)
				domain.Punycode, _ = idna.ToASCII(domain.Domain)
			}
		case "domain_status":
			domain.Status = append(domain.Status, strings.Split(value, ",")...)
		case "domain_dnssec":
			if !domain.DnsSec {
				domain.DnsSec = IsDnsSecEnabled(value)
			}
		case "whois_server":
			if domain.WhoisServer == "" {
				domain.WhoisServer = value
			}
		case "name_servers":
			domain.NameServers = append(domain.NameServers, strings.Split(value, ",")...)
		case "created_date":
			if domain.CreatedDate == "" {
				domain.CreatedDate = value
			}
		case "updated_date":
			if domain.UpdatedDate == "" {
				domain.UpdatedDate = value
			}
		case "expired_date":
			if domain.ExpirationDate == "" {
				domain.ExpirationDate = value
			}
		case "referral_url":
			registrar.ReferralURL = value
		default:
			name = ClearName(name)
			if !strings.Contains(name, " ") {
				name += " name"
			}
			ns := strings.SplitN(name, " ", 2)
			name = strings.TrimSpace("registrant " + ns[1])
			if ns[0] == "registrar" || ns[0] == "registration" {
				parseContact(registrar, name, value)
			} else if ns[0] == "registrant" || ns[0] == "holder" {
				parseContact(registrant, name, value)
			} else if ns[0] == "admin" || ns[0] == "administrative" {
				parseContact(administrative, name, value)
			} else if ns[0] == "tech" || ns[0] == "technical" {
				parseContact(technical, name, value)
			} else if ns[0] == "bill" || ns[0] == "billing" {
				parseContact(billing, name, value)
			}
		}
	}

	domain.NameServers = FixNameServers(domain.NameServers)
	domain.Status = FixDomainStatus(domain.Status)

	domain.NameServers = xslice.Unique(domain.NameServers).([]string)
	domain.Status = xslice.Unique(domain.Status).([]string)

	whoisInfo.Domain = domain
	if *registrar != (Contact{}) {
		whoisInfo.Registrar = registrar
	}

	if *registrant != (Contact{}) {
		whoisInfo.Registrant = registrant
	}

	if *administrative != (Contact{}) {
		whoisInfo.Administrative = administrative
	}

	if *technical != (Contact{}) {
		whoisInfo.Technical = technical
	}

	if *billing != (Contact{}) {
		whoisInfo.Billing = billing
	}

	return
}

// parseContact do parse contact info
func parseContact(contact *Contact, name, value string) {
	switch FindKeyName(name) {
	case "registrant_id":
		contact.ID = value
	case "registrant_name":
		contact.Name = value
	case "registrant_organization":
		contact.Organization = value
	case "registrant_street":
		if contact.Street == "" {
			contact.Street = value
		} else {
			contact.Street += ", " + value
		}
	case "registrant_city":
		contact.City = value
	case "registrant_state_province":
		contact.Province = value
	case "registrant_postal_code":
		contact.PostalCode = value
	case "registrant_country":
		contact.Country = value
	case "registrant_phone":
		contact.Phone = value
	case "registrant_phone_ext":
		contact.PhoneExt = value
	case "registrant_fax":
		contact.Fax = value
	case "registrant_fax_ext":
		contact.FaxExt = value
	case "registrant_email":
		contact.Email = strings.ToLower(value)
	}
}

// searchDomain find domain from whois info
func searchDomain(text string) (string, string) {
	r := regexp.MustCompile(`(?i)\[?domain(\s*\_?name)?\]?[\s\.]*\:?\s*([^\s]+)\.([^\.\s]{2,})`)
	m := r.FindStringSubmatch(text)
	if len(m) > 0 {
		return strings.ToLower(strings.TrimSpace(m[2])), strings.ToLower(strings.TrimSpace(m[3]))
	}

	r = regexp.MustCompile(`(?i)\[?domain(\s*\_?name)?\]?\s*\:?\s*([^\.\s]{2,})\n`)
	m = r.FindStringSubmatch(text)
	if len(m) > 0 {
		return strings.ToLower(strings.TrimSpace(m[2])), ""
	}

	return "", ""
}
