package ctrld

import (
	"context"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"runtime"
	"strings"
	"sync"

	"github.com/cuonglm/osinfo"

	"github.com/miekg/dns"
)

const (
	dohMacHeader          = "x-cd-mac"
	dohIPHeader           = "x-cd-ip"
	dohHostHeader         = "x-cd-host"
	dohOsHeader           = "x-cd-os"
	dohClientIDPrefHeader = "x-cd-cpref"
	headerApplicationDNS  = "application/dns-message"

	// dohMaxResponseSize caps the response body read from a DoH/DoH3
	// upstream. A DNS message is bounded by the protocol's 16-bit length
	// field; anything larger cannot be a valid response. The cap stops a
	// malicious or compromised upstream from driving ctrld into unbounded
	// memory growth via io.ReadAll on attacker-controlled bytes.
	dohMaxResponseSize = dns.MaxMsgSize
	// dohMaxErrorBodySize bounds how much of a non-200 response body is
	// read for inclusion in the returned error.
	dohMaxErrorBodySize = 1024
)

// EncodeOsNameMap provides mapping from OS name to a shorter string, used for encoding x-cd-os value.
var EncodeOsNameMap = map[string]string{
	"windows": "1",
	"darwin":  "2",
	"linux":   "3",
	"freebsd": "4",
}

// DecodeOsNameMap provides mapping from encoded OS name to real value, used for decoding x-cd-os value.
var DecodeOsNameMap = map[string]string{}

// EncodeArchNameMap provides mapping from OS arch to a shorter string, used for encoding x-cd-os value.
var EncodeArchNameMap = map[string]string{
	"amd64":  "1",
	"arm64":  "2",
	"arm":    "3",
	"386":    "4",
	"mips":   "5",
	"mipsle": "6",
	"mips64": "7",
}

// DecodeArchNameMap provides mapping from encoded OS arch to real value, used for decoding x-cd-os value.
var DecodeArchNameMap = map[string]string{}

func init() {
	// Create reverse mappings for OS and architecture names
	// This is needed because the API expects encoded values, but we need to decode
	// them back to their original form for processing
	for k, v := range EncodeOsNameMap {
		DecodeOsNameMap[v] = k
	}
	for k, v := range EncodeArchNameMap {
		DecodeArchNameMap[v] = k
	}
}

var dohOsHeaderValue = sync.OnceValue(func() string {
	oi := osinfo.New()
	return strings.Join([]string{EncodeOsNameMap[runtime.GOOS], EncodeArchNameMap[runtime.GOARCH], oi.Dist}, "-")
})()

func newDohResolver(uc *UpstreamConfig) *dohResolver {
	r := &dohResolver{
		endpoint:          uc.u,
		isDoH3:            uc.Type == ResolverTypeDOH3,
		http3RoundTripper: uc.http3RoundTripper,
		uc:                uc,
	}
	return r
}

type dohResolver struct {
	uc                *UpstreamConfig
	endpoint          *url.URL
	isDoH3            bool
	http3RoundTripper http.RoundTripper
}

// Resolve performs DNS query with given DNS message using DOH protocol.
func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
	if err := validateMsg(msg); err != nil {
		return nil, err
	}
	logger := LoggerFromCtx(ctx)
	Log(ctx, logger.Debug(), "DoH resolver query started")

	data, err := msg.Pack()
	if err != nil {
		Log(ctx, logger.Error().Err(err), "Failed to pack DNS message")
		return nil, err
	}

	enc := base64.RawURLEncoding.EncodeToString(data)
	query := r.endpoint.Query()
	query.Add("dns", enc)

	endpoint := *r.endpoint
	endpoint.RawQuery = query.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
	if err != nil {
		Log(ctx, logger.Error().Err(err), "Could not create HTTP request")
		return nil, fmt.Errorf("could not create request: %w", err)
	}
	addHeader(ctx, req, r.uc)
	dnsTyp := uint16(0)
	if len(msg.Question) > 0 {
		dnsTyp = msg.Question[0].Qtype
	}
	c := http.Client{Transport: r.uc.dohTransport(ctx, dnsTyp)}
	if r.isDoH3 {
		transport := r.uc.doh3Transport(ctx, dnsTyp)
		if transport == nil {
			Log(ctx, logger.Error(), "DoH3 is not supported")
			return nil, errors.New("DoH3 is not supported")
		}
		c.Transport = transport
	}

	Log(ctx, logger.Debug(), "Sending DoH request to: %s", endpoint.String())
	resp, err := c.Do(req)
	if err != nil && r.uc.FallbackToDirectIP(ctx) {
		retryCtx, cancel := r.uc.Context(context.WithoutCancel(ctx))
		defer cancel()
		logger := LoggerFromCtx(ctx)
		logger.Warn().Err(err).Msg("Retrying request after fallback to direct ip")
		resp, err = c.Do(req.Clone(retryCtx))
	}
	if err != nil {
		err = wrapUrlError(err)
		Log(ctx, logger.Error().Err(err), "DoH request failed")
		return nil, fmt.Errorf("could not perform request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, dohMaxErrorBodySize))
		return nil, fmt.Errorf("wrong response from DOH server, got: %s, status: %d", string(body), resp.StatusCode)
	}

	buf, err := io.ReadAll(io.LimitReader(resp.Body, dohMaxResponseSize+1))
	if err != nil {
		Log(ctx, logger.Error().Err(err), "Could not read response body")
		return nil, fmt.Errorf("could not read message from response: %w", err)
	}
	if len(buf) > dohMaxResponseSize {
		return nil, fmt.Errorf("DoH response exceeds %d-byte maximum DNS message size", dohMaxResponseSize)
	}

	answer := new(dns.Msg)
	if err := answer.Unpack(buf); err != nil {
		Log(ctx, logger.Error().Err(err), "Failed to unpack DNS answer")
		return nil, fmt.Errorf("answer.Unpack: %w", err)
	}

	Log(ctx, logger.Debug(), "DoH resolver query successful")
	return answer, nil
}

