package agd

import (
	"fmt"
	"net/netip"
	"unicode/utf8"

	"github.com/AdguardTeam/AdGuardDNS/internal/agdpasswd"
	"github.com/AdguardTeam/AdGuardDNS/internal/agdvalidate"
	"github.com/AdguardTeam/golibs/errors"
	"github.com/AdguardTeam/golibs/netutil"
)

// Device is a device of a device attached to a profile.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
type Device struct {
	// Auth defines authentication settings of this device.  It's never nil.
	Auth *AuthSettings

	// ID is the unique ID of the device.
	ID DeviceID

	// LinkedIP, when non-empty, allows AdGuard DNS to identify a device by its
	// IP address when it can only use plain DNS.
	LinkedIP netip.Addr

	// Name is the human-readable name of the device.
	Name DeviceName

	// HumanIDLower is the lower-case version of the normalized [HumanID] used
	// to create this device, if any.
	HumanIDLower HumanIDLower

	// DedicatedIPs are the destination (server) IP-addresses dedicated to this
	// device, if any.  A device can use one of these addresses as a DNS server
	// address for AdGuard DNS to recognize it.
	DedicatedIPs []netip.Addr

	// FilteringEnabled defines whether queries from the device should be
	// filtered in any way at all.
	FilteringEnabled bool
}

// DeviceID is the ID of a device attached to a profile.  It is an opaque
// string.
type DeviceID string

// The maximum and minimum lengths of a device ID.
const (
	MaxDeviceIDLen = 8
	MinDeviceIDLen = 1
)

// NewDeviceID converts a simple string into a DeviceID and makes sure that it's
// valid.  This should be preferred to a simple type conversion.
func NewDeviceID(s string) (id DeviceID, err error) {
	// Do not use errors.Annotate here, because it allocates even when the error
	// is nil.
	//
	// TODO(a.garipov): Find out, why does it allocate and perhaps file an
	// issue about that in the Go issue tracker.
	defer func() {
		if err != nil {
			err = fmt.Errorf("bad device id %q: %w", s, err)
		}
	}()

	err = agdvalidate.Inclusion(
		len(s),
		MinDeviceIDLen,
		MaxDeviceIDLen,
		agdvalidate.UnitByte,
	)
	if err != nil {
		// The error will be wrapped by the deferred helper.
		return "", err
	}

	err = netutil.ValidateHostnameLabel(s)
	if err != nil {
		// Unwrap the error to replace the domain name label wrapper message
		// with our own.
		return "", errors.Unwrap(err)
	}

	return DeviceID(s), nil
}

// DeviceName is the human-readable name of a device attached to a profile.
type DeviceName string

// MaxDeviceNameRuneLen is the maximum length of a human-readable device name in
// runes.
const MaxDeviceNameRuneLen = 128

// NewDeviceName converts a simple string into a DeviceName and makes sure that
// it's valid.  This should be preferred to a simple type conversion.
func NewDeviceName(s string) (n DeviceName, err error) {
	// Do not use errors.Annotate here, because it allocates even when the error
	// is nil.
	//
	// TODO(a.garipov): Same as the TODO in NewDeviceID.
	defer func() {
		if err != nil {
			err = fmt.Errorf("bad device name %q: %w", s, err)
		}
	}()

	err = agdvalidate.Inclusion(
		utf8.RuneCountInString(s),
		0,
		MaxDeviceNameRuneLen,
		agdvalidate.UnitRune,
	)
	if err != nil {
		// The error will be wrapped by the deferred helper.
		return "", err
	}

	return DeviceName(s), nil
}

// AuthSettings are the authentication settings of a device.
//
// NOTE: Do not change fields of this structure without incrementing
// [internal/profiledb/internal.FileCacheVersion].
type AuthSettings struct {
	// PasswordHash is the hash of the auth password.  It is never nil.
	PasswordHash agdpasswd.Authenticator

	// Enabled tells whether the authentication should be enabled at all.
	// This must be true in order for all parameters to work.
	Enabled bool

	// DoHAuthOnly defines if the device should only be authenticated through
	// DoH protocol.
	DoHAuthOnly bool
}
