// Copyright 2026 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 object

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

	"github.com/casdoor/casdoor/util"
)

// DingtalkSyncerProvider implements SyncerProvider for DingTalk API-based syncers
type DingtalkSyncerProvider struct {
	Syncer *Syncer
}

// InitAdapter initializes the DingTalk syncer (no database adapter needed)
func (p *DingtalkSyncerProvider) InitAdapter() error {
	// DingTalk syncer doesn't need database adapter
	return nil
}

// GetOriginalUsers retrieves all users from DingTalk API
func (p *DingtalkSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
	return p.getDingtalkUsers()
}

// AddUser adds a new user to DingTalk (not supported for read-only API)
func (p *DingtalkSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
	// DingTalk syncer is typically read-only
	return false, fmt.Errorf("adding users to DingTalk is not supported")
}

// UpdateUser updates an existing user in DingTalk (not supported for read-only API)
func (p *DingtalkSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
	// DingTalk syncer is typically read-only
	return false, fmt.Errorf("updating users in DingTalk is not supported")
}

// TestConnection tests the DingTalk API connection
func (p *DingtalkSyncerProvider) TestConnection() error {
	_, err := p.getDingtalkAccessToken()
	return err
}

// Close closes any open connections (no-op for DingTalk API-based syncer)
func (p *DingtalkSyncerProvider) Close() error {
	// DingTalk syncer doesn't maintain persistent connections
	return nil
}

type DingtalkAccessTokenResp struct {
	Errcode     int    `json:"errcode"`
	Errmsg      string `json:"errmsg"`
	AccessToken string `json:"access_token"`
	ExpiresIn   int    `json:"expires_in"`
}

type DingtalkUser struct {
	UserId     string `json:"userid"`
	UnionId    string `json:"unionid"`
	Name       string `json:"name"`
	Department []int  `json:"dept_id_list"`
	Position   string `json:"title"`
	Mobile     string `json:"mobile"`
	Email      string `json:"email"`
	Avatar     string `json:"avatar"`
	JobNumber  string `json:"job_number"`
	Active     bool   `json:"active"`
}

type DingtalkUserListResp struct {
	Errcode   int             `json:"errcode"`
	Errmsg    string          `json:"errmsg"`
	Result    *DingtalkResult `json:"result"`
	RequestId string          `json:"request_id"`
}

type DingtalkResult struct {
	List       []*DingtalkUser `json:"list"`
	HasMore    bool            `json:"has_more"`
	NextCursor int64           `json:"next_cursor"`
}

type DingtalkDeptListResp struct {
	Errcode int    `json:"errcode"`
	Errmsg  string `json:"errmsg"`
	Result  []struct {
		DeptId int64 `json:"dept_id"`
	} `json:"result"`
	RequestId string `json:"request_id"`
}

type DingtalkDepartment struct {
	DeptId          int64  `json:"dept_id"`
	Name            string `json:"name"`
	ParentId        int64  `json:"parent_id"`
	CreateDeptGroup bool   `json:"create_dept_group"`
	AutoAddUser     bool   `json:"auto_add_user"`
}

type DingtalkDeptDetailResp struct {
	Errcode   int                 `json:"errcode"`
	Errmsg    string              `json:"errmsg"`
	Result    *DingtalkDepartment `json:"result"`
	RequestId string              `json:"request_id"`
}

// getDingtalkAccessToken gets access token from DingTalk API
func (p *DingtalkSyncerProvider) getDingtalkAccessToken() (string, error) {
	// syncer.User should be the appKey
	// syncer.Password should be the appSecret
	appKey := p.Syncer.User
	if appKey == "" {
		return "", fmt.Errorf("appKey (user field) is required for DingTalk syncer")
	}

	appSecret := p.Syncer.Password
	if appSecret == "" {
		return "", fmt.Errorf("appSecret (password field) is required for DingTalk syncer")
	}

	apiUrl := fmt.Sprintf("https://oapi.dingtalk.com/gettoken?appkey=%s&appsecret=%s",
		url.QueryEscape(appKey), url.QueryEscape(appSecret))

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, "GET", apiUrl, nil)
	if err != nil {
		return "", err
	}

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

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

	var tokenResp DingtalkAccessTokenResp
	err = json.Unmarshal(data, &tokenResp)
	if err != nil {
		return "", err
	}

	if tokenResp.Errcode != 0 {
		return "", fmt.Errorf("failed to get access token: errcode=%d, errmsg=%s",
			tokenResp.Errcode, tokenResp.Errmsg)
	}

	return tokenResp.AccessToken, nil
}

