package lark

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

type Client struct {
	AppID            string
	AppSecret        string
	BaseURL          string
	HTTPClient       *http.Client
	Token            string
	TokenExp         time.Time
	RootDepartmentID string
	DepartmentIDType string
	UserIDType       string
}

func NewClient(appID, appSecret string) *Client {
	// Auto-detect region based on App ID prefix
	// Feishu (China): cli_a...
	// Lark (International): typically starts with cli_ but uses different API endpoint
	baseURL := "https://open.feishu.cn/open-apis" // Default to Feishu China

	// You can override this with an environment variable for Lark International
	// export LARK_BASE_URL="https://open.larksuite.com/open-apis"
	// For now, using Feishu as default
	if override := strings.TrimSpace(os.Getenv("LARK_BASE_URL")); override != "" {
		baseURL = strings.TrimRight(override, "/")
	}

	rootDeptID := strings.TrimSpace(os.Getenv("LARK_DEPARTMENT_ROOT_ID"))
	if rootDeptID == "" {
		rootDeptID = "0"
	}

	deptIDType := strings.TrimSpace(os.Getenv("LARK_DEPARTMENT_ID_TYPE"))
	userIDType := strings.TrimSpace(os.Getenv("LARK_USER_ID_TYPE"))
	if deptIDType == "" {
		deptIDType = inferDepartmentIDType(rootDeptID)
	}
	if userIDType == "" {
		userIDType = "open_id"
	}

	return &Client{
		AppID:            appID,
		AppSecret:        appSecret,
		BaseURL:          baseURL,
		RootDepartmentID: rootDeptID,
		DepartmentIDType: deptIDType,
		UserIDType:       userIDType,
		HTTPClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

func inferDepartmentIDType(rootDeptID string) string {
	trimmed := strings.TrimSpace(rootDeptID)
	if trimmed == "" || trimmed == "0" || trimmed == "1" {
		return "department_id"
	}
	if strings.HasPrefix(trimmed, "od-") || strings.HasPrefix(trimmed, "od_") {
		return "open_department_id"
	}
	for i := 0; i < len(trimmed); i++ {
		if trimmed[i] < '0' || trimmed[i] > '9' {
			return "open_department_id"
		}
	}
	return "department_id"
}

func dedupeIDs(ids []string) []string {
	seen := make(map[string]struct{})
	out := make([]string, 0, len(ids))
	for _, id := range ids {
		trimmed := strings.TrimSpace(id)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	return out
}

type TenantAccessTokenResponse struct {
	Code              int    `json:"code"`
	Msg               string `json:"msg"`
	TenantAccessToken string `json:"tenant_access_token"`
	Expire            int    `json:"expire"`
}

func (c *Client) GetTenantAccessToken(ctx context.Context) error {
	if c.Token != "" && time.Now().Before(c.TokenExp) {
		return nil
	}

	url := fmt.Sprintf("%s/auth/v3/tenant_access_token/internal", c.BaseURL)
	body := map[string]string{
		"app_id":     c.AppID,
		"app_secret": c.AppSecret,
	}
	jsonBody, _ := json.Marshal(body)

	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json; charset=utf-8")

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to get token, status: %d", resp.StatusCode)
	}

	var result TenantAccessTokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return err
	}

	if result.Code != 0 {
		return fmt.Errorf("lark api error: %s", result.Msg)
	}

	c.Token = result.TenantAccessToken
	c.TokenExp = time.Now().Add(time.Duration(result.Expire) * time.Second)
	return nil
}

type DepartmentResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data struct {
		Items []struct {
			OpenDepartmentID   string `json:"open_department_id"`
			DepartmentID       string `json:"department_id"`
			Name               string `json:"name"`
			ParentDepartmentID string `json:"parent_department_id"`
			LeaderUserID       string `json:"leader_user_id"`
			Status             struct {
				IsDeleted bool `json:"is_deleted"`
			} `json:"status"`
		} `json:"items"`
		HasMore   bool   `json:"has_more"`
		PageToken string `json:"page_token"`
	} `json:"data"`
}

type ScopeResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data struct {
		DepartmentIDs       []string `json:"department_ids"`
		AuthedDepartmentIDs []string `json:"authed_department_ids"`
		Departments         []struct {
			DepartmentID     string `json:"department_id"`
			OpenDepartmentID string `json:"open_department_id"`
		} `json:"departments"`
		AuthedDepartments []struct {
			DepartmentID     string `json:"department_id"`
			OpenDepartmentID string `json:"open_department_id"`
		} `json:"authed_departments"`
	} `json:"data"`
}

