package orgsso

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"sync"

	"github.com/tidbcloud/account/commons/database"

	"github.com/tidbcloud/account/internel/dao/role"

	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/sso"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/tidbcloud/account/internel/dao"
	"gorm.io/gorm"
)

const (
	listOktaUsersUrlFormat = "https://%s/api/v1/users?limit=%d"
	tokenHeaderFormat      = "SSWS %s"
	oktaUserActiveStatus   = "ACTIVE"
)

var linkCompile = regexp.MustCompile("^<(.+)>; rel=\"next\"$")

type SyncUserSvcIface interface {
	StartSyncOktaUsers(ctx context.Context)
}

var (
	syncUserSvcInitOnce sync.Once
	syncUserSvcInstance SyncUserSvcIface
)

func GetSyncUserSvc() (SyncUserSvcIface, error) {
	syncUserSvcInitOnce.Do(func() {
		syncUserSvcInstance = NewSyncUserSvc(database.DBConn.DB, dao.GetDaoHub())

	})
	return syncUserSvcInstance, nil
}

type SyncUserSvc struct {
	db  *gorm.DB
	hub *dao.DaoHub
}

func NewSyncUserSvc(db *gorm.DB, hub *dao.DaoHub) SyncUserSvcIface {
	return &SyncUserSvc{
		db:  db,
		hub: hub,
	}
}

func (s *SyncUserSvc) StartSyncOktaUsers(ctx context.Context) {
	methods, err := s.hub.OrgAuthMethod.ListOrgAuthMethods(ctx, s.db, sso.OrgAuthMethodFilter{
		Types: []entity.AuthMethodType{entity.AuthMethodTypeOIDC},
	})
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers ListOrgAuthMethods error", zap.Any("err", err))
		return
	}
	for _, method := range methods {
		oidcInfo := getOidcInfoFromContent(ctx, method.Content)
		err := s.syncOrgUsers(ctx, method.OrgID.Uint64(), oidcInfo)
		if err != nil {
			tilog.Error(ctx, "SyncOktaUsers ListOrgAuthMethods error", zap.Any("err", err))
		}
	}
}

func (s *SyncUserSvc) syncOrgUsers(ctx context.Context, orgID uint64, oidcInfo *OidcInfo) error {
	tilog.Info(ctx, "SyncOktaUsers start sync org okta users", zap.Any("orgID", orgID))
	// ---
	oidcInfo.ApiToken = "00zNEuUOpajC-Xi8CPWBJG-MKtIR9rn0PC8z_7YNeB" // todo mock api token
	oidcInfo.IssuerUrl = "dev-45131958.okta.com"
	// ---
	if oidcInfo.ApiToken == "" || oidcInfo.IssuerUrl == "" {
		tilog.Info(ctx, "SyncOktaUsers api token or okta domain not set, ignore to sync users", zap.Any("orgID", orgID))
		return nil
	}
	oktaUsers, err := getOktaUsers(ctx, oidcInfo.IssuerUrl, oidcInfo.ApiToken)
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers getOktaUsers error", zap.Any("err", err))
		return nil
	}
	return s.mergeOktaUsers2Account(ctx, orgID, oktaUsers)
}

func getOktaUsers(ctx context.Context, oktaDomain, apiToken string) (map[string]string, error) {
	var oktaUsers = map[string]string{}

	var url = fmt.Sprintf(listOktaUsersUrlFormat, oktaDomain, 10)
	var batchUsers []OktaUser
	var err error
	for {
		batchUsers, url, err = batchGetOktaUsers(ctx, url, apiToken)
		if err != nil {
			tilog.Error(ctx, "SyncOktaUsers batchGetOktaUsers error", zap.Any("err", err))
			return nil, err
		}
		if len(batchUsers) > 0 {
			for _, user := range batchUsers {
				oktaUsers[user.Profile.Email] = user.Status
			}
		}
		if url == "" {
			break
		}
	}
	return oktaUsers, nil
}

func (s *SyncUserSvc) mergeOktaUsers2Account(ctx context.Context, orgID uint64, oktaUsers map[string]string) error {
	projects, err := s.hub.Project.ListProjects(ctx, s.db, []uint64{orgID}, nil, nil)
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers mergeOktaUsers2Account ListProjects error", zap.Any("orgID", orgID), zap.Any("err", err))
		return err
	}
	var projectIds []uint64
	for _, project := range projects {
		projectIds = append(projectIds, project.ID.Uint64())
	}

	var useOktaUsers = map[uint64]string{}
	for email, status := range oktaUsers {
		dbUser, err := s.hub.User.GetUserByEmail(ctx, s.db, email)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				continue
			}
			tilog.Warn(ctx, "SyncOktaUsers mergeOktaUsers2Account GetUserByEmail error", zap.Any("orgID", orgID), zap.Any("email", email), zap.Any("err", err))
			return err
		}
		useOktaUsers[dbUser.ID.Uint64()] = status
	}

	orgRoleBindings, err := s.hub.Role.ListRoleBindings(ctx, s.db, role.RoleBindingFilter{
		TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		ScopeIDs:   []entity.ID{entity.ID(orgID)},
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers mergeOktaUsers2Account ListRoleBindings error", zap.Any("err", err))
		return err
	}

	// for each user to check okta status
	for _, orgRoleBinding := range orgRoleBindings {
		accountUserID := orgRoleBinding.TargetID.Uint64()
		oktaStatus, ok := useOktaUsers[accountUserID]
		if !ok || oktaStatus != oktaUserActiveStatus {
			s.deleteNoActiveOktaUserRelateResource(ctx, orgRoleBinding, projectIds)
		}
	}
	return nil
}

