// Package feishu provides a client for interacting with the Feishu (Lark) API.
// It handles authentication, token caching, and provides methods for approval
// workflow operations and notifications.
package feishu

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"AutoSSO/internal/config"
	"AutoSSO/internal/pkg/httpclient"
	"AutoSSO/internal/pkg/logger"
	"AutoSSO/internal/pkg/retry"
)

// Client is the Feishu API client that handles authentication and API requests.
// It implements thread-safe token caching with automatic refresh.
//
// Requirements: 2.1, 8.2, 8.4
type Client struct {
	config     *config.FeishuConfig
	httpClient *httpclient.Client
	logger     *logger.Logger
	token      string
	tokenExp   time.Time
	mu         sync.RWMutex
}

// tokenResponse represents the response from the Feishu token API.
type tokenResponse struct {
	Code              int    `json:"code"`
	Msg               string `json:"msg"`
	TenantAccessToken string `json:"tenant_access_token"`
	Expire            int    `json:"expire"` // Token expiration time in seconds
}

// tokenRequest represents the request body for obtaining a token.
type tokenRequest struct {
	AppID     string `json:"app_id"`
	AppSecret string `json:"app_secret"`
}

// APIResponse represents a generic Feishu API response.
type APIResponse struct {
	Code int             `json:"code"`
	Msg  string          `json:"msg"`
	Data json.RawMessage `json:"data,omitempty"`
}

// NewClient creates a new Feishu API client.
//
// Parameters:
//   - cfg: Feishu configuration containing AppID, AppSecret, and BaseURL
//   - httpClient: HTTP client for making requests
//
// Returns a new Client instance.
func NewClient(cfg *config.FeishuConfig, httpClient *httpclient.Client) *Client {
	return &Client{
		config:     cfg,
		httpClient: httpClient,
	}
}

// WithLogger sets the logger for the client.
// This enables logging of API calls and token operations.
func (c *Client) WithLogger(log *logger.Logger) *Client {
	c.logger = log.WithModule("feishu-client")
	return c
}

// GetAccessToken returns a valid access token, fetching a new one if necessary.
// The token is cached and automatically refreshed when it expires.
// This method is thread-safe.
//
// Requirements: 8.2 (token caching), 8.4 (context support)
func (c *Client) GetAccessToken(ctx context.Context) (string, error) {
	// First, try to get the cached token with a read lock
	c.mu.RLock()
	if c.token != "" && time.Now().Before(c.tokenExp) {
		token := c.token
		c.mu.RUnlock()
		return token, nil
	}
	c.mu.RUnlock()

	// Token is expired or not set, acquire write lock to refresh
	c.mu.Lock()
	defer c.mu.Unlock()

	// Double-check after acquiring write lock (another goroutine might have refreshed)
	if c.token != "" && time.Now().Before(c.tokenExp) {
		return c.token, nil
	}

	// Fetch new token
	token, expiry, err := c.fetchAccessToken(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to fetch access token: %w", err)
	}

	c.token = token
	c.tokenExp = expiry

	return c.token, nil
}