// getDingtalkDepartments gets all department IDs from DingTalk API recursively
func (p *DingtalkSyncerProvider) getDingtalkDepartments(accessToken string) ([]int64, error) {
	return p.getDingtalkDepartmentsRecursive(accessToken, 1)
}

// getDingtalkDepartmentsRecursive recursively fetches all departments starting from parentDeptId
func (p *DingtalkSyncerProvider) getDingtalkDepartmentsRecursive(accessToken string, parentDeptId int64) ([]int64, error) {
	apiUrl := fmt.Sprintf("https://oapi.dingtalk.com/topapi/v2/department/listsub?access_token=%s",
		url.QueryEscape(accessToken))

	postData := map[string]interface{}{
		"dept_id": parentDeptId,
	}

	data, err := p.postJSON(apiUrl, postData)
	if err != nil {
		return nil, err
	}

	var deptResp DingtalkDeptListResp
	err = json.Unmarshal(data, &deptResp)
	if err != nil {
		return nil, err
	}

	if deptResp.Errcode != 0 {
		return nil, fmt.Errorf("failed to get departments: errcode=%d, errmsg=%s",
			deptResp.Errcode, deptResp.Errmsg)
	}

	// Start with the parent department itself
	deptIds := []int64{parentDeptId}

	// Recursively fetch all child departments
	for _, dept := range deptResp.Result {
		childDeptIds, err := p.getDingtalkDepartmentsRecursive(accessToken, dept.DeptId)
		if err != nil {
			return nil, err
		}
		deptIds = append(deptIds, childDeptIds...)
	}

	return deptIds, nil
}

// getDingtalkDepartmentDetails gets detailed department information
func (p *DingtalkSyncerProvider) getDingtalkDepartmentDetails(accessToken string, deptId int64) (*DingtalkDepartment, error) {
	apiUrl := fmt.Sprintf("https://oapi.dingtalk.com/topapi/v2/department/get?access_token=%s",
		url.QueryEscape(accessToken))

	postData := map[string]interface{}{
		"dept_id": deptId,
	}

	data, err := p.postJSON(apiUrl, postData)
	if err != nil {
		return nil, err
	}

	var resp DingtalkDeptDetailResp
	err = json.Unmarshal(data, &resp)
	if err != nil {
		return nil, err
	}

	if resp.Errcode != 0 {
		return nil, fmt.Errorf("failed to get department details for %d: errcode=%d, errmsg=%s",
			deptId, resp.Errcode, resp.Errmsg)
	}

	return resp.Result, nil
}

// getDingtalkUsersFromDept gets users from a specific department
func (p *DingtalkSyncerProvider) getDingtalkUsersFromDept(accessToken string, deptId int64) ([]*DingtalkUser, error) {
	allUsers := []*DingtalkUser{}
	cursor := int64(0)

	for {
		apiUrl := fmt.Sprintf("https://oapi.dingtalk.com/topapi/user/listsimple?access_token=%s",
			url.QueryEscape(accessToken))

		postData := map[string]interface{}{
			"dept_id": deptId,
			"cursor":  cursor,
			"size":    100,
		}

		data, err := p.postJSON(apiUrl, postData)
		if err != nil {
			return nil, err
		}

		var userResp DingtalkUserListResp
		err = json.Unmarshal(data, &userResp)
		if err != nil {
			return nil, err
		}

		if userResp.Errcode != 0 {
			return nil, fmt.Errorf("failed to get users from dept %d: errcode=%d, errmsg=%s",
				deptId, userResp.Errcode, userResp.Errmsg)
		}

		if userResp.Result != nil {
			allUsers = append(allUsers, userResp.Result.List...)

			if !userResp.Result.HasMore {
				break
			}
			cursor = userResp.Result.NextCursor
		} else {
			break
		}
	}

	return allUsers, nil
}

