package oauth

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

	"github.com/dchest/uniuri"
	"github.com/form3tech-oss/jwt-go"
	"golang.org/x/oauth2"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/account_sdk"
	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/auth/utils"
	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/rbac"
)

const oAuthRequestTimeout = time.Second * 30

var (
	supportOAuth = map[OAuthType]any{
		Internal:  struct{}{},
		ASKTUG:    struct{}{},
		TiDBCloud: struct{}{},
		PingKai:   struct{}{},
	}

	Authers = make(map[OAuthType]OAuth)
)

func NewOAuthService(cfgs []config.OAuthConfig, cli *account_sdk.Client, rbacAuth rbac.Auth) {
	for _, cfg := range cfgs {
		otype := OAuthType(cfg.Name)
		_, ok := supportOAuth[otype]
		if !ok {
			log.Warn("missing oauth type", zap.String("type", string(otype)))
			continue
		}

		if !cfg.Enabled {
			continue
		}

		oauthCfg := oauth2.Config{
			ClientID:     cfg.ClientId,
			ClientSecret: cfg.ClientSecret,
			Scopes:       utils.SplitString(cfg.Scopes),
			RedirectURL:  cfg.Redirect,
			Endpoint: oauth2.Endpoint{
				TokenURL: cfg.TokenUrl,
				AuthURL:  cfg.AuthUrl,
			},
		}

		switch otype {
		case Internal:
			Authers[otype] = NewInternalOAuth(cfg, oauthCfg, rbacAuth)
		case ASKTUG:
			Authers[otype] = NewAskTUGAuth(cfg, oauthCfg)
		case TiDBCloud:
			Authers[otype] = NewTiDBCloudAuth(cfg, cli)
		case PingKai:
			Authers[otype] = NewPingKaiAuth(cfg, oauthCfg, rbacAuth)
		}
	}
}

type oAuthClient struct {
	oauthStateSecret []byte
	oauth2.Config
}

func (o *oAuthClient) generateAuthCodeURL() (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwt.StandardClaims{
		ExpiresAt: time.Now().Add(time.Minute * 5).Unix(),
	})

	tokenString, err := token.SignedString(o.oauthStateSecret)
	if err != nil {
		return "", errcode.NewInternalErr(err)
	}

	return o.AuthCodeURL(tokenString), nil
}

func (o *oAuthClient) withRedirectURL(redirectURL string) *oAuthClient {
	client := *o
	client.Config.RedirectURL = redirectURL

	return &client
}

func (o *oAuthClient) exchange(ctx context.Context, state, code string) (*oauth2.Token, error) {
	stateToken, err := jwt.ParseWithClaims(state, &jwt.StandardClaims{}, func(token *jwt.Token) (any, error) {
		return o.oauthStateSecret, nil
	})
	if err != nil {
		log.Error("invalid oauth state", zap.String("state", state))
		return nil, errcode.NewInvalidInputErr(errors.Annotate(err, "invalid oauth state"))
	}
	_, ok := stateToken.Claims.(*jwt.StandardClaims)
	if !ok || !stateToken.Valid {
		return nil, errcode.NewInvalidInputErr(errors.Annotate(err, "oauth state is expired"))
	}

	ctx2, cancel := context.WithTimeout(ctx, oAuthRequestTimeout)
	defer cancel()

	oAuthToken, err := o.Exchange(ctx2, code)
	if err != nil {
		return nil, errcode.NewInvalidInputErr(errors.Annotate(err, "exchange oauth code failed"))
	}

	return oAuthToken, nil
}

type OAuth interface {
	Exchange(ctx context.Context, state, code string) (*session.Session, error)
	GenerateAuthCodeURL() (string, error)
	WithRedirectURL(redirectURL string) OAuth
}

type askTUGAuth struct {
	cli *oAuthClient
}

func NewAskTUGAuth(config config.OAuthConfig, oauthCfg oauth2.Config) OAuth {
	return &askTUGAuth{
		cli: &oAuthClient{
			oauthStateSecret: []byte(config.ClientSecret),
			Config:           oauthCfg,
		},
	}
}

func (o *askTUGAuth) Exchange(ctx context.Context, state, code string) (*session.Session, error) {
	token, err := o.cli.exchange(ctx, state, code)
	if err != nil {
		return nil, err
	}

	info, err := decodeToken(token.Extra("id_token").(string))
	if err != nil {
		return nil, err
	}

	if info.Username == "" {
		return nil, errors.New("not found the user")
	}

	userName, err := model.InsertUserIfAbsent(info.Sub, info.Username, info.Email, false)
	if err != nil {
		return nil, err
	}

	return &session.Session{
		// Email:    info.Email,
		CSRFToken: uniuri.New(),
		Username:  userName,
	}, nil
}

