package auth

import (
	"context"
	"encoding/json"
	"errors"
	"net/url"
	"slices"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/sigbit/mcp-auth-proxy/v2/pkg/utils"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/github"
)

type githubProvider struct {
	endpoint     string
	oauth2       oauth2.Config
	allowedUsers []string
	allowedOrgs  []string
}

func NewGithubProvider(oauthEndpoint, apiURL, clientID, clientSecret, externalURL string, allowedUsers []string, allowedOrgs []string) (Provider, error) {
	r, err := url.JoinPath(externalURL, GitHubCallbackEndpoint)
	if err != nil {
		return nil, err
	}
	scopes := []string{}
	if len(allowedOrgs) > 0 {
		scopes = append(scopes, "read:org")
	}
	if apiURL == "" {
		apiURL = "https://api.github.com"
	}
	oauth2Endpoint := github.Endpoint
	if oauthEndpoint != "" {
		authURL, err := url.JoinPath(oauthEndpoint, "/login/oauth/authorize")
		if err != nil {
			return nil, err
		}
		tokenURL, err := url.JoinPath(oauthEndpoint, "/login/oauth/access_token")
		if err != nil {
			return nil, err
		}
		deviceAuthURL, err := url.JoinPath(oauthEndpoint, "/login/device/code")
		if err != nil {
			return nil, err
		}
		oauth2Endpoint = oauth2.Endpoint{
			AuthURL:       authURL,
			TokenURL:      tokenURL,
			DeviceAuthURL: deviceAuthURL,
		}
	}
	return &githubProvider{
		endpoint: apiURL,
		oauth2: oauth2.Config{
			ClientID:     clientID,
			ClientSecret: clientSecret,
			RedirectURL:  r,
			Scopes:       scopes,
			Endpoint:     oauth2Endpoint,
		},
		allowedUsers: allowedUsers,
		allowedOrgs:  allowedOrgs,
	}, nil
}

func (p *githubProvider) Name() string {
	return "GitHub"
}

func (p *githubProvider) Type() string {
	return "github"
}

func (p *githubProvider) RedirectURL() string {
	return GitHubCallbackEndpoint
}

func (p *githubProvider) AuthURL() string {
	return GitHubAuthEndpoint
}

func (p *githubProvider) AuthCodeURL(state string) (string, error) {
	authURL := p.oauth2.AuthCodeURL(state)
	return authURL, nil
}

func (p *githubProvider) Exchange(c *gin.Context, state string) (*oauth2.Token, error) {
	if c.Query("state") != state {
		return nil, errors.New("invalid OAuth state")
	}
	code := c.Query("code")
	token, err := p.oauth2.Exchange(c, code)
	if err != nil {
		return nil, err
	}
	return token, nil
}

func (p *githubProvider) Authorization(ctx context.Context, token *oauth2.Token) (bool, string, map[string]any, error) {
	client := p.oauth2.Client(ctx, token)
	resp1, err := client.Get(utils.Must(url.JoinPath(p.endpoint, "/user")))
	if err != nil {
		return false, "", nil, err
	}
	if resp1.StatusCode < 200 || resp1.StatusCode >= 300 {
		return false, "", nil, errors.New("failed to get user info from GitHub API: " + resp1.Status)
	}
	defer resp1.Body.Close()

	var userInfoMap map[string]any
	if err := json.NewDecoder(resp1.Body).Decode(&userInfoMap); err != nil {
		return false, "", nil, err
	}

	login, _ := userInfoMap["login"].(string)

	if len(p.allowedUsers) == 0 && len(p.allowedOrgs) == 0 {
		return true, login, userInfoMap, nil
	}

	if slices.Contains(p.allowedUsers, login) {
		return true, login, userInfoMap, nil
	}

	allowedOrgTeams := []string{}
	allowedOrgs := []string{}
	for _, allowedOrg := range p.allowedOrgs {
		if strings.Contains(allowedOrg, ":") {
			allowedOrgTeams = append(allowedOrgTeams, allowedOrg)
		} else {
			allowedOrgs = append(allowedOrgs, allowedOrg)
		}
	}

	if len(allowedOrgs) > 0 {
		resp2, err := client.Get(utils.Must(url.JoinPath(p.endpoint, "/user/orgs")))
		if err != nil {
			return false, "", nil, err
		}
		if resp2.StatusCode < 200 || resp2.StatusCode >= 300 {
			return false, "", nil, errors.New("failed to get user info from GitHub API: " + resp2.Status)
		}
		defer resp2.Body.Close()
		var orgInfo []struct {
			Login string `json:"login"`
		}
		if err := json.NewDecoder(resp2.Body).Decode(&orgInfo); err != nil {
			return false, "", nil, err
		}
		for _, o := range orgInfo {
			if slices.Contains(allowedOrgs, o.Login) {
				return true, login, userInfoMap, nil
			}
		}
	}
	if len(allowedOrgTeams) > 0 {
		resp3, err := client.Get(utils.Must(url.JoinPath(p.endpoint, "/user/teams")))
		if err != nil {
			return false, "", nil, err
		}
		if resp3.StatusCode < 200 || resp3.StatusCode >= 300 {
			return false, "", nil, errors.New("failed to get user info from GitHub API: " + resp3.Status)
		}
		defer resp3.Body.Close()
		var teamInfo []struct {
			Organization struct {
				Login string `json:"login"`
			} `json:"organization"`
			Slug string `json:"slug"`
		}
		if err := json.NewDecoder(resp3.Body).Decode(&teamInfo); err != nil {
			return false, "", nil, err
		}
		for _, team := range teamInfo {
			if slices.Contains(allowedOrgTeams, team.Organization.Login+":"+team.Slug) {
				return true, login, userInfoMap, nil
			}
		}
	}

	return false, login, userInfoMap, nil
}