func (s *SyncUserSvc) deleteNoActiveOktaUserRelateResource(ctx context.Context, orgRoleBinding entity.RoleBinding, projectIds []uint64) {
	_, err := s.hub.Role.DeleteRole(ctx, s.db, orgRoleBinding.TargetType, orgRoleBinding.TargetID.Uint64(), orgRoleBinding.ScopeType, orgRoleBinding.ScopeID.Uint64(), orgRoleBinding.Role)
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers mergeOktaUsers2Account delete org role error", zap.Any("orgID", orgRoleBinding.ScopeID.Uint64()), zap.Any("userID", orgRoleBinding.TargetID.Uint64()), zap.Any("err", err))
	}
	_, err = s.hub.Role.DeleteRoleByTargetAndScope(ctx, s.db, orgRoleBinding.TargetType, orgRoleBinding.TargetID.Uint64(), entity.RoleBindingScopeProject, projectIds)
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers mergeOktaUsers2Account delete user relate projects error", zap.Any("orgID", orgRoleBinding.ScopeID.Uint64()), zap.Any("userID", orgRoleBinding.TargetID.Uint64()), zap.Any("err", err))
		return
	}
	otherOrgRoleBindings, err := s.hub.Role.ListRoleBindings(ctx, s.db, role.RoleBindingFilter{
		TargetType: []entity.TargetType{orgRoleBinding.TargetType},
		TargetIDs:  []entity.ID{orgRoleBinding.TargetID},
		ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
		Status:     []string{entity.RoleBindingStatusActive},
	})
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers mergeOktaUsers2Account ListRoleBindings error", zap.Any("orgID", orgRoleBinding.ScopeID.Uint64()), zap.Any("userID", orgRoleBinding.TargetID.Uint64()), zap.Any("err", err))
		return
	}
	if len(otherOrgRoleBindings) == 0 {
		dbUser := &entity.User{
			Status: entity.UserStatusInactive,
		}
		dbUser.ID = orgRoleBinding.TargetID
		err = dbUser.Update(s.db,
			entity.UserDBSchema.Status,
		)
		if err != nil {
			tilog.Error(ctx, "SyncOktaUsers mergeOktaUsers2Account invalid user error", zap.Any("orgID", orgRoleBinding.ScopeID.Uint64()), zap.Any("userID", orgRoleBinding.TargetID.Uint64()), zap.Any("err", err))
		}
	}
}

func batchGetOktaUsers(ctx context.Context, url, apiToken string) ([]OktaUser, string, error) {
	tilog.Info(ctx, "SyncOktaUsers list users request", zap.Any("url", url))

	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf(tokenHeaderFormat, apiToken))

	client := http.Client{}
	rsp, err := client.Do(req)
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers list okta users error", zap.Any("err", err))
		return nil, "", err
	}
	defer func() {
		err := rsp.Body.Close()
		if err != nil {
			tilog.Error(ctx, "SyncOktaUsers close body error", zap.Any("err", err))
		}
	}()

	httpCode := rsp.StatusCode
	body, err := io.ReadAll(rsp.Body)
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers read body error", zap.Any("err", err))
	}
	listUsersRet := string(body)
	tilog.Info(ctx, "SyncOktaUsers list users response", zap.Any("httpCode", httpCode), zap.Any("rspBody", listUsersRet))

	var oktaUsers []OktaUser
	err = json.Unmarshal(body, &oktaUsers)
	if err != nil {
		tilog.Error(ctx, "SyncOktaUsers Unmarshal body error", zap.Any("err", err))
		return nil, "", err
	}

	nextUrl := getNextRequestUrlFromHeaders(rsp.Header)

	return oktaUsers, nextUrl, nil
}

func getNextRequestUrlFromHeaders(header http.Header) string {
	links := header["Link"]
	if len(links) == 0 {
		return ""
	}
	for _, link := range links {
		matched := linkCompile.MatchString(link)
		if matched {
			submatch := linkCompile.FindStringSubmatch(link)
			return submatch[1]
		}

	}
	return ""
}

type OktaUser struct {
	ID              string          `json:"id"`
	Status          string          `json:"status"`
	Created         string          `json:"created"`
	Activated       string          `json:"activated"`
	StatusChanged   string          `json:"statusChanged"`
	LastLogin       string          `json:"lastLogin"`
	LastUpdated     string          `json:"lastUpdated"`
	PasswordChanged string          `json:"passwordChanged"`
	Profile         OktaUserProfile `json:"profile"`
}

type OktaUserProfile struct {
	FirstName string `json:"firstName"`
	LastName  string `json:"lastName"`
	Login     string `json:"login"`
	Email     string `json:"email"`
}