func decodeToken(token string) (*UserInfo, error) {
	parts := strings.Split(token, ".")
	if len(parts) != 3 {
		return nil, errors.New("token contains an invalid number of segments")
	}

	data, err := jwt.DecodeSegment(parts[1])
	if err != nil {
		return nil, err
	}

	var u UserInfo
	if err := json.Unmarshal(data, &u); err != nil {
		log.Error("Error decoding user info response", zap.Error(err))
		return nil, err
	}

	return &u, nil
}

func (o *askTUGAuth) GenerateAuthCodeURL() (string, error) {
	return o.cli.generateAuthCodeURL()
}

func (o *askTUGAuth) WithRedirectURL(redirectURL string) OAuth {
	client := *o
	client.cli = o.cli.withRedirectURL(redirectURL)

	return &client
}

type internalOAuth struct {
	apiUrl   string
	cli      *oAuthClient
	rbacAuth rbac.Auth
}

func NewInternalOAuth(config config.OAuthConfig, oauthCfg oauth2.Config, rbacAuth rbac.Auth) OAuth {
	return &internalOAuth{
		apiUrl: config.ApiUrl,
		cli: &oAuthClient{
			oauthStateSecret: []byte(config.ClientSecret),
			Config:           oauthCfg,
		},
		rbacAuth: rbacAuth,
	}
}

func (o *internalOAuth) Exchange(ctx context.Context, state, code string) (*session.Session, error) {
	token, err := o.cli.exchange(ctx, state, code)
	if err != nil {
		return nil, err
	}

	c := o.cli.Client(ctx, token)
	res, err := utils.HttpGet(c, o.apiUrl)
	if err != nil {
		log.Error("get user info failed", zap.String("url", o.apiUrl), zap.Error(err))
		return nil, err
	}

	var u UserInfo
	if err := json.Unmarshal(res.Body, &u); err != nil {
		log.Error("Error decoding user info response", zap.Error(err))
		return nil, err
	}
	u.Internal = true

	if u.Email == "" {
		return nil, fmt.Errorf("no nickname found in userinfo")
	}

	username, err := model.InsertUserIfAbsent(u.Email, u.Email, u.Email, true)
	if err != nil {
		return nil, err
	}
	user, err := model.GetUserByUserName(username)
	if err != nil {
		log.Error("get user by name failed", zap.String("username", username), zap.Error(err))
		return nil, err
	}
	_, err = o.rbacAuth.GetEnforcer().AddGroupingPolicy(fmt.Sprintf("user/%d", user.ID), fmt.Sprintf("role/%s", rbac.RolePingCAPer), "*")
	if err != nil {
		log.Error("add group policy for pingcaper failed", zap.Uint("user id", user.ID), zap.Error(err))
		return nil, err
	}

	return &session.Session{
		Email:     u.Email,
		Username:  username,
		CSRFToken: uniuri.New(),
	}, nil
}

func (o *internalOAuth) WithRedirectURL(redirectURL string) OAuth {
	client := *o
	client.cli = o.cli.withRedirectURL(redirectURL)

	return &client
}

func (o *internalOAuth) GenerateAuthCodeURL() (string, error) {
	return o.cli.generateAuthCodeURL()
}

type UserInfo struct {
	Name        string `json:"name,omitempty"`
	DisplayName string `json:"display_name,omitempty"`
	Login       string `json:"login,omitempty"`
	Username    string `json:"username,omitempty"`
	Email       string `json:"email,omitempty"`
	// asktug
	Sub string `json:"sub,omitempty"`

	Internal bool
}

type tidbCloudAuth struct {
	apiUrl string
	cli    *account_sdk.Client
}

func NewTiDBCloudAuth(config config.OAuthConfig, cli *account_sdk.Client) OAuth {
	return &tidbCloudAuth{
		apiUrl: config.ApiUrl,
		cli:    cli,
	}
}

