package pia

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net"
	"net/http"
	"net/netip"
	"net/url"
	"strconv"
	"time"
)

type RegistrationClient struct {
	CAPEM     []byte
	Port      uint16
	MaxBody   int64
	Timeout   time.Duration
	UserAgent string
}

func NewRegistrationClient(caPEM []byte) *RegistrationClient {
	return &RegistrationClient{
		CAPEM: caPEM, Port: DefaultAddKeyPort, MaxBody: DefaultMaxResponseBody,
		Timeout: DefaultRequestTimeout, UserAgent: DefaultUserAgent,
	}
}

func (c *RegistrationClient) RegisterKey(ctx context.Context, server WireGuardServer, token string, publicKey string) (Registration, error) {
	if !server.IP.IsValid() || !server.IP.Is4() || !validHostname(server.Hostname) {
		return Registration{}, NewError(CodeInvalidInput, "The selected PIA WireGuard server is invalid.")
	}
	if !validSecret([]byte(token), 16, 4096) {
		return Registration{}, NewError(CodeTokenRejected, "The PIA authentication token is invalid.")
	}
	if !validWGKey(publicKey) {
		return Registration{}, NewError(CodeInvalidInput, "The WireGuard public key is invalid.")
	}
	roots := x509.NewCertPool()
	if !roots.AppendCertsFromPEM(c.CAPEM) {
		return Registration{}, NewError(CodeTLSValidation, "The built-in PIA certificate authority is invalid.")
	}
	port := c.Port
	if port == 0 {
		port = DefaultAddKeyPort
	}
	dialer := &net.Dialer{Timeout: 8 * time.Second, KeepAlive: 30 * time.Second}
	transport := &http.Transport{
		Proxy: nil,
		DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
			return dialer.DialContext(ctx, network, net.JoinHostPort(server.IP.String(), strconv.Itoa(int(port))))
		},
		TLSClientConfig:     &tls.Config{ServerName: server.Hostname, RootCAs: roots, MinVersion: tls.VersionTLS12},
		TLSHandshakeTimeout: 8 * time.Second, ResponseHeaderTimeout: 12 * time.Second, ForceAttemptHTTP2: true,
	}
	defer transport.CloseIdleConnections()
	client := &http.Client{Transport: transport, Timeout: c.Timeout, CheckRedirect: noRedirect}
	endpoint := url.URL{Scheme: "https", Host: net.JoinHostPort(server.Hostname, strconv.Itoa(int(port))), Path: "/addKey"}
	query := endpoint.Query()
	query.Set("pt", token)
	query.Set("pubkey", publicKey)
	endpoint.RawQuery = query.Encode()
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
	if err != nil {
		return Registration{}, WrapError(CodeRegistrationRejected, "Could not prepare PIA key registration.", err)
	}
	request.Header.Set("Accept", "application/json")
	request.Header.Set("User-Agent", c.UserAgent)
	response, err := client.Do(request)
	if err != nil {
		return Registration{}, classifyNetworkError(ctx, CodeNetworkUnavailable, "The selected PIA WireGuard server could not be reached.", err)
	}
	defer response.Body.Close()
	if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
		return Registration{}, NewError(CodeTokenRejected, "The PIA authentication token was rejected.")
	}
	if response.StatusCode != http.StatusOK {
		return Registration{}, NewError(CodeRegistrationRejected, fmt.Sprintf("PIA key registration returned HTTP %d.", response.StatusCode))
	}
	if !expectedContentType(response.Header.Get("Content-Type"), "application/json") {
		return Registration{}, NewError(CodeRegistrationInvalid, "PIA key registration returned an unexpected content type.")
	}
	raw, err := readLimitedBody(response.Body, c.MaxBody)
	if err != nil {
		return Registration{}, WrapError(CodeRegistrationInvalid, "PIA key registration returned an invalid response.", err)
	}
	return parseRegistration(raw)
}

func parseRegistration(raw []byte) (Registration, error) {
	var payload struct {
		Status     string   `json:"status"`
		PeerIP     string   `json:"peer_ip"`
		ServerKey  string   `json:"server_key"`
		ServerIP   string   `json:"server_ip"`
		ServerPort int      `json:"server_port"`
		DNSServers []string `json:"dns_servers"`
	}
	if err := decodeSingleJSON(raw, &payload); err != nil {
		return Registration{}, NewError(CodeRegistrationInvalid, "PIA key registration returned malformed JSON.")
	}
	if payload.Status != "OK" {
		return Registration{}, NewError(CodeRegistrationRejected, "The PIA server rejected WireGuard key registration.")
	}
	peerIP, err := parsePeerIP(payload.PeerIP)
	if err != nil {
		return Registration{}, NewError(CodeRegistrationInvalid, "PIA returned an invalid WireGuard peer address.")
	}
	if !validWGKey(payload.ServerKey) {
		return Registration{}, NewError(CodeRegistrationInvalid, "PIA returned an invalid WireGuard server key.")
	}
	serverIP, err := netip.ParseAddr(payload.ServerIP)
	if err != nil || !serverIP.Is4() || serverIP.IsUnspecified() {
		return Registration{}, NewError(CodeRegistrationInvalid, "PIA returned an invalid WireGuard server address.")
	}
	if payload.ServerPort < 1 || payload.ServerPort > 65535 {
		return Registration{}, NewError(CodeRegistrationInvalid, "PIA returned an invalid WireGuard server port.")
	}
	dns := make([]netip.Addr, 0, len(payload.DNSServers))
	for _, value := range payload.DNSServers {
		address, err := netip.ParseAddr(value)
		if err != nil || !address.Is4() || address.IsUnspecified() {
			continue
		}
		if len(dns) == 8 {
			break
		}
		dns = append(dns, address)
	}
	return Registration{PeerIP: peerIP, ServerKey: payload.ServerKey, ServerIP: serverIP, ServerPort: uint16(payload.ServerPort), DNSServers: dns}, nil
}

func parsePeerIP(value string) (netip.Prefix, error) {
	if address, err := netip.ParseAddr(value); err == nil {
		if !address.Is4() || address.IsUnspecified() {
			return netip.Prefix{}, fmt.Errorf("peer address is not a usable IPv4 address")
		}
		return netip.PrefixFrom(address, 32), nil
	}
	prefix, err := netip.ParsePrefix(value)
	if err != nil || !prefix.Addr().Is4() || prefix.Addr().IsUnspecified() || prefix.Bits() != 32 {
		return netip.Prefix{}, fmt.Errorf("peer address is not an IPv4 host prefix")
	}
	return prefix, nil
}
