// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

// DO NOT MODIFY. Generated by gotmpl.
// source: internal/shared/otlp/observ/target.go.tmpl

package observ

import (
	"errors"
	"fmt"
	"net"
	"net/netip"
	"strconv"
	"strings"
)

const (
	schemeUnix         = "unix"
	schemeUnixAbstract = "unix-abstract"
)

// ParseCanonicalTarget parses a target string and returns the extracted host
// (domain name or IP address), the target port, or an error.
//
// If no port is specified, -1 is returned.
//
// If no host is specified, an empty string is returned.
//
// The target string must have the form
// "<scheme>://[authority]/<endpoint>". For example:
//   - "dns:///example.com:42"
//   - "dns://8.8.8.8/example.com:42"
//   - "unix:///path/to/socket"
//   - "unix-abstract:///socket-name"
//   - "passthrough:///192.34.2.1:42"
//
// The target is expected to be returned by gRPC's ClientConn.CanonicalTarget
// method.
func ParseCanonicalTarget(target string) (string, int, error) {
	const sep = "://"

	// Find scheme. Do not allocate the string by using url.Parse.
	scheme, endpoint, found := strings.Cut(target, sep)
	if !found {
		return "", -1, fmt.Errorf("invalid target %q: missing scheme", target)
	}

	// Check for unix schemes.
	if scheme == schemeUnix || scheme == schemeUnixAbstract {
		return parseUnix(endpoint)
	}

	// Strip leading slash and any authority.
	if i := strings.Index(endpoint, "/"); i != -1 {
		endpoint = endpoint[i+1:]
	}

	// DNS, passthrough, and custom resolvers.
	return parseEndpoint(endpoint)
}

// parseUnix parses Unix socket targets.
func parseUnix(endpoint string) (string, int, error) {
	// Format: unix[-abstract]://path
	//
	// We should have "/path" (empty authority) if valid.
	if len(endpoint) >= 1 && endpoint[0] == '/' {
		// Return the full path including leading slash.
		return endpoint, -1, nil
	}

	// If there is no leading slash, an authority might be present, which is
	// invalid. Check for this case: "authority/path".
	if slashIdx := strings.Index(endpoint, "/"); slashIdx > 0 {
		return "", -1, fmt.Errorf("invalid (non-empty) authority: %s", endpoint[:slashIdx])
	}

	return "", -1, errors.New("invalid unix target format")
}

// parseEndpoint parses an endpoint from a gRPC target.
//
// It supports the following formats:
//   - "host"
//   - "host%zone"
//   - "host:port"
//   - "host%zone:port"
//   - "ipv4"
//   - "ipv4%zone"
//   - "ipv4:port"
//   - "ipv4%zone:port"
//   - "ipv6"
//   - "ipv6%zone"
//   - "[ipv6]"
//   - "[ipv6%zone]"
//   - "[ipv6]:port"
//   - "[ipv6%zone]:port"
//
// It returns the host or host%zone (domain name or IP address), the port (or
// -1 if not specified), or an error if the input is invalid.
func parseEndpoint(endpoint string) (string, int, error) {
	// First check if the endpoint is just an IP address.
	if ip := parseIP(endpoint); ip != "" {
		return ip, -1, nil
	}

	// If there's no colon, there is no port (IPv6 with no port checked above).
	if !strings.Contains(endpoint, ":") {
		return endpoint, -1, nil
	}

	host, portStr, err := net.SplitHostPort(endpoint)
	if err != nil {
		return "", -1, fmt.Errorf("invalid host:port %q: %w", endpoint, err)
	}

	const base, bitSize = 10, 16
	port16, err := strconv.ParseUint(portStr, base, bitSize)
	if err != nil {
		return "", -1, fmt.Errorf("invalid port %q: %w", portStr, err)
	}
	port := int(port16) // port is guaranteed to be in the range [0, 65535].

	return host, port, nil
}

// parseIP attempts to parse the entire endpoint as an IP address.
// It returns the normalized string form of the IP if successful,
// or an empty string if parsing fails.
func parseIP(ip string) string {
	// Strip leading and trailing brackets for IPv6 addresses.
	if len(ip) >= 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
		ip = ip[1 : len(ip)-1]
	}
	addr, err := netip.ParseAddr(ip)
	if err != nil {
		return ""
	}
	// Return the normalized string form of the IP.
	return addr.String()
}