func (o *tidbCloudAuth) Exchange(ctx context.Context, state, code string) (*session.Session, error) {
	if o.cli != nil {
		u, err := o.cli.CheckToken(ctx, code)
		if err != nil {
			log.Error("request account service failed", zap.Error(err))
			return nil, err
		}

		if u == nil {
			return nil, errors.New("the user is invalid")
		}

		userName, err := model.InsertUserIfAbsent(fmt.Sprintf("tidbcloud-%d", u.Id), u.Email, u.Email, false)
		if err != nil {
			return nil, err
		}
		return &session.Session{
			Email:     u.Email,
			Username:  userName,
			CSRFToken: uniuri.New(),
		}, nil
	}

	orgMe, err := o.requestTiDBCloud("/api/v1/orgs/me", code)
	if err != nil {
		log.Error("request /orgs/me failed", zap.Error(err))
		return nil, err
	}

	var org TiDBCloudOrg
	if err := json.Unmarshal(orgMe, &org); err != nil {
		log.Error("unmarshal TiDBCloudOrg failed", zap.String("data", string(orgMe)), zap.Error(err))
		return nil, err
	}

	userData, err := o.requestTiDBCloud(fmt.Sprintf("/api/v1/orgs/%s/users/me", org.ID), code)
	if err != nil {
		log.Error("request /orgs/users/me failed", zap.String("org", org.ID), zap.Error(err))
		return nil, err
	}

	var user TiDBCloudOrgUser
	if err := json.Unmarshal(userData, &user); err != nil {
		log.Error("unmarshal TiDBCloudOrgUser failed", zap.String("org", org.ID), zap.String("data", string(userData)), zap.Error(err))
		return nil, err
	}

	if user.Email == "" {
		return nil, errors.New("the user is invalid")
	}

	userName, err := model.InsertUserIfAbsent(user.Email, user.Email, user.Email, false)
	if err != nil {
		return nil, err
	}

	return &session.Session{
		Email:     user.Email,
		Username:  userName,
		CSRFToken: uniuri.New(),
	}, nil
}

func (o *tidbCloudAuth) requestTiDBCloud(path, auth string) ([]byte, error) {
	req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", o.apiUrl, path), nil)
	if err != nil {
		log.Error("new request failed", zap.Error(err))
		return nil, err
	}

	req.Header.Set("authorization", fmt.Sprintf("Bearer %s", auth))
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Error("new request failed", zap.String("path", path), zap.Error(err))
		return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Error("response failed", zap.String("path", path), zap.Int("code", resp.StatusCode), zap.String("status", resp.Status))
		if resp.StatusCode == http.StatusUnauthorized {
			return nil, error2.ErrUnauthorized.New("invalid user")
		}
		return nil, fmt.Errorf("return %d code", resp.StatusCode)
	}

	return io.ReadAll(resp.Body)
}

type TiDBCloudOrg struct {
	ID string `json:"id"`
}

type TiDBCloudOrgUser struct {
	Email string `json:"email"`
}

func (o *tidbCloudAuth) GenerateAuthCodeURL() (string, error) {
	return "", nil
}

func (o *tidbCloudAuth) WithRedirectURL(redirectURL string) OAuth {
	return o
}

type pingKaiOAuth struct {
	apiUrl   string
	cli      *oAuthClient
	rbacAuth rbac.Auth
}

func NewPingKaiAuth(config config.OAuthConfig, oauthCfg oauth2.Config, rbacAuth rbac.Auth) OAuth {
	return &pingKaiOAuth{
		apiUrl: config.ApiUrl,
		cli: &oAuthClient{
			oauthStateSecret: []byte(config.ClientSecret),
			Config:           oauthCfg,
		},
		rbacAuth: rbacAuth,
	}
}

func (o *pingKaiOAuth) Exchange(ctx context.Context, state, code string) (*session.Session, error) {
	log.Info("exch ange pinkai", zap.String("state", state), zap.String("code", code))
	token, err := o.cli.exchange(ctx, state, code)
	if err != nil {
		return nil, err
	}

	c := o.cli.Client(ctx, token)
	res, err := utils.HttpGet(c, o.apiUrl)
	if err != nil {
		log.Error("get user info failed", zap.String("url", o.apiUrl), zap.Error(err))
		return nil, err
	}

	var u UserInfo
	if err := json.Unmarshal(res.Body, &u); err != nil {
		log.Error("Error decoding user info response", zap.Error(err))
		return nil, err
	}
	u.Internal = true

	if u.Email == "" {
		return nil, fmt.Errorf("no email found in userinfo")
	}

	username, err := model.InsertUserIfAbsent(u.Email, u.Email, u.Email, true)
	if err != nil {
		return nil, err
	}
	user, err := model.GetUserByUserName(username)
	if err != nil {
		log.Error("get user by name failed", zap.String("username", username), zap.Error(err))
		return nil, err
	}
	_, err = o.rbacAuth.GetEnforcer().AddGroupingPolicy(fmt.Sprintf("user/%d", user.ID), fmt.Sprintf("role/%s", rbac.RolePingCAPer), "*")
	if err != nil {
		log.Error("add group policy for pingcaper failed", zap.Uint("user id", user.ID), zap.Error(err))
		return nil, err
	}

	return &session.Session{
		Email:     u.Email,
		Username:  username,
		CSRFToken: uniuri.New(),
	}, nil
}

func (o *pingKaiOAuth) WithRedirectURL(redirectURL string) OAuth {
	client := *o
	client.cli = o.cli.withRedirectURL(redirectURL)

	return &client
}

func (o *pingKaiOAuth) GenerateAuthCodeURL() (string, error) {
	return o.cli.generateAuthCodeURL()
}
