// Package auth loads GitHub credentials and refreshes Copilot access tokens.
package auth

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/apr/provider/copilot/state"
	"github.com/tidbcloud/AgentTeam/internal/apr/types"
	"github.com/tidbcloud/AgentTeam/internal/observability/logging"
)

const (
	// GitHubAPIBase is the base URL for GitHub API endpoints
	GitHubAPIBase = "https://api.github.com"
)

// newHTTPClient creates an HTTP client using the provided proxy transporter.
func newHTTPClient(transporter types.HTTPProxy) *http.Client {
	client := &http.Client{
		Timeout: 15 * time.Second,
	}
	if transporter == nil {
		return client
	}
	transport := transporter.Transport()
	if transport == nil || transport.Proxy == nil {
		return client
	}
	client.Transport = transport
	return client
}

func httpClientFor(transporter types.HTTPProxy, httpClient ...*http.Client) *http.Client {
	if len(httpClient) > 0 && httpClient[0] != nil {
		return httpClient[0]
	}
	return newHTTPClient(transporter)
}

// ParseTokensFromFile parses tokens from file content (one token per line, trimmed).
// Exported for use by setup.go to pre-read tokens before manager initialization.
func ParseTokensFromFile(content string) []string {
	lines := strings.Split(content, "\n")
	tokens := make([]string, 0, len(lines))
	for _, line := range lines {
		token := strings.TrimSpace(line)
		if token != "" {
			tokens = append(tokens, token)
		}
	}
	return tokens
}

// ReadTokensFromFile reads GitHub tokens from the token file.
// Returns empty slice if file doesn't exist or cannot be read.
// Exported for use by setup.go to pre-read tokens before manager initialization.
func ReadTokensFromFile() []string {
	if tokenBytes, err := os.ReadFile(GitHubTokenPath()); err == nil && len(tokenBytes) > 0 {
		return ParseTokensFromFile(string(tokenBytes))
	}
	return []string{}
}

// gitHubUser fetches the authenticated user's information from GitHub API.
func gitHubUser(ctx context.Context, accessToken string, transporter types.HTTPProxy, httpClient ...*http.Client) (*GitHubUserInfo, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	req, err := http.NewRequestWithContext(ctx, "GET", GitHubAPIBase+"/user", nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "token "+accessToken)
	req.Header.Set("Accept", "application/vnd.github.v3+json")

	client := httpClientFor(transporter, httpClient...)
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

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

	return &user, nil
}

// AutoRefreshCopilotTokenWithContext starts Copilot token refresh work owned by ctx.
func AutoRefreshCopilotTokenWithContext(ctx context.Context, transporter types.HTTPProxy, httpClient ...*http.Client) error {
	if ctx == nil {
		ctx = context.Background()
	}
	if err := ctx.Err(); err != nil {
		return err
	}
	tokenStates := state.Manager.AllTokens()
	if len(tokenStates) == 0 {
		return nil
	}

	// Start a goroutine for each token to handle initialization and refresh in parallel
	for _, tokenState := range tokenStates {
		logging.Go(nil, func() { initializeAndRefreshToken(ctx, tokenState, transporter, httpClient...) })
	}

	return nil
}

// initializeAndRefreshToken initializes a single token and starts its refresh agentteam.
// This runs in a separate goroutine to avoid blocking the main initialization.
func initializeAndRefreshToken(ctx context.Context, tokenState *state.TokenState, transporter types.HTTPProxy, httpClient ...*http.Client) {
	if ctx == nil {
		ctx = context.Background()
	}
	if ctx.Err() != nil {
		return
	}
	// Populate GitHub username if not already set
	if tokenState.GitHubUser() == "" {
		gitHubToken := tokenState.GitHubToken()
		user, err := gitHubUser(ctx, gitHubToken, transporter, httpClient...)
		if err != nil {
			slog.Warn("Failed to fetch GitHub username for token", "error", err)
		} else {
			tokenState.SetGitHubUser(user.Login)
			slog.Info("Set GitHub username for token", "username", user.Login)
		}
	}

	// Setup the token (fetch Copilot token and usage info)
	if err := setupTokenForState(ctx, tokenState, transporter, httpClient...); err != nil {
		slog.Warn("Failed to setup Copilot token for GitHub token", "error", err)
		// Still start the refresh agentteam - it will retry
	}
	if ctx.Err() != nil {
		return
	}

	// Start the refresh agentteam
	refreshTokenLoop(ctx, tokenState, transporter, httpClient...)
}

// setupTokenForState sets up Copilot token for a single token state.
func setupTokenForState(ctx context.Context, tokenState *state.TokenState, transporter types.HTTPProxy, httpClient ...*http.Client) error {
	tokenResp, err := getCopilotToken(ctx, tokenState.GitHubToken(), transporter, httpClient...)
	if err != nil {
		return fmt.Errorf("failed to get Copilot token: %v", err)
	}

	tokenState.SetCopilotToken(tokenResp.Token)
	tokenState.SetLastRefreshTime(time.Now())

	slog.Debug("GitHub Copilot Token fetched successfully!")
	if state.Manager.ShowToken() {
		slog.Info("Copilot token", "token", tokenResp.Token)
	}

	// Fetch usage and quota information
	usageResp, err := getCopilotUsage(ctx, tokenState.GitHubToken(), transporter, httpClient...)
	if err != nil {
		slog.Warn("Failed to fetch Copilot usage info", "error", err)
		// Don't fail on usage fetch error, just log it
	} else {
		tokenState.SetCopilotUsage(usageResp)
		slog.Info("Copilot usage fetched",
			"plan", usageResp.CopilotPlan,
			"chat_remaining", usageResp.QuotaSnapshots.Chat.Remaining,
			"completions_remaining", usageResp.QuotaSnapshots.Completions.Remaining,
		)
	}

	return nil
}