// fetchAccessToken fetches a new access token from the Feishu API.
// This is an internal method that performs the actual API call.
func (c *Client) fetchAccessToken(ctx context.Context) (string, time.Time, error) {
	url := c.config.BaseURL + "/open-apis/auth/v3/tenant_access_token/internal"

	reqBody := tokenRequest{
		AppID:     c.config.AppID,
		AppSecret: c.config.AppSecret,
	}

	var tokenResp tokenResponse

	// Use retry mechanism for token acquisition
	err := retry.Do(ctx, retry.DefaultConfig, func() error {
		resp, err := c.httpClient.PostJSON(ctx, url, reqBody, nil)
		if err != nil {
			return retry.NewRetryableError(err)
		}
		defer resp.Close()

		if !resp.IsSuccess() {
			body, _ := resp.ReadBody()
			return retry.NewRetryableError(fmt.Errorf("token request failed with status %d: %s", resp.StatusCode, string(body)))
		}

		if err := resp.ReadJSON(&tokenResp); err != nil {
			return fmt.Errorf("failed to parse token response: %w", err)
		}

		if tokenResp.Code != 0 {
			return fmt.Errorf("feishu API error: code=%d, msg=%s", tokenResp.Code, tokenResp.Msg)
		}

		return nil
	})

	if err != nil {
		return "", time.Time{}, err
	}

	// Calculate expiry time with a safety margin (refresh 5 minutes before actual expiry)
	// Feishu tokens typically expire in 2 hours (7200 seconds)
	expiryDuration := time.Duration(tokenResp.Expire) * time.Second
	safetyMargin := 5 * time.Minute
	if expiryDuration > safetyMargin {
		expiryDuration -= safetyMargin
	}
	expiry := time.Now().Add(expiryDuration)

	if c.logger != nil {
		c.logger.Info("Feishu access token refreshed",
			logger.Duration("expires_in", expiryDuration),
		)
	}

	return tokenResp.TenantAccessToken, expiry, nil
}

// doAuthenticatedRequest performs an authenticated HTTP request to the Feishu API.
// It automatically handles token acquisition and adds the Authorization header.
func (c *Client) doAuthenticatedRequest(ctx context.Context, method, url string, body interface{}) (*httpclient.Response, error) {
	token, err := c.GetAccessToken(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get access token: %w", err)
	}

	headers := map[string]string{
		"Authorization": "Bearer " + token,
	}

	var resp *httpclient.Response

	switch method {
	case "GET":
		resp, err = c.httpClient.Get(ctx, url, headers)
	case "POST":
		resp, err = c.httpClient.PostJSON(ctx, url, body, headers)
	default:
		return nil, fmt.Errorf("unsupported HTTP method: %s", method)
	}

	if err != nil {
		return nil, err
	}

	return resp, nil
}

// doAuthenticatedGet performs an authenticated GET request.
func (c *Client) doAuthenticatedGet(ctx context.Context, url string) (*httpclient.Response, error) {
	return c.doAuthenticatedRequest(ctx, "GET", url, nil)
}

// doAuthenticatedPost performs an authenticated POST request with JSON body.
func (c *Client) doAuthenticatedPost(ctx context.Context, url string, body interface{}) (*httpclient.Response, error) {
	return c.doAuthenticatedRequest(ctx, "POST", url, body)
}

// GetNewInstances retrieves approval instance IDs within the specified time range.
// This is used to poll for new approval requests.
// Supports pagination to ensure all instances are retrieved.
//
// Parameters:
//   - ctx: Context for cancellation
//   - startTime: Start of the time range
//   - endTime: End of the time range
//
// Returns:
//   - []string: A slice of all instance IDs found
//   - error: Any error that occurred
func (c *Client) GetNewInstances(ctx context.Context, startTime, endTime time.Time) ([]string, error) {
	// Convert times to milliseconds timestamp
	startMs := startTime.UnixMilli()
	endMs := endTime.UnixMilli()

	if c.logger != nil {
		c.logger.Info("Fetching approval instances from Feishu",
			logger.String("approval_code", c.config.ApprovalCode),
			logger.Time("start_time", startTime),
			logger.Time("end_time", endTime),
		)
	}

	var allInstances []string
	var pageToken string
	hasMore := true

	// Loop to fetch all pages
	for hasMore {
		url := fmt.Sprintf("%s/open-apis/approval/v4/instances?approval_code=%s&start_time=%d&end_time=%d&page_size=100",
			c.config.BaseURL, c.config.ApprovalCode, startMs, endMs)

		if pageToken != "" {
			url += fmt.Sprintf("&page_token=%s", pageToken)
		}

		err := retry.Do(ctx, retry.DefaultConfig, func() error {
			resp, err := c.doAuthenticatedGet(ctx, url)
			if err != nil {
				return retry.NewRetryableError(err)
			}
			defer resp.Close()

			if !resp.IsSuccess() {
				body, _ := resp.ReadBody()
				return retry.NewRetryableError(fmt.Errorf("get instances failed with status %d: %s", resp.StatusCode, string(body)))
			}

			var apiResp instanceListResponse
			if err := resp.ReadJSON(&apiResp); err != nil {
				return fmt.Errorf("failed to parse instances response: %w", err)
			}

			if apiResp.Code != 0 {
				return fmt.Errorf("feishu API error: code=%d, msg=%s", apiResp.Code, apiResp.Msg)
			}

			// Append instances from this page
			allInstances = append(allInstances, apiResp.Data.InstanceCodeList...)

			// Update pagination processing
			pageToken = apiResp.Data.PageToken
			hasMore = apiResp.Data.HasMore
			return nil
		})

		if err != nil {
			if c.logger != nil {
				c.logger.Error("Failed to fetch approval instances",
					logger.Err(err),
				)
			}
			return nil, err
		}
	}

	if c.logger != nil {
		c.logger.Info("Feishu API responded",
			logger.Int("instances_count", len(allInstances)),
		)
	}

	return allInstances, nil
}

