package feishu

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

	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const DefaultBaseURL = "https://open.feishu.cn"

const feishuDirectoryPageSize = 50
const feishuBatchGetIDLimit = 50

type FeishuDirectoryHTTPClientOptions struct {
	BaseURL    string
	AppID      string
	AppSecret  string
	HTTPClient *http.Client
}

type FeishuDirectoryHTTPClient struct {
	baseURL    string
	appID      string
	appSecret  string
	httpClient *http.Client

	tenantAccessToken          string
	tenantAccessTokenExpiresAt time.Time
}

func NewFeishuDirectoryHTTPClient(options FeishuDirectoryHTTPClientOptions) (*FeishuDirectoryHTTPClient, error) {
	appID := strings.TrimSpace(options.AppID)
	if appID == "" {
		return nil, storeerr.BadRequest("feishu app id is required")
	}
	appSecret := strings.TrimSpace(options.AppSecret)
	if appSecret == "" {
		return nil, storeerr.BadRequest("feishu app secret is required")
	}
	baseURL := strings.TrimSpace(options.BaseURL)
	if baseURL == "" {
		baseURL = DefaultBaseURL
	}
	parsedBaseURL, err := url.Parse(baseURL)
	if err != nil {
		return nil, storeerr.BadRequest("feishu base url is invalid")
	}
	switch strings.ToLower(parsedBaseURL.Scheme) {
	case "http", "https":
	default:
		return nil, storeerr.BadRequest("feishu base url must use http or https")
	}
	httpClient := options.HTTPClient
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 10 * time.Second}
	}
	return &FeishuDirectoryHTTPClient{
		baseURL:    strings.TrimRight(baseURL, "/"),
		appID:      appID,
		appSecret:  appSecret,
		httpClient: httpClient,
	}, nil
}

func (client *FeishuDirectoryHTTPClient) EnsureTenantAccessToken(ctx context.Context) (string, error) {
	return client.ensureTenantAccessToken(ctx)
}

func (client *FeishuDirectoryHTTPClient) GetDepartment(ctx context.Context, openDepartmentID string) (FeishuDepartment, error) {
	departmentID := strings.TrimSpace(openDepartmentID)
	if departmentID == "" {
		return FeishuDepartment{}, storeerr.BadRequest("feishu department id is required")
	}

	query := url.Values{}
	query.Set("department_id_type", "open_department_id")
	query.Set("user_id_type", "user_id")

	payload, err := feishuAPIRequest[feishuGetDepartmentData](ctx, client, http.MethodGet, "/open-apis/contact/v3/departments/"+url.PathEscape(departmentID), query, nil)
	if err != nil {
		return FeishuDepartment{}, err
	}
	return payload.Department.toOrganizationDepartment(), nil
}

func (client *FeishuDirectoryHTTPClient) ListDepartmentChildren(ctx context.Context, openDepartmentID string) ([]FeishuDepartment, error) {
	departmentID := strings.TrimSpace(openDepartmentID)
	if departmentID == "" {
		return nil, storeerr.BadRequest("feishu department id is required")
	}

	children := make([]FeishuDepartment, 0)
	pageToken := ""
	for {
		query := url.Values{}
		query.Set("department_id_type", "open_department_id")
		query.Set("user_id_type", "user_id")
		query.Set("page_size", fmt.Sprintf("%d", feishuDirectoryPageSize))
		if pageToken != "" {
			query.Set("page_token", pageToken)
		}

		payload, err := feishuAPIRequest[feishuListDepartmentChildrenData](ctx, client, http.MethodGet, "/open-apis/contact/v3/departments/"+url.PathEscape(departmentID)+"/children", query, nil)
		if err != nil {
			return nil, err
		}
		for _, item := range payload.Items {
			children = append(children, item.toOrganizationDepartment())
		}
		if !payload.Has_more {
			return children, nil
		}
		pageToken = strings.TrimSpace(payload.Page_token)
		if pageToken == "" {
			return nil, fmt.Errorf("feishu department children response missing page_token")
		}
	}
}