// refreshTokenLoop continuously refreshes the Copilot token for a single token state.
func refreshTokenLoop(ctx context.Context, tokenState *state.TokenState, transporter types.HTTPProxy, httpClient ...*http.Client) {
	// Calculate initial refresh interval (default to 1 hour if not set)
	refreshInterval := 1 * time.Hour

	for {
		timer := time.NewTimer(refreshInterval)
		select {
		case <-ctx.Done():
			timer.Stop()
			return
		case <-timer.C:
		}

		slog.Debug("Refreshing Copilot token")
		newTokenResp, err := getCopilotToken(ctx, tokenState.GitHubToken(), transporter, httpClient...)
		if err != nil {
			if ctx.Err() != nil {
				return
			}
			slog.Error("Failed to refresh Copilot token", "error", err)
			// Retry with same interval
			continue
		}

		tokenState.SetCopilotToken(newTokenResp.Token)
		tokenState.SetLastRefreshTime(time.Now())
		slog.Debug("Copilot token refreshed")
		if state.Manager.ShowToken() {
			slog.Info("Refreshed Copilot token", "token", newTokenResp.Token)
		}

		// Refresh usage information as well
		newUsageResp, err := getCopilotUsage(ctx, tokenState.GitHubToken(), transporter, httpClient...)
		if err != nil {
			if ctx.Err() != nil {
				return
			}
			slog.Warn("Failed to refresh Copilot usage info", "error", err)
		} else {
			tokenState.SetCopilotUsage(newUsageResp)
			slog.Info("Copilot usage refreshed",
				"plan", newUsageResp.CopilotPlan,
				"chat_remaining", newUsageResp.QuotaSnapshots.Chat.Remaining,
				"completions_remaining", newUsageResp.QuotaSnapshots.Completions.Remaining,
			)
		}

		// Update refresh interval from token response
		refreshInterval = time.Duration(newTokenResp.RefreshIn-60) * time.Second
		if refreshInterval < 1*time.Minute {
			refreshInterval = 1 * time.Minute
		}
	}
}

// getCopilotToken requests a new Copilot API token from GitHub's internal API.
// Uses the GitHub token and VS Code headers to authenticate as an official client.
// Returns token and refresh interval information.
func getCopilotToken(ctx context.Context, gitHubToken string, transporter types.HTTPProxy, httpClient ...*http.Client) (*CopilotTokenResponse, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/copilot_internal/v2/token", nil)
	if err != nil {
		return nil, err
	}

	// Add VS Code Copilot plugin headers to appear as official client
	req.Header.Set("Authorization", "token "+gitHubToken)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Editor-Version", "vscode/"+state.Manager.VSCodeVersion())
	req.Header.Set("Editor-Plugin-Version", "copilot-chat/"+state.Manager.CopilotChatVersion())
	req.Header.Set("User-Agent", "GitHubCopilotChat/"+state.Manager.CopilotChatVersion())
	req.Header.Set("X-Github-Api-Version", "2025-04-01")
	req.Header.Set("X-Vscode-User-Agent-Library-Version", "electron-fetch")

	client := httpClientFor(transporter, httpClient...)
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		slog.Debug("Copilot token response", "response", string(body))
		if resp.StatusCode == http.StatusForbidden {
			return nil, fmt.Errorf("failed to get Copilot token: GitHub Copilot subscription not found or not accessible. " +
				"Please ensure you have an active GitHub Copilot subscription")
		}
		return nil, fmt.Errorf("failed to get Copilot token: %s", resp.Status)
	}

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

	return &tokenResp, nil
}

// getCopilotUsage fetches Copilot usage and quota information from GitHub's internal API.
// Returns detailed quota snapshots for chat, completions, and premium interactions.
func getCopilotUsage(ctx context.Context, gitHubToken string, transporter types.HTTPProxy, httpClient ...*http.Client) (*state.CopilotUsageResponse, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/copilot_internal/user", nil)
	if err != nil {
		return nil, err
	}

	// Use the same headers as getCopilotToken to authenticate
	req.Header.Set("Authorization", "token "+gitHubToken)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Editor-Version", "vscode/"+state.Manager.VSCodeVersion())
	req.Header.Set("Editor-Plugin-Version", "copilot-chat/"+state.Manager.CopilotChatVersion())
	req.Header.Set("User-Agent", "GitHubCopilotChat/"+state.Manager.CopilotChatVersion())
	req.Header.Set("X-Github-Api-Version", "2025-04-01")
	req.Header.Set("X-Vscode-User-Agent-Library-Version", "electron-fetch")

	client := httpClientFor(transporter, httpClient...)
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		slog.Debug("Copilot usage response", "response", string(body))
		return nil, fmt.Errorf("failed to get Copilot usage: %s", resp.Status)
	}

	var usageResp state.CopilotUsageResponse
	if err := json.NewDecoder(resp.Body).Decode(&usageResp); err != nil {
		return nil, err
	}

	return &usageResp, nil
}
