//go:generate ../../../tools/readme_config_includer/generator

package whois

import (
	_ "embed"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"time"

	"github.com/likexian/whois"
	"github.com/likexian/whois-parser"
	"golang.org/x/net/idna"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/plugins/inputs"
)

//go:embed sample.conf
var sampleConfig string

const maxDomainLength = 253

type Whois struct {
	Domains            []string        `toml:"domains"`
	Server             string          `toml:"server"`
	Timeout            config.Duration `toml:"timeout"`
	ReferralChainQuery bool            `toml:"referral_chain_query"`
	Log                telegraf.Logger `toml:"-"`

	client *whois.Client
}

func (*Whois) SampleConfig() string {
	return sampleConfig
}

func (w *Whois) Init() error {
	if len(w.Domains) == 0 {
		return errors.New("no domains configured")
	}

	if w.Timeout <= 0 {
		return errors.New("timeout has to be greater than zero")
	}

	w.client = whois.NewClient()
	w.client.SetTimeout(time.Duration(w.Timeout))
	w.client.SetDisableReferralChain(!w.ReferralChainQuery)

	if w.Server == "" {
		w.Server = "whois.iana.org"
	}

	return nil
}

var asciiDomainRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$`)

func isValidDomain(domain string) bool {
	if len(domain) > maxDomainLength {
		return false
	}

	// Handle standard ASCII domains
	if asciiDomainRegex.MatchString(domain) {
		return true
	}

	// Try to convert to Punycode (handles IDNs)
	p := idna.New(idna.MapForLookup(), idna.StrictDomainName(true))
	punycodeVersion, err := p.ToASCII(domain)
	if err != nil {
		return false
	}

	return asciiDomainRegex.MatchString(punycodeVersion)
}

func (w *Whois) Gather(acc telegraf.Accumulator) error {
	for _, domain := range w.Domains {
		if !isValidDomain(domain) {
			acc.AddError(fmt.Errorf("invalid domain format: %q", domain))
			continue
		}

		w.Log.Tracef("Fetching WHOIS data for %q using WHOIS server %q with timeout: %v", domain, w.Server, w.Timeout)

		// Fetch WHOIS raw data
		raw, err := w.client.Whois(domain, w.Server)
		if err != nil {
			acc.AddError(fmt.Errorf("whois query failed for %q: %w", domain, err))
			continue
		}

		// Parse WHOIS data using whois-parser
		data, err := whoisparser.Parse(raw)
		if err != nil {
			// Skip metric recording for these errors
			if errors.Is(err, whoisparser.ErrDomainDataInvalid) {
				acc.AddError(fmt.Errorf("whois parsing failed for %q: %w", domain, err))
				continue
			}

			var status string
			switch {
			case errors.Is(err, whoisparser.ErrNotFoundDomain):
				status = "not found"
			case errors.Is(err, whoisparser.ErrReservedDomain):
				status = "reserved"
			case errors.Is(err, whoisparser.ErrPremiumDomain):
				status = "premium"
			case errors.Is(err, whoisparser.ErrBlockedDomain):
				status = "blocked"
			case errors.Is(err, whoisparser.ErrDomainLimitExceed):
				status = "limit exceeded"
			default:
				status = "unknown"
			}

			acc.AddFields(
				"whois",
				map[string]interface{}{
					"error": err.Error(),
				},
				map[string]string{
					"domain": domain,
					"status": status,
				},
			)

			continue
		}

		// Extract expiration date
		var expirationTimestamp int64
		var expiry int64
		if data.Domain.ExpirationDateInTime != nil {
			expirationTimestamp = data.Domain.ExpirationDateInTime.Unix()

			// Calculate expiry in seconds
			expiry = int64(time.Until(*data.Domain.ExpirationDateInTime).Seconds())
		}

		// Extract creation date
		var creationTimestamp int64
		if data.Domain.CreatedDateInTime != nil {
			creationTimestamp = data.Domain.CreatedDateInTime.Unix()
		}

		// Extract updated date
		var updatedTimestamp int64
		if data.Domain.UpdatedDateInTime != nil {
			updatedTimestamp = data.Domain.UpdatedDateInTime.Unix()
		}

		// Extract registrar name (handle nil)
		registrar := "not set"
		if data.Registrar != nil {
			registrar = data.Registrar.Name
		}

		// Extract registrant name (handle nil)
		registrant := "not set"
		if data.Registrant != nil {
			registrant = data.Registrant.Name
		}

		// Extract status (handle empty)
		status := "unknown"
		if len(data.Domain.Status) > 0 {
			status = strings.Join(data.Domain.Status, ",")
		}

		// Add metrics
		fields := map[string]interface{}{
			"creation_timestamp":   creationTimestamp,
			"dnssec_enabled":       data.Domain.DNSSec,
			"expiration_timestamp": expirationTimestamp,
			"expiry":               expiry,
			"updated_timestamp":    updatedTimestamp,
			"registrar":            registrar,
			"registrant":           registrant,
			"name_servers":         strings.Join(data.Domain.NameServers, ","),
		}
		tags := map[string]string{
			"domain": domain,
			"status": status,
		}

		acc.AddFields("whois", fields, tags)
	}

	return nil
}

// Plugin registration
func init() {
	inputs.Add("whois", func() telegraf.Input {
		return &Whois{
			Timeout: config.Duration(30 * time.Second),
		}
	})
}
