package op

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"math/big"
	"net/http"
	"net/url"
	"slices"
	"strings"
	"time"

	httphelper "github.com/zitadel/oidc/v3/pkg/http"
	"github.com/zitadel/oidc/v3/pkg/oidc"
)

type DeviceAuthorizationConfig struct {
	Lifetime     time.Duration
	PollInterval time.Duration

	// UserFormURL is the complete URL where the user must go to authorize the device.
	// Deprecated: use UserFormPath instead.
	UserFormURL string

	// UserFormPath is the path where the user must go to authorize the device.
	// The hostname for the URL is taken from the request by IssuerFromContext.
	UserFormPath string
	UserCode     UserCodeConfig
}

type UserCodeConfig struct {
	CharSet      string
	CharAmount   int
	DashInterval int
}

const (
	CharSetBase20 = "BCDFGHJKLMNPQRSTVWXZ"
	CharSetDigits = "0123456789"
)

var (
	UserCodeBase20 = UserCodeConfig{
		CharSet:      CharSetBase20,
		CharAmount:   8,
		DashInterval: 4,
	}
	UserCodeDigits = UserCodeConfig{
		CharSet:      CharSetDigits,
		CharAmount:   9,
		DashInterval: 3,
	}
)

func DeviceAuthorizationHandler(o OpenIDProvider) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := DeviceAuthorization(w, r, o); err != nil {
			RequestError(w, r, err, o.Logger())
		}
	}
}

func DeviceAuthorization(w http.ResponseWriter, r *http.Request, o OpenIDProvider) error {
	ctx, span := Tracer.Start(r.Context(), "DeviceAuthorization")
	r = r.WithContext(ctx)
	defer span.End()

	req, err := ParseDeviceCodeRequest(r, o)
	if err != nil {
		return err
	}
	response, err := createDeviceAuthorization(r.Context(), req, req.ClientID, o)
	if err != nil {
		return err
	}

	httphelper.MarshalJSON(w, response)
	return nil
}

func createDeviceAuthorization(ctx context.Context, req *oidc.DeviceAuthorizationRequest, clientID string, o OpenIDProvider) (*oidc.DeviceAuthorizationResponse, error) {
	ctx, span := Tracer.Start(ctx, "createDeviceAuthorization")
	defer span.End()

	storage, err := assertDeviceStorage(o.Storage())
	if err != nil {
		return nil, err
	}
	config := o.DeviceAuthorization()

	deviceCode, _ := NewDeviceCode(RecommendedDeviceCodeBytes)
	userCode, err := NewUserCode([]rune(config.UserCode.CharSet), config.UserCode.CharAmount, config.UserCode.DashInterval)
	if err != nil {
		return nil, NewStatusError(err, http.StatusInternalServerError)
	}

	expires := time.Now().Add(config.Lifetime)
	err = storage.StoreDeviceAuthorization(ctx, clientID, deviceCode, userCode, expires, req.Scopes)
	if err != nil {
		return nil, NewStatusError(err, http.StatusInternalServerError)
	}

	var verification *url.URL
	if config.UserFormURL != "" {
		if verification, err = url.Parse(config.UserFormURL); err != nil {
			err = oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for device user form")
			return nil, NewStatusError(err, http.StatusInternalServerError)
		}
	} else {
		if verification, err = url.Parse(IssuerFromContext(ctx)); err != nil {
			err = oidc.ErrServerError().WithParent(err).WithDescription("invalid URL for issuer")
			return nil, NewStatusError(err, http.StatusInternalServerError)
		}
		verification.Path = config.UserFormPath
	}

	response := &oidc.DeviceAuthorizationResponse{
		DeviceCode:      deviceCode,
		UserCode:        userCode,
		VerificationURI: verification.String(),
		ExpiresIn:       int(config.Lifetime / time.Second),
		Interval:        int(config.PollInterval / time.Second),
	}

	verification.RawQuery = "user_code=" + userCode
	response.VerificationURIComplete = verification.String()
	return response, nil
}