func (c *Client) GetDepartments(ctx context.Context) ([]DepartmentResponse, error) {
	if err := c.GetTenantAccessToken(ctx); err != nil {
		return nil, fmt.Errorf("failed to get tenant access token: %w", err)
	}

	var allDeps []DepartmentResponse
	pageToken := ""
	pageNum := 0
	rootID := strings.TrimSpace(c.RootDepartmentID)
	if rootID == "" {
		rootID = "0"
	}
	deptIDType := strings.TrimSpace(c.DepartmentIDType)
	if deptIDType == "" {
		deptIDType = inferDepartmentIDType(rootID)
	}
	userIDType := strings.TrimSpace(c.UserIDType)
	if userIDType == "" {
		userIDType = "open_id"
	}
	scopeFallbackAttempted := false

	for {
		pageNum++
		// Fetch all departments by specifying parent_department_id and fetch_child=true.
		query := url.Values{}
		query.Set("parent_department_id", rootID)
		query.Set("fetch_child", "true")
		query.Set("page_size", "50")
		if pageToken != "" {
			query.Set("page_token", pageToken)
		}
		query.Set("department_id_type", deptIDType)
		query.Set("user_id_type", userIDType)

		endpoint := fmt.Sprintf("%s/contact/v3/departments?%s", c.BaseURL, query.Encode())
		req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
		if err != nil {
			return nil, fmt.Errorf("failed to create request: %w", err)
		}
		req.Header.Set("Authorization", "Bearer "+c.Token)

		resp, err := c.HTTPClient.Do(req)
		if err != nil {
			return nil, fmt.Errorf("failed to execute request: %w", err)
		}
		var result DepartmentResponse
		if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
			resp.Body.Close()
			return nil, fmt.Errorf("failed to decode response: %w", err)
		}
		resp.Body.Close()

		if result.Code != 0 {
			if result.Code == 40004 && !scopeFallbackAttempted {
				scopeFallbackAttempted = true
				fallbackID, fallbackType, err := c.getAuthorizedDepartmentRoot(ctx)
				if err == nil && fallbackID != "" {
					rootID = fallbackID
					deptIDType = fallbackType
					c.RootDepartmentID = fallbackID
					c.DepartmentIDType = fallbackType
					pageToken = ""
					pageNum = 0
					allDeps = nil
					continue
				}
			}
			return nil, fmt.Errorf("lark api error (code=%d): %s", result.Code, result.Msg)
		}

		for i := range result.Data.Items {
			if result.Data.Items[i].OpenDepartmentID == "" && result.Data.Items[i].DepartmentID != "" {
				result.Data.Items[i].OpenDepartmentID = result.Data.Items[i].DepartmentID
			}
		}

		allDeps = append(allDeps, result)

		if !result.Data.HasMore {
			break
		}
		pageToken = result.Data.PageToken
	}

	return allDeps, nil
}

type EmployeeResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data struct {
		Items []struct {
			OpenID  string `json:"open_id"`
			UserID  string `json:"user_id"`
			UnionID string `json:"union_id"`
			Name    string `json:"name"`
			Email   string `json:"email"`
			Avatar  struct {
				Avatar72 string `json:"avatar_72"`
			} `json:"avatar"`
			DepartmentIDs []string `json:"department_ids"`
			Status        struct {
				IsFrozen     bool `json:"is_frozen"`
				IsResigned   bool `json:"is_resigned"`
				IsTerminated bool `json:"is_terminated"`
			} `json:"status"`
			JobTitle string `json:"job_title"`
		} `json:"items"`
		HasMore   bool   `json:"has_more"`
		PageToken string `json:"page_token"`
	} `json:"data"`
}