func (client *FeishuDirectoryHTTPClient) ListDepartmentUsers(ctx context.Context, openDepartmentID string) ([]FeishuUser, error) {
	departmentID := strings.TrimSpace(openDepartmentID)
	if departmentID == "" {
		return nil, storeerr.BadRequest("feishu department id is required")
	}

	users := make([]FeishuUser, 0)
	pageToken := ""
	for {
		query := url.Values{}
		query.Set("department_id", departmentID)
		query.Set("department_id_type", "open_department_id")
		query.Set("user_id_type", "user_id")
		query.Set("page_size", fmt.Sprintf("%d", feishuDirectoryPageSize))
		if pageToken != "" {
			query.Set("page_token", pageToken)
		}

		payload, err := feishuAPIRequest[feishuListDepartmentUsersData](ctx, client, http.MethodGet, "/open-apis/contact/v3/users/find_by_department", query, nil)
		if err != nil {
			return nil, err
		}
		for _, item := range payload.Items {
			users = append(users, item.toOrganizationUser())
		}
		if !payload.Has_more {
			return users, nil
		}
		pageToken = strings.TrimSpace(payload.Page_token)
		if pageToken == "" {
			return nil, fmt.Errorf("feishu department users response missing page_token")
		}
	}
}

type FeishuUserIdentity struct {
	UserID string
	Email  string
	Mobile string
}

type FeishuDepartmentScope struct {
	DepartmentID string
}

func (client *FeishuDirectoryHTTPClient) ListAuthorizedDepartmentScopes(ctx context.Context) ([]FeishuDepartmentScope, error) {
	scopes := make([]FeishuDepartmentScope, 0)
	seen := make(map[string]struct{})
	pageToken := ""
	for {
		query := url.Values{}
		query.Set("department_id_type", "open_department_id")
		query.Set("page_size", fmt.Sprintf("%d", feishuDirectoryPageSize))
		if pageToken != "" {
			query.Set("page_token", pageToken)
		}

		payload, err := feishuAPIRequest[feishuListDepartmentScopesData](ctx, client, http.MethodGet, "/open-apis/contact/v3/scopes", query, nil)
		if err != nil {
			return nil, err
		}
		for _, departmentID := range payload.Department_ids {
			trimmedDepartmentID := strings.TrimSpace(departmentID)
			if trimmedDepartmentID == "" {
				continue
			}
			if _, exists := seen[trimmedDepartmentID]; exists {
				continue
			}
			seen[trimmedDepartmentID] = struct{}{}
			scopes = append(scopes, FeishuDepartmentScope{DepartmentID: trimmedDepartmentID})
		}
		if !payload.Has_more {
			return scopes, nil
		}
		pageToken = strings.TrimSpace(payload.Page_token)
		if pageToken == "" {
			return nil, fmt.Errorf("feishu department scope response missing page_token")
		}
	}
}

func (client *FeishuDirectoryHTTPClient) BatchGetUserIDsByEmail(ctx context.Context, email string) ([]FeishuUserIdentity, error) {
	resolvedEmail := strings.TrimSpace(email)
	if resolvedEmail == "" {
		return nil, storeerr.BadRequest("feishu email is required")
	}
	return client.batchGetUserIDs(ctx, []string{resolvedEmail}, nil)
}

func (client *FeishuDirectoryHTTPClient) BatchGetUserIDsByMobile(ctx context.Context, mobile string) ([]FeishuUserIdentity, error) {
	resolvedMobile := strings.TrimSpace(mobile)
	if resolvedMobile == "" {
		return nil, storeerr.BadRequest("feishu mobile is required")
	}
	return client.batchGetUserIDs(ctx, nil, []string{resolvedMobile})
}

func (client *FeishuDirectoryHTTPClient) batchGetUserIDs(ctx context.Context, emails []string, mobiles []string) ([]FeishuUserIdentity, error) {
	resolvedEmails := normalizeFeishuLookupValues(emails)
	resolvedMobiles := normalizeFeishuLookupValues(mobiles)
	if len(resolvedEmails) == 0 && len(resolvedMobiles) == 0 {
		return nil, storeerr.BadRequest("feishu bootstrap lookup requires email or mobile")
	}
	if len(resolvedEmails) > feishuBatchGetIDLimit || len(resolvedMobiles) > feishuBatchGetIDLimit {
		return nil, storeerr.BadRequest("feishu bootstrap lookup exceeds limit")
	}

	query := url.Values{}
	query.Set("user_id_type", "user_id")

	requestBody := map[string]any{}
	if len(resolvedEmails) > 0 {
		requestBody["emails"] = resolvedEmails
	}
	if len(resolvedMobiles) > 0 {
		requestBody["mobiles"] = resolvedMobiles
	}

	payload, err := feishuAPIRequest[feishuBatchGetUserIDsData](ctx, client, http.MethodPost, "/open-apis/contact/v3/users/batch_get_id", query, requestBody)
	if err != nil {
		return nil, err
	}

	identities := make([]FeishuUserIdentity, 0, len(payload.User_list))
	for _, item := range payload.User_list {
		userID := strings.TrimSpace(item.User_id)
		if userID == "" {
			continue
		}
		identities = append(identities, FeishuUserIdentity{
			UserID: userID,
			Email:  strings.TrimSpace(item.Email),
			Mobile: strings.TrimSpace(item.Mobile),
		})
	}
	return identities, nil
}

