// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package cli

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"net/netip"
	"strings"
	"text/tabwriter"

	"github.com/peterbourgon/ff/v3/ffcli"
	"golang.org/x/net/dns/dnsmessage"
	"tailscale.com/cmd/tailscale/cli/jsonoutput"
)

var dnsQueryArgs struct {
	json bool
}

var dnsQueryCmd = &ffcli.Command{
	Name:       "query",
	ShortUsage: currentCLIName() + " dns query [--json] <name> [type]",
	Exec:       runDNSQuery,
	ShortHelp:  "Perform a DNS query",
	LongHelp: fmt.Sprintf(strings.TrimSpace(`
The '%s dns query' subcommand performs a DNS query for the specified name
using the internal DNS forwarder (100.100.100.100).

By default, the DNS query will request an A record. Specify the record type as
a second argument after the name (e.g. AAAA, CNAME, MX, NS, PTR, SRV, TXT).

The output also provides information about the resolver(s) used to resolve the
query.
`), currentCLIName()),
	FlagSet: (func() *flag.FlagSet {
		fs := newFlagSet("query")
		fs.BoolVar(&dnsQueryArgs.json, "json", false, "output in JSON format")
		return fs
	})(),
}

func runDNSQuery(ctx context.Context, args []string) error {
	if len(args) == 0 {
		return errors.New("missing required argument: name")
	}
	if len(args) > 1 {
		var flags []string
		for _, a := range args[1:] {
			if strings.HasPrefix(a, "-") {
				flags = append(flags, a)
			}
		}
		if len(flags) > 0 {
			return fmt.Errorf("unexpected flags after query name: %s; see '%s dns query --help'", strings.Join(flags, ", "), currentCLIName())
		}
		if len(args) > 2 {
			return fmt.Errorf("unexpected extra arguments: %s", strings.Join(args[2:], " "))
		}
	}
	name := args[0]
	queryType := "A"
	if len(args) > 1 {
		queryType = strings.ToUpper(args[1])
	}

	rawBytes, resolvers, err := localClient.QueryDNS(ctx, name, queryType)
	if err != nil {
		return fmt.Errorf("failed to query DNS: %w", err)
	}

	data := &jsonoutput.DNSQueryResult{
		Name:      name,
		QueryType: queryType,
	}

	for _, r := range resolvers {
		data.Resolvers = append(data.Resolvers, makeDNSResolverInfo(r))
	}

	var p dnsmessage.Parser
	header, err := p.Start(rawBytes)
	if err != nil {
		return fmt.Errorf("failed to parse DNS response: %w", err)
	}
	data.ResponseCode = header.RCode.String()

	p.SkipAllQuestions()

	if header.RCode == dnsmessage.RCodeSuccess {
		answers, err := p.AllAnswers()
		if err != nil {
			return fmt.Errorf("failed to parse DNS answers: %w", err)
		}
		data.Answers = make([]jsonoutput.DNSAnswer, 0, len(answers))
		for _, a := range answers {
			data.Answers = append(data.Answers, jsonoutput.DNSAnswer{
				Name:  a.Header.Name.String(),
				TTL:   a.Header.TTL,
				Class: a.Header.Class.String(),
				Type:  a.Header.Type.String(),
				Body:  makeAnswerBody(a),
			})
		}
	}

	if dnsQueryArgs.json {
		j, err := json.MarshalIndent(data, "", "  ")
		if err != nil {
			return err
		}
		printf("%s\n", j)
		return nil
	}
	printf("%s", formatDNSQueryText(data))
	return nil
}