func (c *Client) GetEmployees(ctx context.Context, departmentIDs []string) ([]EmployeeResponse, error) {
	if err := c.GetTenantAccessToken(ctx); err != nil {
		return nil, fmt.Errorf("failed to get tenant access token: %w", err)
	}

	var allEmps []EmployeeResponse
	rootID := strings.TrimSpace(c.RootDepartmentID)
	if rootID == "" {
		rootID = "0"
	}
	userIDType := strings.TrimSpace(c.UserIDType)
	if userIDType == "" {
		userIDType = "open_id"
	}

	targetDeptIDs := dedupeIDs(departmentIDs)
	if len(targetDeptIDs) == 0 {
		targetDeptIDs = []string{rootID}
	}
	scopeFallbackAttempted := false

	for deptIndex := 0; deptIndex < len(targetDeptIDs); deptIndex++ {
		deptID := strings.TrimSpace(targetDeptIDs[deptIndex])
		if deptID == "" {
			continue
		}
		deptIDType := inferDepartmentIDType(deptID)
		pageToken := ""
		pageNum := 0

		for {
			pageNum++
			// Use department_id_type and user_id_type to ensure consistent ID format in response
			query := url.Values{}
			query.Set("department_id", deptID)
			query.Set("department_id_type", deptIDType)
			query.Set("user_id_type", userIDType)
			query.Set("page_size", "50")
			if pageToken != "" {
				query.Set("page_token", pageToken)
			}

			endpoint := fmt.Sprintf("%s/contact/v3/users?%s", c.BaseURL, query.Encode())
			req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
			if err != nil {
				return nil, fmt.Errorf("failed to create request: %w", err)
			}
			req.Header.Set("Authorization", "Bearer "+c.Token)

			resp, err := c.HTTPClient.Do(req)
			if err != nil {
				return nil, fmt.Errorf("failed to execute request: %w", err)
			}
			var result EmployeeResponse
			if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
				resp.Body.Close()
				return nil, fmt.Errorf("failed to decode response: %w", err)
			}
			resp.Body.Close()

			if result.Code != 0 {
				if result.Code == 40004 && !scopeFallbackAttempted {
					scopeFallbackAttempted = true
					fallbackID, fallbackType, err := c.getAuthorizedDepartmentRoot(ctx)
					if err == nil && fallbackID != "" {
						c.RootDepartmentID = fallbackID
						c.DepartmentIDType = fallbackType
						allEmps = nil
						deptIndex = -1
						targetDeptIDs = []string{fallbackID}
						break
					}
				}
				return nil, fmt.Errorf("lark api error (code=%d): %s", result.Code, result.Msg)
			}

			for i := range result.Data.Items {
				if result.Data.Items[i].OpenID == "" {
					if result.Data.Items[i].UserID != "" {
						result.Data.Items[i].OpenID = result.Data.Items[i].UserID
					} else if result.Data.Items[i].UnionID != "" {
						result.Data.Items[i].OpenID = result.Data.Items[i].UnionID
					}
				}
				if len(result.Data.Items[i].DepartmentIDs) == 0 && deptID != "" {
					result.Data.Items[i].DepartmentIDs = []string{deptID}
				}
			}

			allEmps = append(allEmps, result)

			if !result.Data.HasMore {
				break
			}
			pageToken = result.Data.PageToken
		}
	}

	return allEmps, nil
}

func (c *Client) getAuthorizedDepartmentRoot(ctx context.Context) (string, string, error) {
	endpoint := fmt.Sprintf("%s/contact/v3/scopes", c.BaseURL)
	req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
	if err != nil {
		return "", "", fmt.Errorf("failed to create scopes request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+c.Token)

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return "", "", fmt.Errorf("failed to fetch scopes: %w", err)
	}
	defer resp.Body.Close()

	var result ScopeResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", "", fmt.Errorf("failed to decode scopes response: %w", err)
	}
	if result.Code != 0 {
		return "", "", fmt.Errorf("lark api error (code=%d): %s", result.Code, result.Msg)
	}

	if len(result.Data.DepartmentIDs) > 0 {
		return result.Data.DepartmentIDs[0], inferDepartmentIDType(result.Data.DepartmentIDs[0]), nil
	}
	if len(result.Data.AuthedDepartmentIDs) > 0 {
		return result.Data.AuthedDepartmentIDs[0], inferDepartmentIDType(result.Data.AuthedDepartmentIDs[0]), nil
	}
	for _, dept := range result.Data.Departments {
		if dept.OpenDepartmentID != "" {
			return dept.OpenDepartmentID, inferDepartmentIDType(dept.OpenDepartmentID), nil
		}
		if dept.DepartmentID != "" {
			return dept.DepartmentID, inferDepartmentIDType(dept.DepartmentID), nil
		}
	}
	for _, dept := range result.Data.AuthedDepartments {
		if dept.OpenDepartmentID != "" {
			return dept.OpenDepartmentID, inferDepartmentIDType(dept.OpenDepartmentID), nil
		}
		if dept.DepartmentID != "" {
			return dept.DepartmentID, inferDepartmentIDType(dept.DepartmentID), nil
		}
	}

	return "", "", fmt.Errorf("no authorized departments returned by lark scope API")
}

type SendMessageResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

func (c *Client) SendInteractiveCard(ctx context.Context, receiveIDType string, receiveID string, card map[string]interface{}) error {
	if err := c.GetTenantAccessToken(ctx); err != nil {
		return err
	}
	if receiveIDType == "" {
		receiveIDType = "user_id"
	}
	cardBytes, err := json.Marshal(card)
	if err != nil {
		return err
	}
	payload := map[string]interface{}{
		"receive_id": receiveID,
		"msg_type":   "interactive",
		"content":    string(cardBytes),
	}
	body, _ := json.Marshal(payload)

	url := fmt.Sprintf("%s/im/v1/messages?receive_id_type=%s", c.BaseURL, receiveIDType)
	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json; charset=utf-8")
	req.Header.Set("Authorization", "Bearer "+c.Token)

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	var result SendMessageResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return err
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		if result.Msg != "" {
			return fmt.Errorf("lark send message failed: status=%d msg=%s", resp.StatusCode, result.Msg)
		}
		return fmt.Errorf("lark send message failed: status=%d", resp.StatusCode)
	}
	if result.Code != 0 {
		return fmt.Errorf("lark api error: %s", result.Msg)
	}
	return nil
}