// instanceListResponse represents the response from the instances list API.
type instanceListResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data struct {
		InstanceCodeList []string `json:"instance_code_list"`
		PageToken        string   `json:"page_token"`
		HasMore          bool     `json:"has_more"`
	} `json:"data"`
}

// GetInstanceDetail retrieves the detailed information of an approval instance.
//
// Parameters:
//   - ctx: Context for cancellation
//   - instanceID: The approval instance ID
//
// Returns the raw JSON response body for further parsing.
func (c *Client) GetInstanceDetail(ctx context.Context, instanceID string) ([]byte, error) {
	url := fmt.Sprintf("%s/open-apis/approval/v4/instances/%s", c.config.BaseURL, instanceID)

	var body []byte

	err := retry.Do(ctx, retry.DefaultConfig, func() error {
		resp, err := c.doAuthenticatedGet(ctx, url)
		if err != nil {
			return retry.NewRetryableError(err)
		}
		defer resp.Close()

		if !resp.IsSuccess() {
			respBody, _ := resp.ReadBody()
			return retry.NewRetryableError(fmt.Errorf("get instance detail failed with status %d: %s", resp.StatusCode, string(respBody)))
		}

		var err2 error
		body, err2 = resp.ReadBody()
		if err2 != nil {
			return fmt.Errorf("failed to read instance detail response: %w", err2)
		}

		// Check for API-level errors
		var apiResp APIResponse
		if err := json.Unmarshal(body, &apiResp); err != nil {
			return fmt.Errorf("failed to parse API response: %w", err)
		}

		if apiResp.Code != 0 {
			return fmt.Errorf("feishu API error: code=%d, msg=%s", apiResp.Code, apiResp.Msg)
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return body, nil
}

// SendUserNotification sends a notification message to a user by email.
//
// Parameters:
//   - ctx: Context for cancellation
//   - email: User's email address
//   - title: Message title
//   - message: Message content
//
// Requirements: 10.2 (notification functionality)
func (c *Client) SendUserNotification(ctx context.Context, email, title, message string) error {
	// First, get the user's open_id from their email
	openID, err := c.getUserOpenID(ctx, email)
	if err != nil {
		return fmt.Errorf("failed to get user open_id: %w", err)
	}

	// Send the message
	return c.sendMessage(ctx, openID, "open_id", title, message)
}

// SendGroupNotification sends a notification message to a group chat.
//
// Parameters:
//   - ctx: Context for cancellation
//   - chatID: Group chat ID
//   - title: Message title
//   - message: Message content
func (c *Client) SendGroupNotification(ctx context.Context, chatID, title, message string) error {
	return c.sendMessage(ctx, chatID, "chat_id", title, message)
}

// getUserOpenID retrieves a user's open_id from their email address.
func (c *Client) getUserOpenID(ctx context.Context, email string) (string, error) {
	url := c.config.BaseURL + "/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id"

	reqBody := map[string][]string{
		"emails": {email},
	}

	var openID string

	err := retry.Do(ctx, retry.DefaultConfig, func() error {
		resp, err := c.doAuthenticatedPost(ctx, url, reqBody)
		if err != nil {
			return retry.NewRetryableError(err)
		}
		defer resp.Close()

		if !resp.IsSuccess() {
			body, _ := resp.ReadBody()
			return retry.NewRetryableError(fmt.Errorf("get user open_id failed with status %d: %s", resp.StatusCode, string(body)))
		}

		var apiResp userIDResponse
		if err := resp.ReadJSON(&apiResp); err != nil {
			return fmt.Errorf("failed to parse user ID response: %w", err)
		}

		if apiResp.Code != 0 {
			return fmt.Errorf("feishu API error: code=%d, msg=%s", apiResp.Code, apiResp.Msg)
		}

		if len(apiResp.Data.UserList) == 0 || apiResp.Data.UserList[0].UserID == "" {
			return fmt.Errorf("user not found for email: %s", email)
		}

		openID = apiResp.Data.UserList[0].UserID
		return nil
	})

	if err != nil {
		return "", err
	}

	return openID, nil
}

// userIDResponse represents the response from the user ID lookup API.
type userIDResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data struct {
		UserList []struct {
			UserID string `json:"user_id"`
			Mobile string `json:"mobile,omitempty"`
			Email  string `json:"email,omitempty"`
		} `json:"user_list"`
	} `json:"data"`
}

// sendMessage sends an interactive card message to a recipient.
func (c *Client) sendMessage(ctx context.Context, receiveID, receiveIDType, title, message string) error {
	url := fmt.Sprintf("%s/open-apis/im/v1/messages?receive_id_type=%s", c.config.BaseURL, receiveIDType)

	// Build the interactive card content
	cardContent := buildInteractiveCard(title, message)

	reqBody := map[string]interface{}{
		"receive_id": receiveID,
		"msg_type":   "interactive",
		"content":    cardContent,
	}

	return retry.Do(ctx, retry.DefaultConfig, func() error {
		resp, err := c.doAuthenticatedPost(ctx, url, reqBody)
		if err != nil {
			return retry.NewRetryableError(err)
		}
		defer resp.Close()

		if !resp.IsSuccess() {
			body, _ := resp.ReadBody()
			return retry.NewRetryableError(fmt.Errorf("send message failed with status %d: %s", resp.StatusCode, string(body)))
		}

		var apiResp APIResponse
		if err := resp.ReadJSON(&apiResp); err != nil {
			return fmt.Errorf("failed to parse send message response: %w", err)
		}

		if apiResp.Code != 0 {
			return fmt.Errorf("feishu API error: code=%d, msg=%s", apiResp.Code, apiResp.Msg)
		}

		return nil
	})
}

// buildInteractiveCard builds the JSON string for an interactive card message.
func buildInteractiveCard(title, message string) string {
	card := map[string]interface{}{
		"config": map[string]interface{}{
			"wide_screen_mode": true,
		},
		"header": map[string]interface{}{
			"template": "turquoise",
			"title": map[string]interface{}{
				"content": title,
				"tag":     "plain_text",
			},
		},
		"elements": []map[string]interface{}{
			{
				"tag": "div",
				"text": map[string]interface{}{
					"content": message,
					"tag":     "lark_md",
				},
			},
		},
	}

	cardJSON, _ := json.Marshal(card)
	return string(cardJSON)
}

// InvalidateToken clears the cached token, forcing a refresh on the next request.
// This is useful when the token is known to be invalid.
func (c *Client) InvalidateToken() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.token = ""
	c.tokenExp = time.Time{}
}

// IsTokenValid returns true if there is a valid cached token.
// This is useful for testing and debugging.
func (c *Client) IsTokenValid() bool {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.token != "" && time.Now().Before(c.tokenExp)
}

// GetConfig returns the Feishu configuration.
// This is useful for accessing configuration values like ChatID.
func (c *Client) GetConfig() *config.FeishuConfig {
	return c.config
}
