package dns

import (
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"net/netip"
	"strconv"

	"codeberg.org/miekg/dns/internal/reverse"
	"golang.org/x/crypto/cryptobyte"
)

// ENDS0 option codes.
const (
	CodeNone         uint16 = 0x0
	CodeLLQ          uint16 = 0x1  // Long lived queries: http://tools.ietf.org/html/draft-sekar-dns-llq-01.
	CodeUPDATELEASE  uint16 = 0x2  // Update lease (see RFC 9664).
	CodeNSID         uint16 = 0x3  // Nsid (see RFC 5001).
	CodeESU          uint16 = 0x4  // ENUM Source-URI draft: https://datatracker.ietf.org/doc/html/draft-kaplan-enum-source-uri-00.
	CodeDAU          uint16 = 0x5  // DNSSEC Algorithm Understood.
	CodeDHU          uint16 = 0x6  // DS Hash Understood.
	CodeN3U          uint16 = 0x7  // NSEC3 Hash Understood.
	CodeSUBNET       uint16 = 0x8  // Client-subnet, see RFC 7871.
	CodeEXPIRE       uint16 = 0x9  // Expire, RFC 7314.
	CodeCOOKIE       uint16 = 0xA  // Cookie, RFC 7873.
	CodeTCPKEEPALIVE uint16 = 0xB  // TCP keep alive (see RFC 7828).
	CodePADDING      uint16 = 0xC  // Padding (see RFC 7830).
	CodeEDE          uint16 = 0xF  // Extended DNS errors (see RFC 8914).
	CodeREPORTING    uint16 = 0x12 // EDNS0 reporting (see RFC 9567).
	CodeZONEVERSION  uint16 = 0x13 // Zone version (see RFC 9660).
	CodeMQQUERY      uint16 = 0x14 // MQTYPE-Query (see ietf-dnssd-multi-qtypes-14).
	CodeMQRESPONSE   uint16 = 0x15 // MQTYPE-Response (see ietf-dnssd-multi-qtypes-14).

	CodeLOCALSTART uint16 = 0xFDE9 // Beginning of range reserved for local/experimental use (see RFC 6891).
	CodeLOCALEND   uint16 = 0xFFFE // End of range reserved for local/experimental use (see RFC 6891).
)

// LLQ stands for Long Lived Queries: http://tools.ietf.org/html/draft-sekar-dns-llq-01
// Implemented for completeness, as the EDNS0 type code is assigned.
//
// This record must be put in the pseudo section.
type LLQ struct {
	Version   uint16
	Opcode    uint16
	Error     uint16
	ID        uint64
	LeaseLife uint32
}

