// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package idp

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"golang.org/x/oauth2"
)

type LinkedInIdProvider struct {
	Client *http.Client
	Config *oauth2.Config
}

func NewLinkedInIdProvider(clientId string, clientSecret string, redirectUrl string) *LinkedInIdProvider {
	idp := &LinkedInIdProvider{}

	config := idp.getConfig(clientId, clientSecret, redirectUrl)
	idp.Config = config

	return idp
}

func (idp *LinkedInIdProvider) SetHttpClient(client *http.Client) {
	idp.Client = client
}

// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
func (idp *LinkedInIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
	endpoint := oauth2.Endpoint{
		TokenURL: "https://www.linkedIn.com/oauth/v2/accessToken",
	}

	config := &oauth2.Config{
		Scopes:       []string{"email,public_profile"},
		Endpoint:     endpoint,
		ClientID:     clientId,
		ClientSecret: clientSecret,
		RedirectURL:  redirectUrl,
	}

	return config
}

type LinkedInAccessToken struct {
	AccessToken string `json:"access_token"` // Interface call credentials
	ExpiresIn   int64  `json:"expires_in"`   // access_token interface call credential timeout time, unit (seconds)
}

// GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://docs.microsoft.com/en-us/linkedIn/shared/authentication/authorization-code-flow?context=linkedIn%2Fcontext&tabs=HTTPS
func (idp *LinkedInIdProvider) GetToken(code string) (*oauth2.Token, error) {
	params := url.Values{}
	params.Add("grant_type", "authorization_code")
	params.Add("redirect_uri", idp.Config.RedirectURL)
	params.Add("client_id", idp.Config.ClientID)
	params.Add("client_secret", idp.Config.ClientSecret)
	params.Add("code", code)

	accessTokenUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.TokenURL, params.Encode())
	bs, _ := json.Marshal(params.Encode())
	req, _ := http.NewRequest("POST", accessTokenUrl, strings.NewReader(string(bs)))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	rbs, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	tokenResp := LinkedInAccessToken{}
	if err = json.Unmarshal(rbs, &tokenResp); err != nil {
		return nil, err
	}

	token := &oauth2.Token{
		AccessToken: tokenResp.AccessToken,
		TokenType:   "Bearer",
		Expiry:      time.Unix(time.Now().Unix()+tokenResp.ExpiresIn, 0),
	}

	return token, nil
}

/*
{
    "firstName": {
        "localized": {
            "zh_CN": "继坤"
        },
        "preferredLocale": {
            "country": "CN",
            "language": "zh"
        }
    },
    "lastName": {
        "localized": {
            "zh_CN": "刘"
        },
        "preferredLocale": {
            "country": "CN",
            "language": "zh"
        }
    },
    "profilePicture": {
        "displayImage": "urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg",
        "displayImage~": {
            "paging": {
                "count": 10,
                "start": 0,
                "links": []
            },
            "elements": [
                {
                    "artifact": "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100)",
                    "authorizationMethod": "PUBLIC",
                    "data": {
                        "com.linkedin.digitalmedia.mediaartifact.StillImage": {
                            "mediaType": "image/jpeg",
                            "rawCodecSpec": {
                                "name": "jpeg",
                                "type": "image"
                            },
                            "displaySize": {
                                "width": 100.0,
                                "uom": "PX",
                                "height": 100.0
                            },
                            "storageSize": {
                                "width": 100,
                                "height": 100
                            },
                            "storageAspectRatio": {
                                "widthAspect": 1.0,
                                "heightAspect": 1.0,
                                "formatted": "1.00:1.00"
                            },
                            "displayAspectRatio": {
                                "widthAspect": 1.0,
                                "heightAspect": 1.0,
                                "formatted": "1.00:1.00"
                            }
                        }
                    },
                    "identifiers": [
                        {
                            "identifier": "https://media.licdn.cn/dms/image/C5603AQHbdR8RkG62yg/profile-displayphoto-shrink_100_100/0/1625279434135?e=1630540800&v=beta&t=Z-bQKf_jFv8L1uwr6X5AJLoTQRWZrueT7qrITDSvxWM",
                            "index": 0,
                            "mediaType": "image/jpeg",
                            "file": "urn:li:digitalmediaFile:(urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100,0)",
                            "identifierType": "EXTERNAL_URL",
                            "identifierExpiresInSeconds": 1630540800
                        }
                    ]
                },
				// ...
                }
            ]
        }
    },
    "id": "vvMfLsLIRs"
}
*/

