// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package oidc

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/ory/x/otelx"

	"github.com/dghubble/oauth1"
	"github.com/dghubble/oauth1/twitter"
	"github.com/pkg/errors"

	"github.com/ory/herodot"
)

var _ OAuth1Provider = (*ProviderX)(nil)

const xUserInfoBase = "https://api.twitter.com/1.1/account/verify_credentials.json"
const xUserInfoWithEmail = xUserInfoBase + "?include_email=true"

type ProviderX struct {
	config *Configuration
	reg    Dependencies
}

func (p *ProviderX) Config() *Configuration {
	return p.config
}

func NewProviderX(
	config *Configuration,
	reg Dependencies) Provider {
	return &ProviderX{
		config: config,
		reg:    reg,
	}
}

func (p *ProviderX) ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error) {
	requestToken, verifier, err := oauth1.ParseAuthorizationCallback(req)
	if err != nil {
		return nil, err
	}

	accessToken, accessSecret, err := p.OAuth1(ctx).AccessToken(requestToken, "", verifier)
	if err != nil {
		return nil, err
	}

	return oauth1.NewToken(accessToken, accessSecret), nil
}

func (p *ProviderX) AuthURL(ctx context.Context, state string) (_ string, err error) {
	ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.fetch")
	defer otelx.End(span, &err)

	c := p.OAuth1(ctx)

	// We need to cheat so that callback validates on return
	c.CallbackURL = c.CallbackURL + fmt.Sprintf("?state=%s&code=unused", state)

	requestToken, _, err := c.RequestToken()
	if err != nil {
		span.RecordError(err)
		return "", errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReasonf(`Unable to sign in with X because the OAuth1 request token could not be initialized: %s`, err))
	}

	authzURL, err := c.AuthorizationURL(requestToken)
	if err != nil {
		span.RecordError(err)
		return "", errors.WithStack(herodot.ErrInternalServerError.WithWrap(err).WithReasonf(`Unable to sign in with X because the OAuth1 authorization URL could not be parsed: %s`, err))
	}

	return authzURL.String(), nil
}

func (p *ProviderX) CheckError(ctx context.Context, r *http.Request) error {
	if r.URL.Query().Get("denied") == "" {
		return nil
	}

	return errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to sign in with X because the user denied the request.`))
}

func (p *ProviderX) OAuth1(ctx context.Context) *oauth1.Config {
	return &oauth1.Config{
		ConsumerKey:    p.config.ClientID,
		ConsumerSecret: p.config.ClientSecret,
		Endpoint:       twitter.AuthenticateEndpoint,
		CallbackURL:    p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)),
	}
}

func (p *ProviderX) userInfoEndpoint() string {
	for _, scope := range p.config.Scope {
		if scope == "email" {
			return xUserInfoWithEmail
		}
	}

	return xUserInfoBase
}

func (p *ProviderX) Claims(ctx context.Context, token *oauth1.Token) (*Claims, error) {
	ctx = context.WithValue(ctx, oauth1.HTTPClient, p.reg.HTTPClient(ctx).HTTPClient)

	c := p.OAuth1(ctx)
	client := c.Client(ctx, token)
	endpoint := p.userInfoEndpoint()

	resp, err := client.Get(endpoint)
	if err != nil {
		return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
	}
	defer resp.Body.Close()

	if err := logUpstreamError(p.reg.Logger(), resp); err != nil {
		return nil, err
	}

	user := &xUser{}
	if err := json.NewDecoder(resp.Body).Decode(user); err != nil {
		return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
	}

	website := ""
	if user.URL != nil {
		website = *user.URL
	}

	return &Claims{
		Issuer:            endpoint,
		Subject:           user.IDStr,
		Name:              user.Name,
		Picture:           user.ProfileImageURLHTTPS,
		Email:             user.Email,
		PreferredUsername: user.ScreenName,
		Website:           website,
	}, nil
}

type xUser struct {
	ID                     int      `json:"id"`
	IDStr                  string   `json:"id_str"`
	Name                   string   `json:"name"`
	ScreenName             string   `json:"screen_name"`
	Location               string   `json:"location"`
	Description            string   `json:"description"`
	URL                    *string  `json:"url,omitempty"`
	Protected              bool     `json:"protected"`
	FollowersCount         int      `json:"followers_count"`
	FriendsCount           int      `json:"friends_count"`
	ListedCount            int      `json:"listed_count"`
	CreatedAt              string   `json:"created_at"`
	FavouritesCount        int      `json:"favourites_count"`
	Verified               bool     `json:"verified"`
	StatusesCount          int      `json:"statuses_count"`
	DefaultProfile         bool     `json:"default_profile"`
	DefaultProfileImage    bool     `json:"default_profile_image"`
	ProfileImageURLHTTPS   string   `json:"profile_image_url_https"`
	WithheldInCountries    []string `json:"withheld_in_countries"`
	Suspended              bool     `json:"suspended"`
	NeedsPhoneVerification bool     `json:"needs_phone_verification"`
	Email                  string   `json:"email"`
}