func (o *LLQ) Len() int    { return tlv + 18 }
func (o *LLQ) Data() RDATA { return o }
func (o *LLQ) String() string {
	sb := sprintOptionHeader(o)
	sprintData(sb, strconv.FormatUint(uint64(o.Version), 10), strconv.FormatUint(uint64(o.Opcode), 10),
		strconv.FormatUint(uint64(o.Error), 10), strconv.FormatUint(o.ID, 10),
		strconv.FormatUint(uint64(o.LeaseLife), 10))
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// UPDATELEASE implements the EDNS0 Update Lease option (RFC 9664).
//
// The Update Lease option provides lease durations that should be associated
// with a given DNS Update request, and are included in responses for acknowledgement.
// A DNS server should automatically delete the included RRs after the lease time
// is over.
//
// The Update Lease option has two variants: a pre-RFC 4-byte variant and
// the 8-byte variant. This library only supports the post-standardization
// 8-byte variant.
//
// This record must be put in the pseudo section.
type UPDATELEASE struct {
	Lease    uint32
	KeyLease uint32
}

func (o *UPDATELEASE) Len() int    { return tlv + 8 }
func (o *UPDATELEASE) Data() RDATA { return o }
func (o *UPDATELEASE) String() string {
	sb := sprintOptionHeader(o)
	sprintData(sb, strconv.FormatUint(uint64(o.Lease), 10), strconv.FormatUint(uint64(o.KeyLease), 10))
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// REPORTING implements the EDNS0 Reporting Channel option (RFC 9567).
//
// This record must be put in the pseudo section.
type REPORTING struct {
	AgentDomain string `dns:"name"`
}

func (o *REPORTING) Len() int    { return tlv + len(o.AgentDomain) }
func (o *REPORTING) Data() RDATA { return o }
func (o *REPORTING) String() string {
	sb := sprintOptionHeader(o)
	sprintData(sb, o.AgentDomain)
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// The Cookie option is used to add a DNS Cookie to a message.
//
// The Cookie field consists out of a client cookie (RFC 7873 Section 4), that is
// always 8 bytes. It may then optionally be followed by the server cookie. The server
// cookie is of variable length, 8 to a maximum of 32 bytes. In other words:
//
//	cCookie := o.Cookie[:16]
//	sCookie := o.Cookie[16:]
//
// There is no guarantee that the Cookie string has a specific length.
//
// This record must be put in the pseudo section.
type COOKIE struct {
	Cookie string `dns:"hex"`
}

func (o *COOKIE) Len() int    { return tlv + len(o.Cookie)/2 }
func (o *COOKIE) Data() RDATA { return o }

// String outputs: "COOKIE 962d3a4c596914578386a9a1dbbebf9e" (depending on the cookie size). This is the presentation
// format.
func (o *COOKIE) String() string {
	sb := sprintOptionHeader(o)
	sb.WriteString(o.Cookie)
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// NSID option is used to retrieve a nameserver identifier. When sending a request Nsid must be empty.
// The identifier is an opaque string encoded as hex.
//
// This record must be put in the pseudo section.
type NSID struct {
	Nsid string `dns:"hex"`
}

func (o *NSID) Len() int    { return tlv + len(o.Nsid)/2 }
func (o *NSID) Data() RDATA { return o }

// String outputs: "NSID 5573652074686520666f726365: "Use the force"
// This is the presentation format.
func (o *NSID) String() string {
	sb := sprintOptionHeader(o)
	sb.WriteString(o.Nsid)
	if x, err := hex.DecodeString(o.Nsid); err == nil { // == nil
		sb.WriteString(": \"")
		sb.Write(x)
		sb.WriteString("\"")
	}
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// PADDING option is used to add padding to a request/response. The default value of padding SHOULD be 0x0 but
// other values MAY be used.
//
// This record must be put in the pseudo section.
type PADDING struct {
	Padding string `dns:"hex"`
}

func (o *PADDING) Len() int    { return tlv + len(o.Padding)/2 }
func (o *PADDING) Data() RDATA { return o }
func (o *PADDING) String() string {
	sb := sprintOptionHeader(o)
	sb.WriteString(o.Padding)
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// EXPIRE implements the EDNS0 option as described in RFC 7314.
//
// This record must be put in the pseudo section.
type EXPIRE struct {
	// If Expire is zero this option will be empty.
	Expire uint32
}

func (o *EXPIRE) Len() int    { return tlv + 4 }
func (o *EXPIRE) Data() RDATA { return o }
func (o *EXPIRE) String() string {
	sb := sprintOptionHeader(o)
	if o.Expire == 0 {
		s := sb.String()
		builderPool.Put(*sb)
		return s
	}
	sb.WriteString(strconv.FormatUint(uint64(o.Expire), 10))
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// DAU implements the EDNS0 "DNSSEC Algorithm Understood" option. See RFC 6975.
//
// This record must be put in the pseudo section.
type DAU struct {
	AlgCode []uint8
}

func (o *DAU) Len() int    { return tlv + len(o.AlgCode) }
func (o *DAU) Data() RDATA { return o }
func (o *DAU) String() string {
	sb := sprintOptionHeader(o)
	for _, alg := range o.AlgCode {
		sb.WriteByte(' ')
		if a, ok := AlgorithmToString[alg]; ok {
			sb.WriteString(a)
		} else {
			sb.WriteString(strconv.Itoa(int(alg)))
		}
	}
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// DHU implements the EDNS0 "DS Hash Understood" option. See RFC 6975.
//
// This record must be put in the pseudo section.
type DHU struct {
	AlgCode []uint8
}

func (o *DHU) Len() int    { return tlv + len(o.AlgCode) }
func (o *DHU) Data() RDATA { return o }
func (o *DHU) String() string {
	sb := sprintOptionHeader(o)
	for _, alg := range o.AlgCode {
		sb.WriteByte(' ')
		if a, ok := AlgorithmToString[alg]; ok {
			sb.WriteString(a)
		} else {
			sb.WriteString(strconv.Itoa(int(alg)))
		}
	}
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// EDNS0_N3U implements the EDNS0 "NSEC3 Hash Understood" option. See RFC 6975.
//
// This record must be put in the pseudo section.
type N3U struct {
	AlgCode []uint8
}

func (o *N3U) Len() int    { return tlv + len(o.AlgCode) }
func (o *N3U) Data() RDATA { return o }
func (o *N3U) String() string {
	sb := sprintOptionHeader(o)
	for _, alg := range o.AlgCode {
		sb.WriteByte(' ')
		if a, ok := AlgorithmToString[alg]; ok {
			sb.WriteString(a)
		} else {
			sb.WriteString(strconv.Itoa(int(alg)))
		}
	}
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// TCPKEEPALIVE is an EDNS0 option that instructs the server to keep the TCP connection alivo. See RFC 7828.
//
// This record must be put in the pseudo section.
type TCPKEEPALIVE struct {
	// Timeout is an idle timeout value for the TCP connection, specified in units of 100 milliseconds
	Timeout uint16
}

func (o *TCPKEEPALIVE) Len() int {
	if o.Timeout == 0 {
		return tlv
	}
	return tlv + 2
}

func (o *TCPKEEPALIVE) Data() RDATA { return o }

func (o *TCPKEEPALIVE) String() string {
	sb := sprintOptionHeader(o)
	sb.WriteString("use tcp keep-alive")
	if o.Timeout == 0 {
		sb.WriteString(", timeout omitted")
	} else {
		fmt.Fprintf(sb, ", timeout %dms", o.Timeout*100)
	}
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// EDE option is used to return additional information about the cause of DNS errors. EDE stands for Extended
// DNS Errors. See RFC 8914.
//
// This record must be put in the pseudo section.
type EDE struct {
	InfoCode  uint16
	ExtraText string
}

func (o *EDE) Len() int    { return tlv + 2 + len(o.ExtraText) }
func (o *EDE) Data() RDATA { return o }

// String outputs: "EDE 15 "Blocked": "", where ExtraText is always printed, even if it's
// empty. This is the presentation format.
func (o *EDE) String() string {
	sb := sprintOptionHeader(o)
	sb.WriteString(strconv.FormatUint(uint64(o.InfoCode), 10))
	if s, ok := ExtendedErrorToString[o.InfoCode]; ok {
		sb.WriteString(" \"")
		sb.WriteString(s)
		sb.WriteByte('"')
	}
	sb.WriteString(": \"")
	sb.WriteString(o.ExtraText)
	sb.WriteByte('"')
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// SUBNET is the subnet option that is used to give the remote nameserver
// an idea of where the client is, see RFC 7871. It can give back a different
// answer depending on the location or network topology.
//
// This record must be put in the pseudo section.
type SUBNET struct {
	Family  uint16 // 1 for IP, 2 for IP6.
	Netmask uint8  // 32 for IPV4, 128 for IPv6.
	Scope   uint8
	Address netip.Addr // Client IP address.
}

func (o *SUBNET) Len() int    { return tlv + 2 + 2 + int((o.Netmask+7)/8) }
func (o *SUBNET) Data() RDATA { return o }
func (o *SUBNET) String() string {
	sb := sprintOptionHeader(o)
	switch {
	case !o.Address.IsValid():
		sb.WriteString("<nil>")
	case o.Address.Unmap().Is4():
		sb.WriteString(o.Address.Unmap().String())
	default:
		sb.WriteByte('[')
		sb.WriteString(o.Address.String())
		sb.WriteByte(']')
	}
	sb.WriteByte('/')
	sb.WriteString(strconv.Itoa(int(o.Netmask)))
	sb.WriteByte('/')
	sb.WriteString(strconv.Itoa(int(o.Scope)))
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// The ESU option for ENUM Source-URI Extension.
type ESU struct {
	URI string
}

func (o *ESU) Len() int    { return tlv + len(o.URI) }
func (o *ESU) Data() RDATA { return o }
func (o *ESU) String() string {
	sb := sprintOptionHeader(o)
	sb.WriteString(o.URI)
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

// The ZONEVERSION option, see RFC 9660. Only a single type (0) has been allocated, if used the [SOA] serial
// is put in Version. Example on how to create a ZONEVERSION:
//
//	z := &ZONEVERSION{Labels: 8, Type: 0, Version: make([]byte, 4)}
//	binary.BigEndian.PutUint32(z.Version, serial)
//
// Or if you know your binary: &ZONEVERSION{Labels: 8, Type: 0, Version: {1,2,3,4}}
//
// This record must be put in the pseudo section.
type ZONEVERSION struct {
	Labels  uint8
	Type    uint8
	Version []byte
}

func (o *ZONEVERSION) Len() int    { return tlv + 2 + len(o.Version) }
func (o *ZONEVERSION) Data() RDATA { return o }

// Strings outputs "ZONEVERSION 4 SOA-SERIAL 1002" as the presentation format.
func (o *ZONEVERSION) String() string {
	sb := sprintOptionHeader(o)
	sb.WriteString(strconv.Itoa(int(o.Labels)))
	sb.WriteByte(' ')
	switch o.Type {
	case 0:
		sb.WriteString("SOA-SERIAL")
		sb.WriteByte(' ')
		version := binary.BigEndian.Uint32([]byte(o.Version))
		sb.WriteString(strconv.Itoa(int(version)))
	default:
		sb.WriteString("TYPE")
		sb.WriteString(strconv.Itoa(int(o.Type)))
		sb.WriteByte(' ')
		sb.Write(o.Version)
	}
	s := sb.String()
	builderPool.Put(*sb)
	return s
}

type MQQUERY struct {
	Types []uint16
}

func (o *MQQUERY) Len() int    { return tlv + len(o.Types)*2 }
func (o *MQQUERY) Data() RDATA { return o }
func (o *MQQUERY) String() string {
	// TODO(miekg)
	return ""
}

type MQRESPONSE struct {
	Types []uint16
}

func (o *MQRESPONSE) Len() int    { return tlv + len(o.Types)*2 }
func (o *MQRESPONSE) Data() RDATA { return o }
func (o *MQRESPONSE) String() string {
	// TODO(miekg)
	return ""
}

// ERFC3597 is used to represent unknown EDNS0 options.
type ERFC3597 struct {
	EDNS0Code uint16 `dns:"-"`
	Code      string `dns:"hex"`
}

func (o *ERFC3597) Len() int    { return tlv + len(o.Code)/2 }
func (o *ERFC3597) Data() RDATA { return o }
func (o *ERFC3597) String() string {
	sb := builderPool.Get()
	sb.WriteByte('.')
	sb.WriteByte('\t')
	sb.WriteByte('\t') // skip TTL
	sb.WriteString(classToString(o.Header().Class))
	sb.WriteByte('\t')
	sb.WriteString(codeToString(o.EDNS0Code))
	sb.WriteByte('\t')

	sprintData(&sb, `\#`, strconv.Itoa(len(o.Code)/2), o.Code)
	s := sb.String()
	builderPool.Put(sb)
	return s
}

// Extended DNS Error Codes (RFC 8914). These are used in the [EDE] pseudo RR.
const (
	ExtendedErrorOther uint16 = iota
	ExtendedErrorUnsupportedDNSKEYAlgorithm
	ExtendedErrorUnsupportedDSDigestType
	ExtendedErrorStaleAnswer
	ExtendedErrorForgedAnswer
	ExtendedErrorDNSSECIndeterminate
	ExtendedErrorDNSBogus
	ExtendedErrorSignatureExpired
	ExtendedErrorSignatureNotYetValid
	ExtendedErrorDNSKEYMissing
	ExtendedErrorRRSIGsMissing
	ExtendedErrorNoZoneKeyBitSet
	ExtendedErrorNSECMissing
	ExtendedErrorCachedError
	ExtendedErrorNotReady
	ExtendedErrorBlocked
	ExtendedErrorCensored
	ExtendedErrorFiltered
	ExtendedErrorProhibited
	ExtendedErrorStaleNXDOMAINAnswer
	ExtendedErrorNotAuthoritative
	ExtendedErrorNotSupported
	ExtendedErrorNoReachableAuthority
	ExtendedErrorNetworkError
	ExtendedErrorInvalidData
	ExtendedErrorSignatureExpiredBeforeValid
	ExtendedErrorTooEarly                  // RFC 9250
	ExtendedErrorUnsupportedNSEC3IterValue // RFC 9276
	ExtendedErrorUnableToConformToPolicy
	ExtendedErrorSynthesized
	ExtendedErrorInvalidQueryType // RFC 9824
	ExtendedErrorRateLimited
	ExtendedErrorOverQuota
)

// ExtendedErrorToString maps extended error info codes to a human readable description. This is used inside
// the [EDE] pseudo RR.
var ExtendedErrorToString = map[uint16]string{
	ExtendedErrorOther:                       "Other",
	ExtendedErrorUnsupportedDNSKEYAlgorithm:  "Unsupported DNSKEY Algorithm",
	ExtendedErrorUnsupportedDSDigestType:     "Unsupported DS Digest Type",
	ExtendedErrorStaleAnswer:                 "Stale Answer",
	ExtendedErrorForgedAnswer:                "Forged Answer",
	ExtendedErrorDNSSECIndeterminate:         "DNSSEC Indeterminate",
	ExtendedErrorDNSBogus:                    "DNSSEC Bogus",
	ExtendedErrorSignatureExpired:            "Signature Expired",
	ExtendedErrorSignatureNotYetValid:        "Signature Not Yet Valid",
	ExtendedErrorDNSKEYMissing:               "DNSKEY Missing",
	ExtendedErrorRRSIGsMissing:               "RRSIGs Missing",
	ExtendedErrorNoZoneKeyBitSet:             "No Zone Key Bit Set",
	ExtendedErrorNSECMissing:                 "NSEC Missing",
	ExtendedErrorCachedError:                 "Cached Error",
	ExtendedErrorNotReady:                    "Not Ready",
	ExtendedErrorBlocked:                     "Blocked",
	ExtendedErrorCensored:                    "Censored",
	ExtendedErrorFiltered:                    "Filtered",
	ExtendedErrorProhibited:                  "Prohibited",
	ExtendedErrorStaleNXDOMAINAnswer:         "Stale NXDOMAIN Answer",
	ExtendedErrorNotAuthoritative:            "Not Authoritative",
	ExtendedErrorNotSupported:                "Not Supported",
	ExtendedErrorNoReachableAuthority:        "No Reachable Authority",
	ExtendedErrorNetworkError:                "Network Error",
	ExtendedErrorInvalidData:                 "Invalid Data",
	ExtendedErrorSignatureExpiredBeforeValid: "Signature Expired Before Valid",
	ExtendedErrorTooEarly:                    "Too Early",
	ExtendedErrorUnsupportedNSEC3IterValue:   "Unsupported NSEC3 Iterations Value",
	ExtendedErrorUnableToConformToPolicy:     "Unable To Conform To Policy",
	ExtendedErrorSynthesized:                 "Synthesized",
	ExtendedErrorInvalidQueryType:            "Invalid Query Type",
}

// StringToExtendedError is a map from human readable descriptions to extended error info codes.
var StringToExtendedError = reverse.Map(ExtendedErrorToString)

func unpackOptionCode(option EDNS0, s *cryptobyte.String) error {
	switch x := option.(type) {
	case *LLQ:
		return x.unpack(s)
	case *NSID:
		return x.unpack(s)
	case *PADDING:
		return x.unpack(s)
	case *EDE:
		return x.unpack(s)
	case *REPORTING:
		return x.unpack(s)
	case *COOKIE:
		return x.unpack(s)
	case *EXPIRE:
		return x.unpack(s)
	case *DAU:
		return x.unpack(s)
	case *DHU:
		return x.unpack(s)
	case *N3U:
		return x.unpack(s)
	case *TCPKEEPALIVE:
		return x.unpack(s)
	case *SUBNET:
		return x.unpack(s)
	case *ESU:
		return x.unpack(s)
	case *ZONEVERSION:
		return x.unpack(s)
	case *ERFC3597:
		return x.unpack(s)
	}
	if x, ok := option.(Packer); ok {
		msg := []byte(*s)
		return x.Unpack(msg)
	}
	return fmt.Errorf("dns: no option unpack defined")
}

func packOptionCode(option EDNS0, msg []byte, off int) (int, error) {
	switch x := option.(type) {
	case *LLQ:
		return x.pack(msg, off)
	case *NSID:
		return x.pack(msg, off)
	case *PADDING:
		return x.pack(msg, off)
	case *EDE:
		return x.pack(msg, off)
	case *REPORTING:
		return x.pack(msg, off)
	case *COOKIE:
		return x.pack(msg, off)
	case *EXPIRE:
		return x.pack(msg, off)
	case *DAU:
		return x.pack(msg, off)
	case *DHU:
		return x.pack(msg, off)
	case *N3U:
		return x.pack(msg, off)
	case *TCPKEEPALIVE:
		return x.pack(msg, off)
	case *SUBNET:
		return x.pack(msg, off)
	case *ESU:
		return x.pack(msg, off)
	case *ZONEVERSION:
		return x.pack(msg, off)
	case *ERFC3597:
		return x.pack(msg, off)
	}
	if x, ok := option.(Packer); ok {
		return x.Pack(msg, off)
	}
	return 0, fmt.Errorf("dns: no option pack defined")
}

// type, length, value is the length the code (2 octets) and length (2 octets) of each EDNS0 option code.
const tlv = 4