func normalizeFeishuLookupValues(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	resolved := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		trimmedValue := strings.TrimSpace(value)
		if trimmedValue == "" {
			continue
		}
		if _, exists := seen[trimmedValue]; exists {
			continue
		}
		seen[trimmedValue] = struct{}{}
		resolved = append(resolved, trimmedValue)
	}
	return resolved
}

func feishuAPIRequest[T any](ctx context.Context, client *FeishuDirectoryHTTPClient, method string, requestPath string, query url.Values, requestBody any) (T, error) {
	var zero T
	if client == nil {
		return zero, fmt.Errorf("feishu http client is nil")
	}

	body, err := client.doRequest(ctx, method, requestPath, query, requestBody, true)
	if err != nil {
		return zero, err
	}

	var payload feishuAPIResponse[T]
	if err := model.UnmarshalJSONObject("feishu response", body, &payload); err != nil {
		return zero, fmt.Errorf("decode feishu response: %w", err)
	}
	if payload.Code != 0 {
		return zero, storeerr.BadRequest(APIErrorMessage(payload.Code, payload.Msg))
	}
	return payload.Data, nil
}

func (client *FeishuDirectoryHTTPClient) doRequest(ctx context.Context, method string, requestPath string, query url.Values, requestBody any, withTenantToken bool) ([]byte, error) {
	requestURL := client.baseURL + requestPath
	if encodedQuery := query.Encode(); encodedQuery != "" {
		requestURL += "?" + encodedQuery
	}

	var bodyReader io.Reader
	if requestBody != nil {
		encodedBody, err := model.MarshalJSONObject("feishu request", requestBody)
		if err != nil {
			return nil, fmt.Errorf("encode feishu request: %w", err)
		}
		bodyReader = bytes.NewReader(encodedBody)
	}

	request, err := http.NewRequestWithContext(ctx, method, requestURL, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("build feishu request: %w", err)
	}
	request.Header.Set("Accept", "application/json")
	if requestBody != nil {
		request.Header.Set("Content-Type", "application/json; charset=utf-8")
	}
	if withTenantToken {
		tenantAccessToken, err := client.ensureTenantAccessToken(ctx)
		if err != nil {
			return nil, err
		}
		request.Header.Set("Authorization", "Bearer "+tenantAccessToken)
	}

	response, err := client.httpClient.Do(request)
	if err != nil {
		return nil, fmt.Errorf("request feishu api: %w", err)
	}
	defer func() { _ = response.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(response.Body, 4<<20))
	if err != nil {
		return nil, fmt.Errorf("read feishu response: %w", err)
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return nil, storeerr.BadRequest(fmt.Sprintf("feishu request failed (%d): %s", response.StatusCode, feishuHTTPErrorMessage(body)))
	}
	return body, nil
}

func (client *FeishuDirectoryHTTPClient) ensureTenantAccessToken(ctx context.Context) (string, error) {
	if client.tenantAccessToken != "" && time.Until(client.tenantAccessTokenExpiresAt) > 30*time.Second {
		return client.tenantAccessToken, nil
	}

	body, err := client.doRequest(ctx, http.MethodPost, "/open-apis/auth/v3/tenant_access_token/internal", nil, map[string]string{
		"app_id":     client.appID,
		"app_secret": client.appSecret,
	}, false)
	if err != nil {
		return "", err
	}

	var payload feishuTenantAccessTokenResponse
	if err := model.UnmarshalJSONObject("feishu tenant access token response", body, &payload); err != nil {
		return "", fmt.Errorf("decode feishu tenant access token response: %w", err)
	}
	if payload.Code != 0 {
		return "", storeerr.BadRequest(APIErrorMessage(payload.Code, payload.Msg))
	}
	token := strings.TrimSpace(payload.Tenant_access_token)
	if token == "" {
		return "", fmt.Errorf("feishu tenant access token response missing token")
	}

	expireSeconds := payload.Expire
	if expireSeconds <= 0 {
		expireSeconds = 7200
	}
	client.tenantAccessToken = token
	client.tenantAccessTokenExpiresAt = time.Now().UTC().Add(time.Duration(expireSeconds) * time.Second)
	return client.tenantAccessToken, nil
}

