// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package net

import (
	"context"
	"errors"
	"internal/bytealg"
	"internal/itoa"
	"io"
	"os"
)

// cgoAvailable set to true to indicate that the cgo resolver
// is available on Plan 9. Note that on Plan 9 the cgo resolver
// does not actually use cgo.
const cgoAvailable = true

func query(ctx context.Context, filename, query string, bufSize int) (addrs []string, err error) {
	queryAddrs := func() (addrs []string, err error) {
		file, err := os.OpenFile(filename, os.O_RDWR, 0)
		if err != nil {
			return nil, err
		}
		defer file.Close()

		_, err = file.Seek(0, io.SeekStart)
		if err != nil {
			return nil, err
		}
		_, err = file.WriteString(query)
		if err != nil {
			return nil, err
		}
		_, err = file.Seek(0, io.SeekStart)
		if err != nil {
			return nil, err
		}
		buf := make([]byte, bufSize)
		for {
			n, _ := file.Read(buf)
			if n <= 0 {
				break
			}
			addrs = append(addrs, string(buf[:n]))
		}
		return addrs, nil
	}

	type ret struct {
		addrs []string
		err   error
	}

	ch := make(chan ret, 1)
	go func() {
		addrs, err := queryAddrs()
		ch <- ret{addrs: addrs, err: err}
	}()

	select {
	case r := <-ch:
		return r.addrs, r.err
	case <-ctx.Done():
		return nil, &DNSError{
			Name:      query,
			Err:       ctx.Err().Error(),
			IsTimeout: ctx.Err() == context.DeadlineExceeded,
		}
	}
}

func queryCS(ctx context.Context, net, host, service string) (res []string, err error) {
	switch net {
	case "tcp4", "tcp6":
		net = "tcp"
	case "udp4", "udp6":
		net = "udp"
	}
	if host == "" {
		host = "*"
	}
	return query(ctx, netdir+"/cs", net+"!"+host+"!"+service, 128)
}

func queryCS1(ctx context.Context, net string, ip IP, port int) (clone, dest string, err error) {
	ips := "*"
	if len(ip) != 0 && !ip.IsUnspecified() {
		ips = ip.String()
	}
	lines, err := queryCS(ctx, net, ips, itoa.Itoa(port))
	if err != nil {
		return
	}
	f := getFields(lines[0])
	if len(f) < 2 {
		return "", "", errors.New("bad response from ndb/cs")
	}
	clone, dest = f[0], f[1]
	return
}

func queryDNS(ctx context.Context, addr string, typ string) (res []string, err error) {
	return query(ctx, netdir+"/dns", addr+" "+typ, 1024)
}

func handlePlan9DNSError(err error, name string) error {
	if stringsHasSuffix(err.Error(), "dns: name does not exist") ||
		stringsHasSuffix(err.Error(), "dns: resource does not exist; negrcode 0") ||
		stringsHasSuffix(err.Error(), "dns: resource does not exist; negrcode") {
		return &DNSError{
			Err:        errNoSuchHost.Error(),
			Name:       name,
			IsNotFound: true,
		}
	}
	return &DNSError{
		Err:  err.Error(),
		Name: name,
	}
}

// toLower returns a lower-case version of in. Restricting us to
// ASCII is sufficient to handle the IP protocol names and allow
// us to not depend on the strings and unicode packages.
func toLower(in string) string {
	for _, c := range in {
		if 'A' <= c && c <= 'Z' {
			// Has upper case; need to fix.
			out := []byte(in)
			for i := 0; i < len(in); i++ {
				c := in[i]
				if 'A' <= c && c <= 'Z' {
					c += 'a' - 'A'
				}
				out[i] = c
			}
			return string(out)
		}
	}
	return in
}