type LinkedInUserInfo struct {
	FirstName struct {
		Localized       map[string]string `json:"localized"`
		PreferredLocale struct {
			Country  string `json:"country"`
			Language string `json:"language"`
		} `json:"preferredLocale"`
	} `json:"firstName"`
	LastName struct {
		Localized       map[string]string `json:"localized"`
		PreferredLocale struct {
			Country  string `json:"country"`
			Language string `json:"language"`
		} `json:"preferredLocale"`
	} `json:"lastName"`
	ProfilePicture struct {
		DisplayImage  string `json:"displayImage"`
		DisplayImage1 struct {
			Paging struct {
				Count int           `json:"count"`
				Start int           `json:"start"`
				Links []interface{} `json:"links"`
			} `json:"paging"`
			Elements []struct {
				Artifact            string `json:"artifact"`
				AuthorizationMethod string `json:"authorizationMethod"`
				Data                struct {
					ComLinkedinDigitalmediaMediaartifactStillImage struct {
						MediaType    string `json:"mediaType"`
						RawCodecSpec struct {
							Name string `json:"name"`
							Type string `json:"type"`
						} `json:"rawCodecSpec"`
						DisplaySize struct {
							Width  float64 `json:"width"`
							Uom    string  `json:"uom"`
							Height float64 `json:"height"`
						} `json:"displaySize"`
						StorageSize struct {
							Width  int `json:"width"`
							Height int `json:"height"`
						} `json:"storageSize"`
						StorageAspectRatio struct {
							WidthAspect  float64 `json:"widthAspect"`
							HeightAspect float64 `json:"heightAspect"`
							Formatted    string  `json:"formatted"`
						} `json:"storageAspectRatio"`
						DisplayAspectRatio struct {
							WidthAspect  float64 `json:"widthAspect"`
							HeightAspect float64 `json:"heightAspect"`
							Formatted    string  `json:"formatted"`
						} `json:"displayAspectRatio"`
					} `json:"com.linkedin.digitalmedia.mediaartifact.StillImage"`
				} `json:"data"`
				Identifiers []struct {
					Identifier                 string `json:"identifier"`
					Index                      int    `json:"index"`
					MediaType                  string `json:"mediaType"`
					File                       string `json:"file"`
					IdentifierType             string `json:"identifierType"`
					IdentifierExpiresInSeconds int    `json:"identifierExpiresInSeconds"`
				} `json:"identifiers"`
			} `json:"elements"`
		} `json:"displayImage~"`
	} `json:"profilePicture"`
	Id string `json:"id"`
}

/*
{
    "handle": "urn:li:emailAddress:3775708763",
    "handle~": {
        "emailAddress": "hsimpson@linkedin.com"
    }
}
*/

type LinkedInUserEmail struct {
	Elements []struct {
		Handle struct {
			EmailAddress string `json:"emailAddress"`
		} `json:"handle~"`
		Handle1 string `json:"handle"`
	} `json:"elements"`
}

// GetUserInfo use LinkedInAccessToken gotten before return LinkedInUserInfo
// get more detail via: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context
func (idp *LinkedInIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
	var linkedInUserInfo LinkedInUserInfo
	bs, err := idp.GetUrlRespWithAuthorization("https://api.linkedIn.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))", token.AccessToken)
	if err != nil {
		return nil, err
	}
	if err = json.Unmarshal(bs, &linkedInUserInfo); err != nil {
		return nil, err
	}

	var linkedInUserEmail LinkedInUserEmail
	bs, err = idp.GetUrlRespWithAuthorization("https://api.linkedIn.com/v2/emailAddress?q=members&projection=(elements*(handle~))", token.AccessToken)
	if err != nil {
		return nil, err
	}
	if err = json.Unmarshal(bs, &linkedInUserEmail); err != nil {
		return nil, err
	}

	username := ""
	for _, name := range linkedInUserInfo.FirstName.Localized {
		username += name
	}
	for _, name := range linkedInUserInfo.LastName.Localized {
		username += name
	}
	userInfo := UserInfo{
		Id:          linkedInUserInfo.Id,
		DisplayName: username,
		Username:    username,
		Email:       linkedInUserEmail.Elements[0].Handle.EmailAddress,
		AvatarUrl:   linkedInUserInfo.ProfilePicture.DisplayImage1.Elements[0].Identifiers[0].Identifier,
	}
	return &userInfo, nil
}

func (idp *LinkedInIdProvider) GetUrlRespWithAuthorization(url, token string) ([]byte, error) {
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
			return
		}
	}(resp.Body)

	bs, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return bs, nil
}