// addHeader adds necessary HTTP header to request based on upstream config.
func addHeader(ctx context.Context, req *http.Request, uc *UpstreamConfig) {
	printed := false
	dohHeader := make(http.Header)
	if uc.UpstreamSendClientInfo() {
		if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
			printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != ""
			switch {
			case uc.IsControlD():
				dohHeader = newControlDHeaders(ci)
			case uc.isNextDNS():
				dohHeader = newNextDNSHeaders(ci)
			}
		}
	}
	if printed {
		logger := LoggerFromCtx(ctx)
		Log(ctx, logger.Debug(), "Sending request header: %v", dohHeader)
	}
	dohHeader.Set("Content-Type", headerApplicationDNS)
	dohHeader.Set("Accept", headerApplicationDNS)
	req.Header = dohHeader
}

// newControlDHeaders returns DoH/Doh3 HTTP request headers for ControlD upstream.
func newControlDHeaders(ci *ClientInfo) http.Header {
	header := make(http.Header)
	if ci.Mac != "" {
		header.Set(dohMacHeader, ci.Mac)
	}
	if ci.IP != "" {
		header.Set(dohIPHeader, ci.IP)
	}
	if ci.Hostname != "" {
		header.Set(dohHostHeader, ci.Hostname)
	}
	if ci.Self {
		header.Set(dohOsHeader, dohOsHeaderValue)
	}
	switch ci.ClientIDPref {
	case "mac":
		header.Set(dohClientIDPrefHeader, "1")
	case "host":
		header.Set(dohClientIDPrefHeader, "2")
	}
	return header
}

// newNextDNSHeaders returns DoH/Doh3 HTTP request headers for nextdns upstream.
// https://github.com/nextdns/nextdns/blob/v1.41.0/resolver/doh.go#L100
func newNextDNSHeaders(ci *ClientInfo) http.Header {
	header := make(http.Header)
	if ci.Mac != "" {
		// https: //github.com/nextdns/nextdns/blob/v1.41.0/run.go#L543
		header.Set("X-Device-Model", "mac:"+ci.Mac[:8])
	}
	if ci.IP != "" {
		header.Set("X-Device-Ip", ci.IP)
	}
	if ci.Hostname != "" {
		header.Set("X-Device-Name", ci.Hostname)
	}
	return header
}

// wrapCertificateVerificationError wraps a certificate verification error with additional context about the certificate issuer.
// It extracts information like the issuer, organization, and subject from the certificate for a more descriptive error output.
// If no certificate-related information is available, it simply returns the original error unmodified.
func wrapCertificateVerificationError(err error) error {
	var tlsErr *tls.CertificateVerificationError
	if errors.As(err, &tlsErr) {
		if len(tlsErr.UnverifiedCertificates) > 0 {
			cert := tlsErr.UnverifiedCertificates[0]
			// Extract a more user-friendly issuer name
			var issuer string
			var organization string
			if len(cert.Issuer.Organization) > 0 {
				organization = cert.Issuer.Organization[0]
				issuer = organization
			} else if cert.Issuer.CommonName != "" {
				issuer = cert.Issuer.CommonName
			} else {
				issuer = cert.Issuer.String()
			}

			// Get the organization from the subject field as well
			if len(cert.Subject.Organization) > 0 {
				organization = cert.Subject.Organization[0]
			}

			// Extract the subject information
			subjectCN := cert.Subject.CommonName
			if subjectCN == "" && len(cert.Subject.Organization) > 0 {
				subjectCN = cert.Subject.Organization[0]
			}
			return fmt.Errorf("%w: %s, %s, %s", tlsErr, subjectCN, organization, issuer)
		}
	}
	return err
}

// wrapUrlError inspects and wraps a URL error, focusing on certificate verification errors for detailed context.
func wrapUrlError(err error) error {
	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		var tlsErr *tls.CertificateVerificationError
		if errors.As(urlErr.Err, &tlsErr) {
			urlErr.Err = wrapCertificateVerificationError(tlsErr)
			return urlErr
		}
	}
	return err
}