// lookupProtocol looks up IP protocol name and returns
// the corresponding protocol number.
func lookupProtocol(ctx context.Context, name string) (proto int, err error) {
	lines, err := query(ctx, netdir+"/cs", "!protocol="+toLower(name), 128)
	if err != nil {
		return 0, err
	}
	if len(lines) == 0 {
		return 0, UnknownNetworkError(name)
	}
	f := getFields(lines[0])
	if len(f) < 2 {
		return 0, UnknownNetworkError(name)
	}
	s := f[1]
	if n, _, ok := dtoi(s[bytealg.IndexByteString(s, '=')+1:]); ok {
		return n, nil
	}
	return 0, UnknownNetworkError(name)
}

func (*Resolver) lookupHost(ctx context.Context, host string) (addrs []string, err error) {
	// Use netdir/cs instead of netdir/dns because cs knows about
	// host names in local network (e.g. from /lib/ndb/local)
	lines, err := queryCS(ctx, "net", host, "1")
	if err != nil {
		if stringsHasSuffix(err.Error(), "dns failure") {
			return nil, &DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true}
		}
		return nil, handlePlan9DNSError(err, host)
	}
loop:
	for _, line := range lines {
		f := getFields(line)
		if len(f) < 2 {
			continue
		}
		addr := f[1]
		if i := bytealg.IndexByteString(addr, '!'); i >= 0 {
			addr = addr[:i] // remove port
		}
		if ParseIP(addr) == nil {
			continue
		}
		// only return unique addresses
		for _, a := range addrs {
			if a == addr {
				continue loop
			}
		}
		addrs = append(addrs, addr)
	}
	return
}

func (r *Resolver) lookupIP(ctx context.Context, network, host string) (addrs []IPAddr, err error) {
	if order, conf := systemConf().hostLookupOrder(r, host); order != hostLookupCgo {
		return r.goLookupIP(ctx, network, host, order, conf)
	}

	lits, err := r.lookupHost(ctx, host)
	if err != nil {
		return
	}
	for _, lit := range lits {
		host, zone := splitHostZone(lit)
		if ip := ParseIP(host); ip != nil {
			addr := IPAddr{IP: ip, Zone: zone}
			addrs = append(addrs, addr)
		}
	}
	return
}

func (r *Resolver) lookupPort(ctx context.Context, network, service string) (port int, err error) {
	switch network {
	case "ip": // no hints
		if p, err := r.lookupPortWithNetwork(ctx, "tcp", "ip", service); err == nil {
			return p, nil
		}
		return r.lookupPortWithNetwork(ctx, "udp", "ip", service)
	case "tcp", "tcp4", "tcp6":
		return r.lookupPortWithNetwork(ctx, "tcp", "tcp", service)
	case "udp", "udp4", "udp6":
		return r.lookupPortWithNetwork(ctx, "udp", "udp", service)
	default:
		return 0, &DNSError{Err: "unknown network", Name: network + "/" + service}
	}
}

func (*Resolver) lookupPortWithNetwork(ctx context.Context, network, errNetwork, service string) (port int, err error) {
	lines, err := queryCS(ctx, network, "127.0.0.1", toLower(service))
	if err != nil {
		if stringsHasSuffix(err.Error(), "can't translate service") {
			return 0, &DNSError{Err: "unknown port", Name: errNetwork + "/" + service, IsNotFound: true}
		}
		return
	}
	if len(lines) == 0 {
		return 0, &DNSError{Err: "unknown port", Name: errNetwork + "/" + service, IsNotFound: true}
	}
	f := getFields(lines[0])
	if len(f) < 2 {
		return 0, &DNSError{Err: "unknown port", Name: errNetwork + "/" + service, IsNotFound: true}
	}
	s := f[1]
	if i := bytealg.IndexByteString(s, '!'); i >= 0 {
		s = s[i+1:] // remove address
	}
	if n, _, ok := dtoi(s); ok {
		return n, nil
	}
	return 0, &DNSError{Err: "unknown port", Name: errNetwork + "/" + service, IsNotFound: true}
}