func ParseDeviceCodeRequest(r *http.Request, o OpenIDProvider) (*oidc.DeviceAuthorizationRequest, error) {
	ctx, span := Tracer.Start(r.Context(), "ParseDeviceCodeRequest")
	r = r.WithContext(ctx)
	defer span.End()

	clientID, _, err := ClientIDFromRequest(r, o)
	if err != nil {
		return nil, err
	}
	client, err := o.Storage().GetClientByClientID(r.Context(), clientID)
	if err != nil {
		return nil, err
	}
	if !ValidateGrantType(client, oidc.GrantTypeDeviceCode) {
		return nil, oidc.ErrUnauthorizedClient().WithDescription("client missing grant type " + string(oidc.GrantTypeDeviceCode))
	}

	req := new(oidc.DeviceAuthorizationRequest)
	if err := o.Decoder().Decode(req, r.Form); err != nil {
		return nil, oidc.ErrInvalidRequest().WithDescription("cannot parse device authentication request").WithParent(err)
	}
	req.ClientID = clientID

	return req, nil
}

// 16 bytes gives 128 bit of entropy.
// results in a 22 character base64 encoded string.
const RecommendedDeviceCodeBytes = 16

// NewDeviceCode generates a new cryptographically secure device code as a base64 encoded string.
// The length of the string is nBytes * 4 / 3.
// An error is never returned.
//
// TODO(v4): change return type to string alone.
func NewDeviceCode(nBytes int) (string, error) {
	bytes := make([]byte, nBytes)
	rand.Read(bytes)
	return base64.RawURLEncoding.EncodeToString(bytes), nil
}

func NewUserCode(charSet []rune, charAmount, dashInterval int) (string, error) {
	var buf strings.Builder
	if dashInterval > 0 {
		buf.Grow(charAmount + charAmount/dashInterval - 1)
	} else {
		buf.Grow(charAmount)
	}

	max := big.NewInt(int64(len(charSet)))

	for i := 0; i < charAmount; i++ {
		if dashInterval != 0 && i != 0 && i%dashInterval == 0 {
			buf.WriteByte('-')
		}

		bi, err := rand.Int(rand.Reader, max)
		if err != nil {
			return "", fmt.Errorf("%w getting entropy for user code", err)
		}

		buf.WriteRune(charSet[int(bi.Int64())])
	}

	return buf.String(), nil
}

func DeviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) {
	ctx, span := Tracer.Start(r.Context(), "DeviceAccessToken")
	defer span.End()
	r = r.WithContext(ctx)

	if err := deviceAccessToken(w, r, exchanger); err != nil {
		RequestError(w, r, err, exchanger.Logger())
	}
}

func deviceAccessToken(w http.ResponseWriter, r *http.Request, exchanger Exchanger) error {
	// use a limited context timeout shorter as the default
	// poll interval of 5 seconds.
	ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
	defer cancel()
	r = r.WithContext(ctx)

	clientID, clientAuthenticated, err := ClientIDFromRequest(r, exchanger)
	if err != nil {
		return err
	}

	req, err := ParseDeviceAccessTokenRequest(r, exchanger)
	if err != nil {
		return err
	}
	tokenRequest, err := CheckDeviceAuthorizationState(ctx, clientID, req.DeviceCode, exchanger)
	if err != nil {
		return err
	}

	client, err := exchanger.Storage().GetClientByClientID(ctx, clientID)
	if err != nil {
		return err
	}
	if clientAuthenticated != IsConfidentialType(client) {
		return oidc.ErrInvalidClient().WithParent(ErrNoClientCredentials).
			WithDescription("confidential client requires authentication")
	}

	resp, err := CreateDeviceTokenResponse(r.Context(), tokenRequest, exchanger, client)
	if err != nil {
		return err
	}

	httphelper.MarshalJSON(w, resp)
	return nil
}

