package degoo

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/OpenListTeam/OpenList/v4/drivers/base"
	"github.com/OpenListTeam/OpenList/v4/internal/op"
)

// Thanks to https://github.com/bernd-wechner/Degoo for API research.

const (
	// API endpoints
	loginURL       = "https://rest-api.degoo.com/login"
	accessTokenURL = "https://rest-api.degoo.com/access-token/v2"
	apiURL         = "https://production-appsync.degoo.com/graphql"

	// API configuration
	apiKey         = "da2-vs6twz5vnjdavpqndtbzg3prra"
	folderChecksum = "CgAQAg"

	// Token management
	tokenRefreshThreshold = 5 * time.Minute

	// Rate limiting
	minRequestInterval = 1 * time.Second

	// Error messages
	errRateLimited  = "rate limited (429), please try again later"
	errUnauthorized = "unauthorized access"
)

var (
	// Global rate limiting - protects against concurrent API calls
	lastRequestTime time.Time
	requestMutex    sync.Mutex
)

// JWT payload structure for token expiration checking
type JWTPayload struct {
	UserID string `json:"userID"`
	Exp    int64  `json:"exp"`
	Iat    int64  `json:"iat"`
}

// Rate limiting helper functions

// applyRateLimit ensures minimum interval between API requests
func applyRateLimit() {
	requestMutex.Lock()
	defer requestMutex.Unlock()

	if !lastRequestTime.IsZero() {
		if elapsed := time.Since(lastRequestTime); elapsed < minRequestInterval {
			time.Sleep(minRequestInterval - elapsed)
		}
	}
	lastRequestTime = time.Now()
}

// HTTP request helper functions

// createJSONRequest creates a new HTTP request with JSON body
func createJSONRequest(ctx context.Context, method, url string, body interface{}) (*http.Request, error) {
	jsonBody, err := json.Marshal(body)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(jsonBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", base.UserAgent)
	return req, nil
}

// checkHTTPResponse checks for common HTTP error conditions
func checkHTTPResponse(resp *http.Response, operation string) error {
	if resp.StatusCode == http.StatusTooManyRequests {
		return fmt.Errorf("%s %s", operation, errRateLimited)
	}
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("%s failed: %s", operation, resp.Status)
	}
	return nil
}

// isTokenExpired checks if the JWT token is expired or will expire soon
func (d *Degoo) isTokenExpired() bool {
	if d.AccessToken == "" {
		return true
	}

	payload, err := extractJWTPayload(d.AccessToken)
	if err != nil {
		return true // Invalid token format
	}

	// Check if token expires within the threshold
	expireTime := time.Unix(payload.Exp, 0)
	return time.Now().Add(tokenRefreshThreshold).After(expireTime)
}

// extractJWTPayload extracts and parses JWT payload
func extractJWTPayload(token string) (*JWTPayload, error) {
	parts := strings.Split(token, ".")
	if len(parts) != 3 {
		return nil, fmt.Errorf("invalid JWT format")
	}

	// Decode the payload (second part)
	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return nil, fmt.Errorf("failed to decode JWT payload: %w", err)
	}

	var jwtPayload JWTPayload
	if err := json.Unmarshal(payload, &jwtPayload); err != nil {
		return nil, fmt.Errorf("failed to parse JWT payload: %w", err)
	}

	return &jwtPayload, nil
}

// refreshToken attempts to refresh the access token using the refresh token
func (d *Degoo) refreshToken(ctx context.Context) error {
	if d.RefreshToken == "" {
		return fmt.Errorf("no refresh token available")
	}

	// Create request
	tokenReq := DegooAccessTokenRequest{RefreshToken: d.RefreshToken}
	req, err := createJSONRequest(ctx, "POST", accessTokenURL, tokenReq)
	if err != nil {
		return fmt.Errorf("failed to create refresh token request: %w", err)
	}

	// Execute request
	resp, err := d.client.Do(req)
	if err != nil {
		return fmt.Errorf("refresh token request failed: %w", err)
	}
	defer resp.Body.Close()

	// Check response
	if err := checkHTTPResponse(resp, "refresh token"); err != nil {
		return err
	}

	var accessTokenResp DegooAccessTokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&accessTokenResp); err != nil {
		return fmt.Errorf("failed to parse access token response: %w", err)
	}

	if accessTokenResp.AccessToken == "" {
		return fmt.Errorf("empty access token received")
	}

	d.AccessToken = accessTokenResp.AccessToken
	// Save the updated token to storage
	op.MustSaveDriverStorage(d)

	return nil
}

// ensureValidToken ensures we have a valid, non-expired token
func (d *Degoo) ensureValidToken(ctx context.Context) error {
	// Check if token is expired or will expire soon
	if d.isTokenExpired() {
		// Try to refresh token first if we have a refresh token
		if d.RefreshToken != "" {
			if refreshErr := d.refreshToken(ctx); refreshErr == nil {
				return nil // Successfully refreshed
			} else {
				// If refresh failed, fall back to full login
				fmt.Printf("Token refresh failed, falling back to full login: %v\n", refreshErr)
			}
		}

		// Perform full login
		if d.Username != "" && d.Password != "" {
			return d.login(ctx)
		}
	}

	return nil
}