// getDingtalkUserDetails gets detailed user information
func (p *DingtalkSyncerProvider) getDingtalkUserDetails(accessToken string, userId string) (*DingtalkUser, error) {
	apiUrl := fmt.Sprintf("https://oapi.dingtalk.com/topapi/v2/user/get?access_token=%s",
		url.QueryEscape(accessToken))

	postData := map[string]interface{}{
		"userid": userId,
	}

	data, err := p.postJSON(apiUrl, postData)
	if err != nil {
		return nil, err
	}

	var resp struct {
		Errcode int           `json:"errcode"`
		Errmsg  string        `json:"errmsg"`
		Result  *DingtalkUser `json:"result"`
	}

	err = json.Unmarshal(data, &resp)
	if err != nil {
		return nil, err
	}

	if resp.Errcode != 0 {
		return nil, fmt.Errorf("failed to get user details for %s: errcode=%d, errmsg=%s",
			userId, resp.Errcode, resp.Errmsg)
	}

	return resp.Result, nil
}

// postJSON sends a POST request with JSON body
func (p *DingtalkSyncerProvider) postJSON(url string, data map[string]interface{}) ([]byte, error) {
	jsonData, err := json.Marshal(data)
	if err != nil {
		return nil, err
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	return respData, nil
}

// getDingtalkUsers gets all users from DingTalk API
func (p *DingtalkSyncerProvider) getDingtalkUsers() ([]*OriginalUser, error) {
	// Get access token
	accessToken, err := p.getDingtalkAccessToken()
	if err != nil {
		return nil, err
	}

	// Get all departments
	deptIds, err := p.getDingtalkDepartments(accessToken)
	if err != nil {
		return nil, err
	}

	// Get users from all departments (deduplicate by userid)
	userMap := make(map[string]*DingtalkUser)
	for _, deptId := range deptIds {
		users, err := p.getDingtalkUsersFromDept(accessToken, deptId)
		if err != nil {
			return nil, err
		}

		for _, user := range users {
			// Deduplicate users by userid
			if _, exists := userMap[user.UserId]; !exists {
				// Get detailed user information
				detailedUser, err := p.getDingtalkUserDetails(accessToken, user.UserId)
				if err != nil {
					// Use basic user info if details fail
					userMap[user.UserId] = user
				} else {
					userMap[user.UserId] = detailedUser
				}
			}
		}
	}

	// Convert DingTalk users to Casdoor OriginalUser
	originalUsers := []*OriginalUser{}
	for _, dingtalkUser := range userMap {
		originalUser := p.dingtalkUserToOriginalUser(dingtalkUser)
		originalUsers = append(originalUsers, originalUser)
	}

	return originalUsers, nil
}

// getDingtalkUserFieldValue extracts a field value from DingtalkUser by field name
func (p *DingtalkSyncerProvider) getDingtalkUserFieldValue(dingtalkUser *DingtalkUser, fieldName string) string {
	switch fieldName {
	case "userid":
		return dingtalkUser.UserId
	case "unionid":
		return dingtalkUser.UnionId
	case "name":
		return dingtalkUser.Name
	case "email":
		return dingtalkUser.Email
	case "mobile":
		return dingtalkUser.Mobile
	case "avatar":
		return dingtalkUser.Avatar
	case "title":
		return dingtalkUser.Position
	case "job_number":
		return dingtalkUser.JobNumber
	case "active":
		// Invert the boolean because active=true means NOT forbidden
		return util.BoolToString(!dingtalkUser.Active)
	default:
		return ""
	}
}

// dingtalkUserToOriginalUser converts DingTalk user to Casdoor OriginalUser
func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *DingtalkUser) *OriginalUser {
	user := &OriginalUser{
		Address:    []string{},
		Properties: map[string]string{},
		Groups:     []string{},
		DingTalk:   dingtalkUser.UserId, // Link DingTalk provider account
	}

	// Apply TableColumns mapping if configured
	if len(p.Syncer.TableColumns) > 0 {
		for _, tableColumn := range p.Syncer.TableColumns {
			value := p.getDingtalkUserFieldValue(dingtalkUser, tableColumn.Name)
			p.Syncer.setUserByKeyValue(user, tableColumn.CasdoorName, value)
		}
	} else {
		// Fallback to default mapping for backward compatibility
		user.Id = dingtalkUser.UserId
		user.Name = dingtalkUser.UserId
		if dingtalkUser.UnionId != "" {
			user.Name = dingtalkUser.UnionId
		}
		user.DisplayName = dingtalkUser.Name
		user.Email = dingtalkUser.Email
		user.Phone = dingtalkUser.Mobile
		user.Avatar = dingtalkUser.Avatar
		user.Title = dingtalkUser.Position
		user.IsForbidden = !dingtalkUser.Active
	}

	// Add department IDs to Groups field
	for _, deptId := range dingtalkUser.Department {
		user.Groups = append(user.Groups, fmt.Sprintf("%d", deptId))
	}

	// Set CreatedTime to current time if not set
	if user.CreatedTime == "" {
		user.CreatedTime = util.GetCurrentTime()
	}

	return user
}