func ParseDeviceAccessTokenRequest(r *http.Request, exchanger Exchanger) (*oidc.DeviceAccessTokenRequest, error) {
	req := new(oidc.DeviceAccessTokenRequest)
	if err := exchanger.Decoder().Decode(req, r.PostForm); err != nil {
		return nil, err
	}
	return req, nil
}

// DeviceAuthorizationState describes the current state of
// the device authorization flow.
// It implements the [IDTokenRequest] interface.
type DeviceAuthorizationState struct {
	ClientID string
	Audience []string
	Scopes   []string
	Expires  time.Time // The time after we consider the authorization request timed-out
	Done     bool      // The user authenticated and approved the authorization request
	Denied   bool      // The user authenticated and denied the authorization request

	// The following fields are populated after Done == true
	Subject  string
	AMR      []string
	AuthTime time.Time
}

func (r *DeviceAuthorizationState) GetAMR() []string {
	return r.AMR
}

func (r *DeviceAuthorizationState) GetAudience() []string {
	if !slices.Contains(r.Audience, r.ClientID) {
		r.Audience = append(r.Audience, r.ClientID)
	}
	return r.Audience
}

func (r *DeviceAuthorizationState) GetAuthTime() time.Time {
	return r.AuthTime
}

func (r *DeviceAuthorizationState) GetClientID() string {
	return r.ClientID
}

func (r *DeviceAuthorizationState) GetScopes() []string {
	return r.Scopes
}

func (r *DeviceAuthorizationState) GetSubject() string {
	return r.Subject
}

func CheckDeviceAuthorizationState(ctx context.Context, clientID, deviceCode string, exchanger Exchanger) (*DeviceAuthorizationState, error) {
	ctx, span := Tracer.Start(ctx, "CheckDeviceAuthorizationState")
	defer span.End()

	storage, err := assertDeviceStorage(exchanger.Storage())
	if err != nil {
		return nil, err
	}

	state, err := storage.GetDeviceAuthorizatonState(ctx, clientID, deviceCode)
	if errors.Is(err, context.DeadlineExceeded) {
		return nil, oidc.ErrSlowDown().WithParent(err)
	}
	if err != nil {
		return nil, oidc.ErrAccessDenied().WithParent(err)
	}
	if state.Denied {
		return state, oidc.ErrAccessDenied()
	}
	if state.Done {
		return state, nil
	}
	if time.Now().After(state.Expires) {
		return state, oidc.ErrExpiredDeviceCode()
	}
	return state, oidc.ErrAuthorizationPending()
}

func CreateDeviceTokenResponse(ctx context.Context, tokenRequest TokenRequest, creator TokenCreator, client Client) (*oidc.AccessTokenResponse, error) {
	/* TODO(v4):
	Change the TokenRequest argument type to *DeviceAuthorizationState.
	Breaking change that can not be done for v3.
	*/
	ctx, span := Tracer.Start(ctx, "CreateDeviceTokenResponse")
	defer span.End()

	accessToken, refreshToken, validity, err := CreateAccessToken(ctx, tokenRequest, client.AccessTokenType(), creator, client, "")
	if err != nil {
		return nil, err
	}

	response := &oidc.AccessTokenResponse{
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		TokenType:    oidc.BearerToken,
		ExpiresIn:    uint64(validity.Seconds()),
		Scope:        tokenRequest.GetScopes(),
	}

	// TODO(v4): remove type assertion
	if idTokenRequest, ok := tokenRequest.(IDTokenRequest); ok && slices.Contains(tokenRequest.GetScopes(), oidc.ScopeOpenID) {
		response.IDToken, err = CreateIDToken(ctx, IssuerFromContext(ctx), idTokenRequest, client.IDTokenLifetime(), accessToken, "", creator.Storage(), client)
		if err != nil {
			return nil, err
		}
	}

	return response, nil
}
