// 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"
	"strconv"
	"strings"
	"time"

	"golang.org/x/oauth2"
)

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

func NewGithubIdProvider(clientId string, clientSecret string, redirectUrl string) *GithubIdProvider {
	idp := &GithubIdProvider{}

	config := idp.getConfig()
	config.ClientID = clientId
	config.ClientSecret = clientSecret
	config.RedirectURL = redirectUrl
	idp.Config = config

	return idp
}

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

func (idp *GithubIdProvider) getConfig() *oauth2.Config {
	endpoint := oauth2.Endpoint{
		AuthURL:  "https://github.com/login/oauth/authorize",
		TokenURL: "https://github.com/login/oauth/access_token",
	}

	config := &oauth2.Config{
		Scopes:   []string{"user:email", "read:user"},
		Endpoint: endpoint,
	}

	return config
}

type GithubToken struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	Scope       string `json:"scope"`
	Error       string `json:"error"`
}

func (idp *GithubIdProvider) GetToken(code string) (*oauth2.Token, error) {
	params := &struct {
		Code         string `json:"code"`
		ClientId     string `json:"client_id"`
		ClientSecret string `json:"client_secret"`
	}{code, idp.Config.ClientID, idp.Config.ClientSecret}
	data, err := idp.postWithBody(params, idp.Config.Endpoint.TokenURL)
	if err != nil {
		return nil, err
	}
	pToken := &GithubToken{}
	if err = json.Unmarshal(data, pToken); err != nil {
		return nil, err
	}
	if pToken.Error != "" {
		return nil, fmt.Errorf("err: %s", pToken.Error)
	}

	token := &oauth2.Token{
		AccessToken: pToken.AccessToken,
		TokenType:   "Bearer",
	}

	return token, nil
}

//{
//	"login": "jimgreen",
//	"id": 3781234,
//	"node_id": "MDQ6VXNlcjM3O123456=",
//	"avatar_url": "https://avatars.githubusercontent.com/u/3781234?v=4",
//	"gravatar_id": "",
//	"url": "https://api.github.com/users/jimgreen",
//	"html_url": "https://github.com/jimgreen",
//	"followers_url": "https://api.github.com/users/jimgreen/followers",
//	"following_url": "https://api.github.com/users/jimgreen/following{/other_user}",
//	"gists_url": "https://api.github.com/users/jimgreen/gists{/gist_id}",
//	"starred_url": "https://api.github.com/users/jimgreen/starred{/owner}{/repo}",
//	"subscriptions_url": "https://api.github.com/users/jimgreen/subscriptions",
//	"organizations_url": "https://api.github.com/users/jimgreen/orgs",
//	"repos_url": "https://api.github.com/users/jimgreen/repos",
//	"events_url": "https://api.github.com/users/jimgreen/events{/privacy}",
//	"received_events_url": "https://api.github.com/users/jimgreen/received_events",
//	"type": "User",
//	"site_admin": false,
//	"name": "Jim Green",
//	"company": "Casbin",
//	"blog": "https://casbin.org",
//	"location": "Bay Area",
//	"email": "jimgreen@gmail.com",
//	"hireable": true,
//	"bio": "My bio",
//	"twitter_username": null,
//	"public_repos": 45,
//	"public_gists": 3,
//	"followers": 123,
//	"following": 31,
//	"created_at": "2016-03-06T13:16:13Z",
//	"updated_at": "2020-05-30T12:15:29Z",
//	"private_gists": 0,
//	"total_private_repos": 12,
//	"owned_private_repos": 12,
//	"disk_usage": 46331,
//	"collaborators": 5,
//	"two_factor_authentication": true,
//	"plan": {
//		"name": "free",
//		"space": 976562499,
//		"collaborators": 0,
//		"private_repos": 10000
//	}
//}