// GetOriginalGroups retrieves all groups (departments) from DingTalk
func (p *DingtalkSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
	// Get access token
	accessToken, err := p.getDingtalkAccessToken()
	if err != nil {
		return nil, err
	}

	// Get all department IDs
	deptIds, err := p.getDingtalkDepartments(accessToken)
	if err != nil {
		return nil, err
	}

	// Get detailed information for each department
	originalGroups := []*OriginalGroup{}
	for _, deptId := range deptIds {
		dept, err := p.getDingtalkDepartmentDetails(accessToken, deptId)
		if err != nil {
			// Log error but continue with other departments
			fmt.Printf("Warning: failed to get details for department %d: %v\n", deptId, err)
			continue
		}

		originalGroup := p.dingtalkDepartmentToOriginalGroup(dept)
		originalGroups = append(originalGroups, originalGroup)
	}

	return originalGroups, nil
}

// dingtalkDepartmentToOriginalGroup converts DingTalk department to Casdoor OriginalGroup
func (p *DingtalkSyncerProvider) dingtalkDepartmentToOriginalGroup(dept *DingtalkDepartment) *OriginalGroup {
	// Convert department ID to string for group ID
	deptIdStr := fmt.Sprintf("%d", dept.DeptId)

	return &OriginalGroup{
		Id:          deptIdStr,
		Name:        deptIdStr,    // Use ID as name for uniqueness
		DisplayName: dept.Name,    // Use actual name as display name
		Description: "",           // DingTalk doesn't provide description
		Type:        "department", // Mark as department type
		Manager:     "",           // DingTalk doesn't provide manager in dept details
		Email:       "",           // DingTalk doesn't provide email for departments
	}
}

// GetOriginalUserGroups retrieves the group (department) IDs that a user belongs to
func (p *DingtalkSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
	// Get access token
	accessToken, err := p.getDingtalkAccessToken()
	if err != nil {
		return nil, err
	}

	// Get detailed user information which includes department list
	user, err := p.getDingtalkUserDetails(accessToken, userId)
	if err != nil {
		return nil, err
	}

	// Convert department IDs to strings
	groupIds := []string{}
	for _, deptId := range user.Department {
		groupIds = append(groupIds, fmt.Sprintf("%d", deptId))
	}

	return groupIds, nil
}