// login performs the login process and retrieves the access token.
func (d *Degoo) login(ctx context.Context) error {
	if d.Username == "" || d.Password == "" {
		return fmt.Errorf("username or password not provided")
	}

	creds := DegooLoginRequest{
		GenerateToken: true,
		Username:      d.Username,
		Password:      d.Password,
	}

	jsonCreds, err := json.Marshal(creds)
	if err != nil {
		return fmt.Errorf("failed to serialize login credentials: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", loginURL, bytes.NewBuffer(jsonCreds))
	if err != nil {
		return fmt.Errorf("failed to create login request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", base.UserAgent)
	req.Header.Set("Origin", "https://app.degoo.com")

	resp, err := d.client.Do(req)
	if err != nil {
		return fmt.Errorf("login request failed: %w", err)
	}
	defer resp.Body.Close()

	// Handle rate limiting (429 Too Many Requests)
	if resp.StatusCode == http.StatusTooManyRequests {
		return fmt.Errorf("login rate limited (429), please try again later")
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("login failed: %s", resp.Status)
	}

	var loginResp DegooLoginResponse
	if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
		return fmt.Errorf("failed to parse login response: %w", err)
	}

	if loginResp.RefreshToken != "" {
		tokenReq := DegooAccessTokenRequest{RefreshToken: loginResp.RefreshToken}
		jsonTokenReq, err := json.Marshal(tokenReq)
		if err != nil {
			return fmt.Errorf("failed to serialize access token request: %w", err)
		}

		tokenReqHTTP, err := http.NewRequestWithContext(ctx, "POST", accessTokenURL, bytes.NewBuffer(jsonTokenReq))
		if err != nil {
			return fmt.Errorf("failed to create access token request: %w", err)
		}

		tokenReqHTTP.Header.Set("User-Agent", base.UserAgent)

		tokenResp, err := d.client.Do(tokenReqHTTP)
		if err != nil {
			return fmt.Errorf("failed to get access token: %w", err)
		}
		defer tokenResp.Body.Close()

		var accessTokenResp DegooAccessTokenResponse
		if err := json.NewDecoder(tokenResp.Body).Decode(&accessTokenResp); err != nil {
			return fmt.Errorf("failed to parse access token response: %w", err)
		}
		d.AccessToken = accessTokenResp.AccessToken
		d.RefreshToken = loginResp.RefreshToken // Save refresh token
	} else if loginResp.Token != "" {
		d.AccessToken = loginResp.Token
		d.RefreshToken = "" // Direct token, no refresh token available
	} else {
		return fmt.Errorf("login failed, no valid token returned")
	}

	// Save the updated tokens to storage
	op.MustSaveDriverStorage(d)

	return nil
}

// apiCall performs a Degoo GraphQL API request.
func (d *Degoo) apiCall(ctx context.Context, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {
	// Apply rate limiting
	applyRateLimit()

	// Ensure we have a valid token before making the API call
	if err := d.ensureValidToken(ctx); err != nil {
		return nil, fmt.Errorf("failed to ensure valid token: %w", err)
	}

	// Update the Token in variables if it exists (after potential refresh)
	d.updateTokenInVariables(variables)

	return d.executeGraphQLRequest(ctx, operationName, query, variables)
}

// updateTokenInVariables updates the Token field in GraphQL variables
func (d *Degoo) updateTokenInVariables(variables map[string]interface{}) {
	if variables != nil {
		if _, hasToken := variables["Token"]; hasToken {
			variables["Token"] = d.AccessToken
		}
	}
}

// executeGraphQLRequest executes a GraphQL request with retry logic
func (d *Degoo) executeGraphQLRequest(ctx context.Context, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {
	reqBody := map[string]interface{}{
		"operationName": operationName,
		"query":         query,
		"variables":     variables,
	}

	// Create and configure request
	req, err := createJSONRequest(ctx, "POST", apiURL, reqBody)
	if err != nil {
		return nil, err
	}

	// Set Degoo-specific headers
	req.Header.Set("x-api-key", apiKey)
	if d.AccessToken != "" {
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.AccessToken))
	}

	// Execute request
	resp, err := d.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("GraphQL API request failed: %w", err)
	}
	defer resp.Body.Close()

	// Check for HTTP errors
	if err := checkHTTPResponse(resp, "GraphQL API"); err != nil {
		return nil, err
	}

	// Parse GraphQL response
	var degooResp DegooGraphqlResponse
	if err := json.NewDecoder(resp.Body).Decode(&degooResp); err != nil {
		return nil, fmt.Errorf("failed to decode GraphQL response: %w", err)
	}

	// Handle GraphQL errors
	if len(degooResp.Errors) > 0 {
		return d.handleGraphQLError(ctx, degooResp.Errors[0], operationName, query, variables)
	}

	return degooResp.Data, nil
}

