// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Package oauthkey registers support for using OAuth client secrets to
// automatically request authkeys for logging in.
package oauthkey

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"strings"

	"golang.org/x/oauth2/clientcredentials"
	"tailscale.com/feature"
	"tailscale.com/internal/client/tailscale"
)

func init() {
	feature.Register("oauthkey")
	tailscale.HookResolveAuthKey.Set(resolveAuthKey)
}

// resolveAuthKey either returns v unchanged (in the common case) or, if it
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
//
//	tskey-client-xxxx[?ephemeral=false&bar&preauthorized=BOOL&baseURL=...]
//
// and does the OAuth2 dance to get and return an authkey. The "ephemeral"
// property defaults to true if unspecified. The "preauthorized" defaults to
// false. The "baseURL" defaults to https://api.tailscale.com.
// The passed in tags are required, and must be non-empty. These will be
// set on the authkey generated by the OAuth2 dance.
func resolveAuthKey(ctx context.Context, args tailscale.ResolveAuthKeyArgs) (string, error) {
	if !strings.HasPrefix(args.AuthKey, "tskey-client-") {
		return args.AuthKey, nil
	}
	if len(args.Tags) == 0 {
		return "", errors.New("oauth authkeys require --advertise-tags")
	}

	strippedSecret, ephemeral, preauth, baseURL, err := parseOptionalAttributes(args.AuthKey)
	if err != nil {
		return "", err
	}

	credentials := clientcredentials.Config{
		ClientID:     "some-client-id", // ignored
		ClientSecret: strippedSecret,
		TokenURL:     baseURL + "/api/v2/oauth/token",
	}

	tsClient := tailscale.NewClient("-", nil)
	tsClient.UserAgent = "tailscale-cli"
	tsClient.HTTPClient = credentials.Client(ctx)
	tsClient.BaseURL = baseURL

	caps := tailscale.KeyCapabilities{
		Devices: tailscale.KeyDeviceCapabilities{
			Create: tailscale.KeyDeviceCreateCapabilities{
				Reusable:      false,
				Ephemeral:     ephemeral,
				Preauthorized: preauth,
				Tags:          args.Tags,
			},
		},
	}

	authkey, _, err := tsClient.CreateKey(ctx, caps)
	if err != nil {
		return "", err
	}
	return authkey, nil
}

func parseOptionalAttributes(clientSecret string) (strippedSecret string, ephemeral bool, preauth bool, baseURL string, err error) {
	strippedSecret, named, _ := strings.Cut(clientSecret, "?")
	attrs, err := url.ParseQuery(named)
	if err != nil {
		return "", false, false, "", err
	}
	for k := range attrs {
		switch k {
		case "ephemeral", "preauthorized", "baseURL":
		default:
			return "", false, false, "", fmt.Errorf("unknown attribute %q", k)
		}
	}
	getBool := func(name string, def bool) (bool, error) {
		v := attrs.Get(name)
		if v == "" {
			return def, nil
		}
		ret, err := strconv.ParseBool(v)
		if err != nil {
			return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
		}
		return ret, nil
	}
	ephemeral, err = getBool("ephemeral", true)
	if err != nil {
		return "", false, false, "", err
	}
	preauth, err = getBool("preauthorized", false)
	if err != nil {
		return "", false, false, "", err
	}
	baseURL = "https://api.tailscale.com"
	if v := attrs.Get("baseURL"); v != "" {
		baseURL = v
	}
	return strippedSecret, ephemeral, preauth, baseURL, nil
}
