// Package copilot provides GitHub Copilot integration for the gateway system.
// This file handles the initialization and configuration of the Copilot provider.
package copilot

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"

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

type setupOptions struct {
	httpClient      *http.Client
	setupHTTPClient *http.Client
	lifecycle       context.Context
}

// SetupOption customizes Copilot provider setup.
type SetupOption func(*setupOptions)

// WithHTTPClient sets the client used for Copilot model/chat/embedding API calls.
func WithHTTPClient(httpClient *http.Client) SetupOption {
	return func(options *setupOptions) {
		options.httpClient = httpClient
	}
}

// WithSetupHTTPClient sets the client used for GitHub setup and auth calls.
func WithSetupHTTPClient(httpClient *http.Client) SetupOption {
	return func(options *setupOptions) {
		options.setupHTTPClient = httpClient
	}
}

// WithLifecycleContext sets the context that owns background provider work.
func WithLifecycleContext(ctx context.Context) SetupOption {
	return func(options *setupOptions) {
		if ctx != nil {
			options.lifecycle = ctx
		}
	}
}

// mergeAndDeduplicateTokens merges two token slices and removes duplicates.
// Returns a deduplicated slice of tokens.
func mergeAndDeduplicateTokens(t1, t2 []string) []string {
	tokenMap := make(map[string]bool)
	for _, t := range t1 {
		if t != "" {
			tokenMap[t] = true
		}
	}
	for _, t := range t2 {
		if t != "" {
			tokenMap[t] = true
		}
	}
	result := make([]string, 0, len(tokenMap))
	for t := range tokenMap {
		result = append(result, t)
	}
	return result
}

// Setup initializes the GitHub Copilot provider with the given configuration.
// This function performs several key setup tasks:
//   - Configures account settings and rate limiting
//   - Sets up GitHub and Copilot authentication tokens for all configured tokens
//   - Caches version information for VS Code and Copilot Chat extensions
//   - Creates and initializes the model cache using ModelProvider architecture
//
// The setup process is designed to be resilient, with fallback values for
// version information if GitHub API calls fail.
//
// Parameters:
//   - cfg: Configuration settings for the Copilot provider
//
// Returns:
//   - *Copilot: The configured Copilot instance
//   - error: Any error that occurred during setup, or nil on success
func Setup(cfg *CopilotConfig, tr types.HTTPProxy, opts ...SetupOption) (*Copilot, error) {
	options := setupOptions{}
	for _, opt := range opts {
		if opt != nil {
			opt(&options)
		}
	}
	lifecycle := options.lifecycle
	if lifecycle == nil {
		lifecycle = context.Background()
	}

	// Initialize global state with service configuration
	state.Manager.SetAccountType(cfg.AccountType)
	state.Manager.SetRateLimitSeconds(cfg.RateLimit)
	state.Manager.SetRateLimitWait(cfg.Wait)

	// Restore selection algorithm from config
	if cfg.SelectionAlgorithm != "" {
		state.Manager.SetSelectionAlgorithm(cfg.SelectionAlgorithm)
	}

	// Apply user-configured model tier overrides
	if len(cfg.ModelTiers) > 0 {
		state.ApplyModelTierOverrides(cfg.ModelTiers)
		slog.Info("Applied model tier overrides", "count", len(cfg.ModelTiers))
	}

	if cfg.AccountType != "individual" {
		slog.Info("Using GitHub account", "plan", cfg.AccountType)
	}

	// Cache VS Code and Copilot Chat versions for compatibility
	if err := CacheVSCodeVersion(lifecycle, tr, options.setupHTTPClient); err != nil {
		return nil, fmt.Errorf("failed to cache VS Code version: %v", err)
	}

	if err := CacheCopilotChatVersion(lifecycle, tr, options.setupHTTPClient); err != nil {
		return nil, fmt.Errorf("failed to cache Copilot Chat version: %v", err)
	}

	// Collect all configured tokens from config
	var configTokens []string

	// Add tokens from Tokens field
	if len(cfg.Tokens) > 0 {
		configTokens = append(configTokens, cfg.Tokens...)
	}

	// Read tokens from file (if exists)
	// This is done BEFORE manager.Initialize to allow merging with config tokens
	fileTokens := auth.ReadTokensFromFile()

	// Merge config tokens with file tokens, deduplicating
	allTokens := mergeAndDeduplicateTokens(configTokens, fileTokens)

	// Initialize manager with all tokens
	if len(allTokens) > 0 {
		state.Manager.Initialize(allTokens)
		slog.Info("Using GitHub tokens",
			"total", len(allTokens),
			"from_config", len(configTokens),
			"from_file", len(fileTokens),
		)

		// Restore disabled tokens state from config
		if len(cfg.DisabledTokens) > 0 {
			disabledSet := make(map[string]bool, len(cfg.DisabledTokens))
			for _, t := range cfg.DisabledTokens {
				disabledSet[t] = true
			}
			for _, ts := range state.Manager.AllTokens() {
				if disabledSet[ts.GitHubToken()] {
					ts.SetEnabled(false)
				}
			}
		}
	}

	// Setup Copilot authentication token for all GitHub tokens
	authCtx, cancelAuth := context.WithCancel(lifecycle)
	if err := auth.AutoRefreshCopilotTokenWithContext(authCtx, tr, options.setupHTTPClient); err != nil {
		cancelAuth()
		return nil, fmt.Errorf("failed to setup Copilot token: %v", err)
	}

	// Create static model provider for known Copilot models
	// Copilot uses a static list since their models are well-known and documented
	staticModels := []types.StaticModel{
		// GPT-5 series
		{ID: "gpt-4.1", MaxOutputTokens: 65536},
		{ID: "gpt-4o", MaxOutputTokens: 65536},
		{ID: "gpt-5-mini", MaxOutputTokens: 65536},
		{ID: "gpt-5.2", MaxOutputTokens: 100000},
		{ID: "gpt-5.2-codex", MaxOutputTokens: 100000},
		{ID: "gpt-5.3-codex", MaxOutputTokens: 100000},
		{ID: "gpt-5.4-mini", MaxOutputTokens: 128000},
		{ID: "gpt-5.4", MaxOutputTokens: 128000},

		// Claude series
		{ID: "claude-haiku-4.5", MaxOutputTokens: 65536},
		{ID: "claude-sonnet-4.6", MaxOutputTokens: 65536},

		// Gemini series
		{ID: "gemini-3-flash", MaxOutputTokens: 32768},
		{ID: "gemini-3.1-pro", MaxOutputTokens: 65536},
	}

	modelProvider := NewModelProvider(staticModels, options.httpClient)
	modelCache := types.NewModelCache(modelProvider)

	// Initialize model cache (static providers don't need periodic refresh)
	if err := modelCache.Initialize(); err != nil {
		cancelAuth()
		return nil, fmt.Errorf("failed to initialize model cache: %v", err)
	}

	// Return configured Copilot instance
	return &Copilot{transporter: tr, httpClient: options.httpClient, modelCache: modelCache, cancelAuth: cancelAuth}, nil
}