type GitHubUserInfo struct {
	Login                   string      `json:"login"`
	Id                      int         `json:"id"`
	NodeId                  string      `json:"node_id"`
	AvatarUrl               string      `json:"avatar_url"`
	GravatarId              string      `json:"gravatar_id"`
	Url                     string      `json:"url"`
	HtmlUrl                 string      `json:"html_url"`
	FollowersUrl            string      `json:"followers_url"`
	FollowingUrl            string      `json:"following_url"`
	GistsUrl                string      `json:"gists_url"`
	StarredUrl              string      `json:"starred_url"`
	SubscriptionsUrl        string      `json:"subscriptions_url"`
	OrganizationsUrl        string      `json:"organizations_url"`
	ReposUrl                string      `json:"repos_url"`
	EventsUrl               string      `json:"events_url"`
	ReceivedEventsUrl       string      `json:"received_events_url"`
	Type                    string      `json:"type"`
	SiteAdmin               bool        `json:"site_admin"`
	Name                    string      `json:"name"`
	Company                 string      `json:"company"`
	Blog                    string      `json:"blog"`
	Location                string      `json:"location"`
	Email                   string      `json:"email"`
	Hireable                bool        `json:"hireable"`
	Bio                     string      `json:"bio"`
	TwitterUsername         interface{} `json:"twitter_username"`
	PublicRepos             int         `json:"public_repos"`
	PublicGists             int         `json:"public_gists"`
	Followers               int         `json:"followers"`
	Following               int         `json:"following"`
	CreatedAt               time.Time   `json:"created_at"`
	UpdatedAt               time.Time   `json:"updated_at"`
	PrivateGists            int         `json:"private_gists"`
	TotalPrivateRepos       int         `json:"total_private_repos"`
	OwnedPrivateRepos       int         `json:"owned_private_repos"`
	DiskUsage               int         `json:"disk_usage"`
	Collaborators           int         `json:"collaborators"`
	TwoFactorAuthentication bool        `json:"two_factor_authentication"`
	Plan                    struct {
		Name          string `json:"name"`
		Space         int    `json:"space"`
		Collaborators int    `json:"collaborators"`
		PrivateRepos  int    `json:"private_repos"`
	} `json:"plan"`
}

type GitHubUserEmailInfo struct {
	Email      string `json:"email"`
	Primary    bool   `json:"primary"`
	Verified   bool   `json:"verified"`
	Visibility string `json:"visibility"`
}

type GitHubErrorInfo struct {
	Message          string `json:"message"`
	DocumentationUrl string `json:"documentation_url"`
	Status           string `json:"status"`
}

func (idp *GithubIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
	req, err := http.NewRequest("GET", "https://api.github.com/user", nil)
	if err != nil {
		return nil, err
	}
	req.Header.Add("Authorization", "token "+token.AccessToken)
	resp, err := idp.Client.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

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

	var githubUserInfo GitHubUserInfo
	err = json.Unmarshal(body, &githubUserInfo)
	if err != nil {
		return nil, err
	}

	if githubUserInfo.Email == "" {
		reqEmail, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
		if err != nil {
			return nil, err
		}
		reqEmail.Header.Add("Authorization", "token "+token.AccessToken)
		respEmail, err := idp.Client.Do(reqEmail)
		if err != nil {
			return nil, err
		}

		defer respEmail.Body.Close()
		emailBody, err := io.ReadAll(respEmail.Body)
		if err != nil {
			return nil, err
		}

		if respEmail.StatusCode != 200 {
			var errMessage GitHubErrorInfo
			err = json.Unmarshal(emailBody, &errMessage)
			if err != nil {
				return nil, err
			}

			fmt.Printf("GithubIdProvider:GetUserInfo() error, status code = %d, error message = %v\n", respEmail.StatusCode, errMessage)
		} else {
			var userEmails []GitHubUserEmailInfo
			err = json.Unmarshal(emailBody, &userEmails)
			if err != nil {
				return nil, err
			}

			githubUserInfo.Email = idp.getEmailFromEmailsResult(userEmails)
		}
	}

	userInfo := UserInfo{
		Id:          strconv.Itoa(githubUserInfo.Id),
		Username:    githubUserInfo.Login,
		DisplayName: githubUserInfo.Name,
		Email:       githubUserInfo.Email,
		AvatarUrl:   githubUserInfo.AvatarUrl,
	}
	return &userInfo, nil
}

func (idp *GithubIdProvider) postWithBody(body interface{}, url string) ([]byte, error) {
	bs, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	r := strings.NewReader(string(bs))
	req, _ := http.NewRequest("POST", url, r)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	resp, err := idp.Client.Do(req)
	if err != nil {
		return nil, err
	}
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
			return
		}
	}(resp.Body)

	return data, nil
}

func (idp *GithubIdProvider) getEmailFromEmailsResult(emailInfo []GitHubUserEmailInfo) string {
	primaryEmail := ""
	verifiedEmail := ""

	for _, addr := range emailInfo {
		if !addr.Verified || strings.Contains(addr.Email, "users.noreply.github.com") {
			continue
		}

		if addr.Primary {
			primaryEmail = addr.Email
			break
		} else if verifiedEmail == "" {
			verifiedEmail = addr.Email
		}
	}

	if primaryEmail != "" {
		return primaryEmail
	}

	return verifiedEmail
}