// handleGraphQLError handles GraphQL-level errors with retry logic
func (d *Degoo) handleGraphQLError(ctx context.Context, gqlError DegooErrors, operationName, query string, variables map[string]interface{}) (json.RawMessage, error) {
	if gqlError.ErrorType == "Unauthorized" {
		// Re-login and retry
		if err := d.login(ctx); err != nil {
			return nil, fmt.Errorf("%s, login failed: %w", errUnauthorized, err)
		}

		// Update token in variables and retry
		d.updateTokenInVariables(variables)
		return d.apiCall(ctx, operationName, query, variables)
	}

	return nil, fmt.Errorf("GraphQL API error: %s", gqlError.Message)
}

// humanReadableTimes converts Degoo timestamps to Go time.Time.
func humanReadableTimes(creation, modification, upload string) (cTime, mTime, uTime time.Time) {
	cTime, _ = time.Parse(time.RFC3339, creation)
	if modification != "" {
		modMillis, _ := strconv.ParseInt(modification, 10, 64)
		mTime = time.Unix(0, modMillis*int64(time.Millisecond))
	}
	if upload != "" {
		upMillis, _ := strconv.ParseInt(upload, 10, 64)
		uTime = time.Unix(0, upMillis*int64(time.Millisecond))
	}
	return cTime, mTime, uTime
}

// getDevices fetches and caches top-level devices and folders.
func (d *Degoo) getDevices(ctx context.Context) error {
	const query = `query GetFileChildren5($Token: String! $ParentID: String $AllParentIDs: [String] $Limit: Int! $Order: Int! $NextToken: String ) { getFileChildren5(Token: $Token ParentID: $ParentID AllParentIDs: $AllParentIDs Limit: $Limit Order: $Order NextToken: $NextToken) { Items { ParentID } NextToken } }`
	variables := map[string]interface{}{
		"Token":    d.AccessToken,
		"ParentID": "0",
		"Limit":    10,
		"Order":    3,
	}
	data, err := d.apiCall(ctx, "GetFileChildren5", query, variables)
	if err != nil {
		return err
	}
	var resp DegooGetChildren5Data
	if err := json.Unmarshal(data, &resp); err != nil {
		return fmt.Errorf("failed to parse device list: %w", err)
	}
	if d.RootFolderID == "0" {
		if len(resp.GetFileChildren5.Items) > 0 {
			d.RootFolderID = resp.GetFileChildren5.Items[0].ParentID
		}
		op.MustSaveDriverStorage(d)
	}
	return nil
}

// getAllFileChildren5 fetches all children of a directory with pagination.
func (d *Degoo) getAllFileChildren5(ctx context.Context, parentID string) ([]DegooFileItem, error) {
	const query = `query GetFileChildren5($Token: String! $ParentID: String $AllParentIDs: [String] $Limit: Int! $Order: Int! $NextToken: String ) { getFileChildren5(Token: $Token ParentID: $ParentID AllParentIDs: $AllParentIDs Limit: $Limit Order: $Order NextToken: $NextToken) { Items { ID ParentID Name Category Size CreationTime LastModificationTime LastUploadTime FilePath IsInRecycleBin DeviceID MetadataID } NextToken } }`
	var allItems []DegooFileItem
	nextToken := ""
	for {
		variables := map[string]interface{}{
			"Token":    d.AccessToken,
			"ParentID": parentID,
			"Limit":    1000,
			"Order":    3,
		}
		if nextToken != "" {
			variables["NextToken"] = nextToken
		}
		data, err := d.apiCall(ctx, "GetFileChildren5", query, variables)
		if err != nil {
			return nil, err
		}
		var resp DegooGetChildren5Data
		if err := json.Unmarshal(data, &resp); err != nil {
			return nil, err
		}
		allItems = append(allItems, resp.GetFileChildren5.Items...)
		if resp.GetFileChildren5.NextToken == "" {
			break
		}
		nextToken = resp.GetFileChildren5.NextToken
	}
	return allItems, nil
}

// getOverlay4 fetches metadata for a single item by ID.
func (d *Degoo) getOverlay4(ctx context.Context, id string) (DegooFileItem, error) {
	const query = `query GetOverlay4($Token: String!, $ID: IDType!) { getOverlay4(Token: $Token, ID: $ID) { ID ParentID Name Category Size CreationTime LastModificationTime LastUploadTime URL FilePath IsInRecycleBin DeviceID MetadataID } }`
	variables := map[string]interface{}{
		"Token": d.AccessToken,
		"ID": map[string]string{
			"FileID": id,
		},
	}
	data, err := d.apiCall(ctx, "GetOverlay4", query, variables)
	if err != nil {
		return DegooFileItem{}, err
	}
	var resp DegooGetOverlay4Data
	if err := json.Unmarshal(data, &resp); err != nil {
		return DegooFileItem{}, fmt.Errorf("failed to parse item metadata: %w", err)
	}
	return resp.GetOverlay4, nil
}