func APIErrorMessage(code int, message string) string {
	trimmedMessage := strings.TrimSpace(message)
	switch code {
	case 40004:
		if trimmedMessage == "" {
			return "feishu api error (40004): missing department authority; grant the app contact directory access to the target department, widen data permissions to all departments for MVP, or select an authorized import root in AgentTeam Enterprise settings"
		}
		return fmt.Sprintf(
			"feishu api error (40004): %s; grant the app contact directory access to the target department, widen data permissions to all departments for MVP, or select an authorized import root in AgentTeam Enterprise settings",
			trimmedMessage,
		)
	}
	if trimmedMessage == "" {
		return fmt.Sprintf("feishu api error (%d)", code)
	}
	return fmt.Sprintf("feishu api error (%d): %s", code, trimmedMessage)
}

func feishuHTTPErrorMessage(body []byte) string {
	var payload struct {
		Code int
		Msg  string
	}

	if err := model.UnmarshalJSONObject("feishu error response", body, &payload); err == nil && (payload.Code != 0 || strings.TrimSpace(payload.Msg) != "") {
		return APIErrorMessage(payload.Code, payload.Msg)
	}
	trimmedBody := strings.TrimSpace(string(body))
	if trimmedBody == "" {
		return "empty response body"
	}
	return trimmedBody
}

type feishuTenantAccessTokenResponse struct {
	Code                int
	Msg                 string
	Tenant_access_token string
	Expire              int
}

type feishuAPIResponse[T any] struct {
	Code int
	Msg  string
	Data T
}

type feishuGetDepartmentData struct {
	Department feishuDepartmentPayload
}

type feishuListDepartmentChildrenData struct {
	Has_more   bool
	Page_token string
	Items      []feishuDepartmentPayload
}

type feishuListDepartmentUsersData struct {
	Has_more   bool
	Page_token string
	Items      []feishuUserPayload
}

type feishuListDepartmentScopesData struct {
	Has_more       bool
	Page_token     string
	Department_ids []string
}

type feishuBatchGetUserIDsData struct {
	User_list []feishuUserIDPayload
}

type feishuUserIDPayload struct {
	User_id string
	Email   string
	Mobile  string
}

type feishuDepartmentPayload struct {
	Name                 string
	Parent_department_id string
	Open_department_id   string
	Status               feishuDepartmentStatusPayload
}

type feishuDepartmentStatusPayload struct {
	Is_deleted *bool
}

func (payload feishuDepartmentPayload) toOrganizationDepartment() FeishuDepartment {
	isDeleted := payload.Status.Is_deleted != nil && *payload.Status.Is_deleted
	return FeishuDepartment{
		OpenDepartmentID:       strings.TrimSpace(payload.Open_department_id),
		ParentOpenDepartmentID: strings.TrimSpace(payload.Parent_department_id),
		Name:                   strings.TrimSpace(payload.Name),
		IsDeleted:              isDeleted,
	}
}

type feishuUserPayload struct {
	User_id        string
	Open_id        string
	Name           string
	Email          string
	Mobile         string
	Status         feishuUserStatusPayload
	Department_ids []string
	Orders         []feishuUserOrderPayload
}

type feishuUserStatusPayload struct {
	Is_frozen    *bool
	Is_resigned  *bool
	Is_activated *bool
	Is_exited    *bool
	Is_unjoin    *bool
}

type feishuUserOrderPayload struct {
	Department_id   string
	Is_primary_dept bool
}

func (payload feishuUserPayload) toOrganizationUser() FeishuUser {
	return FeishuUser{
		UserID:              strings.TrimSpace(payload.User_id),
		Name:                strings.TrimSpace(payload.Name),
		Email:               strings.TrimSpace(payload.Email),
		Mobile:              strings.TrimSpace(payload.Mobile),
		PrimaryDepartmentID: resolveFeishuPrimaryDepartmentID(payload),
		Active:              feishuUserIsActive(payload.Status),
	}
}

func resolveFeishuPrimaryDepartmentID(payload feishuUserPayload) string {
	for _, order := range payload.Orders {
		if order.Is_primary_dept {
			return strings.TrimSpace(order.Department_id)
		}
	}
	for _, departmentID := range payload.Department_ids {
		trimmedDepartmentID := strings.TrimSpace(departmentID)
		if trimmedDepartmentID != "" {
			return trimmedDepartmentID
		}
	}
	return ""
}

func feishuUserIsActive(status feishuUserStatusPayload) bool {
	if status.Is_frozen != nil && *status.Is_frozen {
		return false
	}
	if status.Is_resigned != nil && *status.Is_resigned {
		return false
	}
	if status.Is_exited != nil && *status.Is_exited {
		return false
	}
	if status.Is_unjoin != nil && *status.Is_unjoin {
		return false
	}
	if status.Is_activated != nil {
		return *status.Is_activated
	}
	return true
}