func formatDNSQueryText(data *jsonoutput.DNSQueryResult) string {
	var sb strings.Builder

	fmt.Fprintf(&sb, "DNS query for %q (%s) using internal resolver:\n", data.Name, data.QueryType)
	fmt.Fprintf(&sb, "\n")
	if len(data.Resolvers) == 1 {
		fmt.Fprintf(&sb, "Forwarding to resolver: %v\n", formatResolverString(data.Resolvers[0]))
	} else {
		fmt.Fprintf(&sb, "Multiple resolvers available:\n")
		for _, r := range data.Resolvers {
			fmt.Fprintf(&sb, "  - %v\n", formatResolverString(r))
		}
	}
	fmt.Fprintf(&sb, "\n")
	fmt.Fprintf(&sb, "Response code: %v\n", data.ResponseCode)
	fmt.Fprintf(&sb, "\n")

	if data.Answers == nil {
		fmt.Fprintf(&sb, "No answers were returned.\n")
		return sb.String()
	}

	if len(data.Answers) == 0 {
		fmt.Fprintf(&sb, "  (no answers found)\n")
	}

	w := tabwriter.NewWriter(&sb, 0, 0, 2, ' ', 0)
	fmt.Fprintln(w, "Name\tTTL\tClass\tType\tBody")
	fmt.Fprintln(w, "----\t---\t-----\t----\t----")
	for _, a := range data.Answers {
		fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%s\n", a.Name, a.TTL, a.Class, a.Type, a.Body)
	}
	w.Flush()

	fmt.Fprintf(&sb, "\n")
	return sb.String()
}

// formatResolverString formats a jsonoutput.DNSResolverInfo for human-readable text output.
func formatResolverString(r jsonoutput.DNSResolverInfo) string {
	if len(r.BootstrapResolution) > 0 {
		return fmt.Sprintf("%s (bootstrap: %v)", r.Addr, r.BootstrapResolution)
	}
	return r.Addr
}

// makeAnswerBody returns a string with the DNS answer body in a human-readable format.
func makeAnswerBody(a dnsmessage.Resource) string {
	switch a.Header.Type {
	case dnsmessage.TypeA:
		return makeABody(a.Body)
	case dnsmessage.TypeAAAA:
		return makeAAAABody(a.Body)
	case dnsmessage.TypeCNAME:
		return makeCNAMEBody(a.Body)
	case dnsmessage.TypeMX:
		return makeMXBody(a.Body)
	case dnsmessage.TypeNS:
		return makeNSBody(a.Body)
	case dnsmessage.TypeOPT:
		return makeOPTBody(a.Body)
	case dnsmessage.TypePTR:
		return makePTRBody(a.Body)
	case dnsmessage.TypeSRV:
		return makeSRVBody(a.Body)
	case dnsmessage.TypeTXT:
		return makeTXTBody(a.Body)
	default:
		return a.Body.GoString()
	}
}

func makeABody(a dnsmessage.ResourceBody) string {
	if a, ok := a.(*dnsmessage.AResource); ok {
		return netip.AddrFrom4(a.A).String()
	}
	return ""
}
func makeAAAABody(aaaa dnsmessage.ResourceBody) string {
	if a, ok := aaaa.(*dnsmessage.AAAAResource); ok {
		return netip.AddrFrom16(a.AAAA).String()
	}
	return ""
}
func makeCNAMEBody(cname dnsmessage.ResourceBody) string {
	if c, ok := cname.(*dnsmessage.CNAMEResource); ok {
		return c.CNAME.String()
	}
	return ""
}
func makeMXBody(mx dnsmessage.ResourceBody) string {
	if m, ok := mx.(*dnsmessage.MXResource); ok {
		return fmt.Sprintf("%s (Priority=%d)", m.MX, m.Pref)
	}
	return ""
}
func makeNSBody(ns dnsmessage.ResourceBody) string {
	if n, ok := ns.(*dnsmessage.NSResource); ok {
		return n.NS.String()
	}
	return ""
}
func makeOPTBody(opt dnsmessage.ResourceBody) string {
	if o, ok := opt.(*dnsmessage.OPTResource); ok {
		return o.GoString()
	}
	return ""
}
func makePTRBody(ptr dnsmessage.ResourceBody) string {
	if p, ok := ptr.(*dnsmessage.PTRResource); ok {
		return p.PTR.String()
	}
	return ""
}
func makeSRVBody(srv dnsmessage.ResourceBody) string {
	if s, ok := srv.(*dnsmessage.SRVResource); ok {
		return fmt.Sprintf("Target=%s, Port=%d, Priority=%d, Weight=%d", s.Target.String(), s.Port, s.Priority, s.Weight)
	}
	return ""
}
func makeTXTBody(txt dnsmessage.ResourceBody) string {
	if t, ok := txt.(*dnsmessage.TXTResource); ok {
		return fmt.Sprintf("%q", t.TXT)
	}
	return ""
}