func (r *Resolver) lookupCNAME(ctx context.Context, name string) (cname string, err error) {
	if order, conf := systemConf().hostLookupOrder(r, name); order != hostLookupCgo {
		return r.goLookupCNAME(ctx, name, order, conf)
	}

	lines, err := queryDNS(ctx, name, "cname")
	if err != nil {
		if stringsHasSuffix(err.Error(), "dns failure") || stringsHasSuffix(err.Error(), "resource does not exist; negrcode 0") {
			return absDomainName(name), nil
		}
		return "", handlePlan9DNSError(err, cname)
	}
	if len(lines) > 0 {
		if f := getFields(lines[0]); len(f) >= 3 {
			return f[2] + ".", nil
		}
	}
	return "", errors.New("bad response from ndb/dns")
}

func (r *Resolver) lookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*SRV, err error) {
	if systemConf().mustUseGoResolver(r) {
		return r.goLookupSRV(ctx, service, proto, name)
	}
	var target string
	if service == "" && proto == "" {
		target = name
	} else {
		target = "_" + service + "._" + proto + "." + name
	}
	lines, err := queryDNS(ctx, target, "srv")
	if err != nil {
		return "", nil, handlePlan9DNSError(err, name)
	}
	for _, line := range lines {
		f := getFields(line)
		if len(f) < 6 {
			continue
		}
		port, _, portOk := dtoi(f[4])
		priority, _, priorityOk := dtoi(f[3])
		weight, _, weightOk := dtoi(f[2])
		if !(portOk && priorityOk && weightOk) {
			continue
		}
		addrs = append(addrs, &SRV{absDomainName(f[5]), uint16(port), uint16(priority), uint16(weight)})
		cname = absDomainName(f[0])
	}
	byPriorityWeight(addrs).sort()
	return
}

func (r *Resolver) lookupMX(ctx context.Context, name string) (mx []*MX, err error) {
	if systemConf().mustUseGoResolver(r) {
		return r.goLookupMX(ctx, name)
	}
	lines, err := queryDNS(ctx, name, "mx")
	if err != nil {
		return nil, handlePlan9DNSError(err, name)
	}
	for _, line := range lines {
		f := getFields(line)
		if len(f) < 4 {
			continue
		}
		if pref, _, ok := dtoi(f[2]); ok {
			mx = append(mx, &MX{absDomainName(f[3]), uint16(pref)})
		}
	}
	byPref(mx).sort()
	return
}

func (r *Resolver) lookupNS(ctx context.Context, name string) (ns []*NS, err error) {
	if systemConf().mustUseGoResolver(r) {
		return r.goLookupNS(ctx, name)
	}
	lines, err := queryDNS(ctx, name, "ns")
	if err != nil {
		return nil, handlePlan9DNSError(err, name)
	}
	for _, line := range lines {
		f := getFields(line)
		if len(f) < 3 {
			continue
		}
		ns = append(ns, &NS{absDomainName(f[2])})
	}
	return
}

func (r *Resolver) lookupTXT(ctx context.Context, name string) (txt []string, err error) {
	if systemConf().mustUseGoResolver(r) {
		return r.goLookupTXT(ctx, name)
	}
	lines, err := queryDNS(ctx, name, "txt")
	if err != nil {
		return nil, handlePlan9DNSError(err, name)
	}
	for _, line := range lines {
		if i := bytealg.IndexByteString(line, '\t'); i >= 0 {
			txt = append(txt, line[i+1:])
		}
	}
	return
}

func (r *Resolver) lookupAddr(ctx context.Context, addr string) (name []string, err error) {
	if order, conf := systemConf().addrLookupOrder(r, addr); order != hostLookupCgo {
		return r.goLookupPTR(ctx, addr, order, conf)
	}
	arpa, err := reverseaddr(addr)
	if err != nil {
		return
	}
	lines, err := queryDNS(ctx, arpa, "ptr")
	if err != nil {
		return nil, handlePlan9DNSError(err, addr)
	}
	for _, line := range lines {
		f := getFields(line)
		if len(f) < 3 {
			continue
		}
		name = append(name, absDomainName(f[2]))
	}
	return
}

// concurrentThreadsLimit returns the number of threads we permit to
// run concurrently doing DNS lookups.
func concurrentThreadsLimit() int {
	return 500
}