// CacheVSCodeVersion fetches and caches the latest VS Code version from GitHub releases.
// This version information is used to maintain compatibility with the GitHub Copilot API,
// which expects client version headers to match supported VS Code versions.
//
// The function queries the GitHub API for the latest VS Code release and extracts
// the version number from the tag name. If the API call fails, it falls back to
// a known stable version to ensure the service remains functional.
//
// Returns:
//   - error: Always returns nil as this function uses fallback values on failure
func CacheVSCodeVersion(ctx context.Context, tr types.HTTPProxy, httpClient *http.Client) error {
	version, err := getOSSLatestVersion(ctx, "microsoft/vscode", tr, httpClient)
	if err != nil {
		slog.Warn("Failed to get latest VS Code version, using fallback", "error", err)
		state.Manager.SetVSCodeVersion("1.105.1")
		return nil
	}

	state.Manager.SetVSCodeVersion(version)
	slog.Debug("Cached VS Code version", "version", version)
	return nil
}

// CacheCopilotChatVersion fetches and caches the latest Copilot Chat extension version.
// This version information is used to maintain compatibility with the GitHub Copilot API,
// which may have different behavior or requirements based on the client extension version.
//
// Similar to CacheVSCodeVersion, this function queries the GitHub API for the latest
// Copilot Chat extension release. If the API call fails, it falls back to a known
// stable version to ensure continued operation.
//
// Returns:
//   - error: Always returns nil as this function uses fallback values on failure
func CacheCopilotChatVersion(ctx context.Context, tr types.HTTPProxy, httpClient *http.Client) error {
	version, err := getOSSLatestVersion(ctx, "microsoft/vscode-copilot-chat", tr, httpClient)
	if err != nil {
		slog.Warn("Failed to get latest Copilot Chat version, using fallback", "error", err)
		state.Manager.SetCopilotChatVersion("0.32.0")
		return nil
	}

	state.Manager.SetCopilotChatVersion(version)
	slog.Debug("Cached Copilot Chat version", "version", version)
	return nil
}

// getOSSLatestVersion fetches the latest release version for a GitHub repository.
// This is a utility function used to get version information for VS Code and
// Copilot Chat extensions from their respective GitHub repositories.
//
// The function makes an authenticated request to the GitHub API to get the latest
// release information and extracts the version number from the tag name, removing
// any 'v' prefix that may be present.
//
// Parameters:
//   - repo: The GitHub repository in "owner/name" format (e.g., "microsoft/vscode")
//
// Returns:
//   - string: The version string with any 'v' prefix removed
//   - error: Any error that occurred during the API request or JSON parsing
func getOSSLatestVersion(ctx context.Context, repo string, tr types.HTTPProxy, httpClient *http.Client) (string, error) {
	// Create HTTP request to GitHub API
	req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/repos/"+repo+"/releases/latest", nil)
	if err != nil {
		return "", err
	}

	// Set required headers for GitHub API compatibility
	req.Header.Set("Accept", "application/vnd.github.v3+json")
	req.Header.Set("User-Agent", "finetun-copilot")

	// Execute the HTTP request
	client := httpClient
	if client == nil {
		client = &http.Client{}
	}
	if httpClient == nil && tr != nil {
		client.Transport = tr.Transport()
	}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	// Check for successful response
	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to get %s releases: %s", repo, resp.Status)
	}

	// Parse the JSON response to extract tag name
	var release struct {
		TagName string `json:"tag_name"`
	}

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

	// Remove 'v' prefix if present (e.g., "v1.85.1" -> "1.85.1")
	// This normalizes version strings for consistent usage
	version := release.TagName
	if len(version) > 0 && version[0] == 'v' {
		version = version[1:]
	}

	return version, nil
}
