// Package cmd provides command-line interface functionality for the CLI Proxy API server.
// It includes authentication flows for various AI service providers, service startup,
// and other command-line operations.
package cmd

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/router-for-me/CLIProxyAPI/v7/internal/auth/gemini"
	"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
	"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
	"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
	sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
	cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
	log "github.com/sirupsen/logrus"
	"github.com/tidwall/gjson"
)

const (
	geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com"
	geminiCLIVersion  = "v1internal"
)

type projectSelectionRequiredError struct{}

func (e *projectSelectionRequiredError) Error() string {
	return "gemini cli: project selection required"
}

// DoLogin handles Google Gemini authentication using the shared authentication manager.
// It initiates the OAuth flow for Google Gemini services, performs the legacy CLI user setup,
// and saves the authentication tokens to the configured auth directory.
//
// Parameters:
//   - cfg: The application configuration
//   - projectID: Optional Google Cloud project ID for Gemini services
//   - options: Login options including browser behavior and prompts
func DoLogin(cfg *config.Config, projectID string, options *LoginOptions) {
	if options == nil {
		options = &LoginOptions{}
	}

	ctx := context.Background()

	promptFn := options.Prompt
	if promptFn == nil {
		promptFn = defaultProjectPrompt()
	}

	trimmedProjectID := strings.TrimSpace(projectID)
	callbackPrompt := promptFn
	if trimmedProjectID == "" {
		callbackPrompt = nil
	}

	loginOpts := &sdkAuth.LoginOptions{
		NoBrowser:    options.NoBrowser,
		ProjectID:    trimmedProjectID,
		CallbackPort: options.CallbackPort,
		Metadata:     map[string]string{},
		Prompt:       callbackPrompt,
	}

	authenticator := sdkAuth.NewGeminiAuthenticator()
	record, errLogin := authenticator.Login(ctx, cfg, loginOpts)
	if errLogin != nil {
		log.Errorf("Gemini authentication failed: %v", errLogin)
		return
	}

	storage, okStorage := record.Storage.(*gemini.GeminiTokenStorage)
	if !okStorage || storage == nil {
		log.Error("Gemini authentication failed: unsupported token storage")
		return
	}

	geminiAuth := gemini.NewGeminiAuth()
	httpClient, errClient := geminiAuth.GetAuthenticatedClient(ctx, storage, cfg, &gemini.WebLoginOptions{
		NoBrowser:    options.NoBrowser,
		CallbackPort: options.CallbackPort,
		Prompt:       callbackPrompt,
	})
	if errClient != nil {
		log.Errorf("Gemini authentication failed: %v", errClient)
		return
	}

	log.Info("Authentication successful.")

	var activatedProjects []string

	useGoogleOne := false
	if trimmedProjectID == "" && promptFn != nil {
		fmt.Println("\nSelect login mode:")
		fmt.Println("  1. Code Assist  (GCP project, manual selection)")
		fmt.Println("  2. Google One   (personal account, auto-discover project)")
		choice, errPrompt := promptFn("Enter choice [1/2] (default: 1): ")
		if errPrompt == nil && strings.TrimSpace(choice) == "2" {
			useGoogleOne = true
		}
	}

	if useGoogleOne {
		log.Info("Google One mode: auto-discovering project...")
		if errSetup := performGeminiCLISetup(ctx, httpClient, storage, ""); errSetup != nil {
			log.Errorf("Google One auto-discovery failed: %v", errSetup)
			return
		}
		autoProject := strings.TrimSpace(storage.ProjectID)
		if autoProject == "" {
			log.Error("Google One auto-discovery returned empty project ID")
			return
		}
		log.Infof("Auto-discovered project: %s", autoProject)
		activatedProjects = []string{autoProject}
	} else {
		projects, errProjects := fetchGCPProjects(ctx, httpClient)
		if errProjects != nil {
			log.Errorf("Failed to get project list: %v", errProjects)
			return
		}

		selectedProjectID := promptForProjectSelection(projects, trimmedProjectID, promptFn)
		projectSelections, errSelection := resolveProjectSelections(selectedProjectID, projects)
		if errSelection != nil {
			log.Errorf("Invalid project selection: %v", errSelection)
			return
		}
		if len(projectSelections) == 0 {
			log.Error("No project selected; aborting login.")
			return
		}

		seenProjects := make(map[string]bool)
		for _, candidateID := range projectSelections {
			log.Infof("Activating project %s", candidateID)
			if errSetup := performGeminiCLISetup(ctx, httpClient, storage, candidateID); errSetup != nil {
				if _, ok := errors.AsType[*projectSelectionRequiredError](errSetup); ok {
					log.Error("Failed to start user onboarding: A project ID is required.")
					showProjectSelectionHelp(storage.Email, projects)
					return
				}
				log.Errorf("Failed to complete user setup: %v", errSetup)
				return
			}
			finalID := strings.TrimSpace(storage.ProjectID)
			if finalID == "" {
				finalID = candidateID
			}

			if seenProjects[finalID] {
				log.Infof("Project %s already activated, skipping", finalID)
				continue
			}
			seenProjects[finalID] = true
			activatedProjects = append(activatedProjects, finalID)
		}
	}

	storage.Auto = false
	storage.ProjectID = strings.Join(activatedProjects, ",")

	if !storage.Auto && !storage.Checked {
		for _, pid := range activatedProjects {
			isChecked, errCheck := checkCloudAPIIsEnabled(ctx, httpClient, pid)
			if errCheck != nil {
				log.Errorf("Failed to check if Cloud AI API is enabled for %s: %v", pid, errCheck)
				return
			}
			if !isChecked {
				log.Errorf("Failed to check if Cloud AI API is enabled for project %s. If you encounter an error message, please create an issue.", pid)
				return
			}
		}
		storage.Checked = true
	}

	updateAuthRecord(record, storage)

	store := sdkAuth.GetTokenStore()
	if setter, okSetter := store.(interface{ SetBaseDir(string) }); okSetter && cfg != nil {
		setter.SetBaseDir(cfg.AuthDir)
	}

	savedPath, errSave := store.Save(ctx, record)
	if errSave != nil {
		log.Errorf("Failed to save token to file: %v", errSave)
		return
	}

	if savedPath != "" {
		fmt.Printf("Authentication saved to %s\n", savedPath)
	}

	fmt.Println("Gemini authentication successful!")
}

func performGeminiCLISetup(ctx context.Context, httpClient *http.Client, storage *gemini.GeminiTokenStorage, requestedProject string) error {
	metadata := map[string]string{
		"ideType":    "IDE_UNSPECIFIED",
		"platform":   "PLATFORM_UNSPECIFIED",
		"pluginType": "GEMINI",
	}

	trimmedRequest := strings.TrimSpace(requestedProject)
	explicitProject := trimmedRequest != ""

	loadReqBody := map[string]any{
		"metadata": metadata,
	}
	if explicitProject {
		loadReqBody["cloudaicompanionProject"] = trimmedRequest
	}

	var loadResp map[string]any
	if errLoad := callGeminiCLI(ctx, httpClient, "loadCodeAssist", loadReqBody, &loadResp); errLoad != nil {
		return fmt.Errorf("load code assist: %w", errLoad)
	}

	tierID := "legacy-tier"
	if tiers, okTiers := loadResp["allowedTiers"].([]any); okTiers {
		for _, rawTier := range tiers {
			tier, okTier := rawTier.(map[string]any)
			if !okTier {
				continue
			}
			if isDefault, okDefault := tier["isDefault"].(bool); okDefault && isDefault {
				if id, okID := tier["id"].(string); okID && strings.TrimSpace(id) != "" {
					tierID = strings.TrimSpace(id)
					break
				}
			}
		}
	}

	projectID := trimmedRequest
	if projectID == "" {
		if id, okProject := loadResp["cloudaicompanionProject"].(string); okProject {
			projectID = strings.TrimSpace(id)
		}
		if projectID == "" {
			if projectMap, okProject := loadResp["cloudaicompanionProject"].(map[string]any); okProject {
				if id, okID := projectMap["id"].(string); okID {
					projectID = strings.TrimSpace(id)
				}
			}
		}
	}
	if projectID == "" {
		// Auto-discovery: try onboardUser without specifying a project
		// to let Google auto-provision one (matches Gemini CLI headless behavior
		// and Antigravity's FetchProjectID pattern).
		autoOnboardReq := map[string]any{
			"tierId":   tierID,
			"metadata": metadata,
		}

		autoCtx, autoCancel := context.WithTimeout(ctx, 30*time.Second)
		defer autoCancel()
		for attempt := 1; ; attempt++ {
			var onboardResp map[string]any
			if errOnboard := callGeminiCLI(autoCtx, httpClient, "onboardUser", autoOnboardReq, &onboardResp); errOnboard != nil {
				return fmt.Errorf("auto-discovery onboardUser: %w", errOnboard)
			}

			if done, okDone := onboardResp["done"].(bool); okDone && done {
				if resp, okResp := onboardResp["response"].(map[string]any); okResp {
					switch v := resp["cloudaicompanionProject"].(type) {
					case string:
						projectID = strings.TrimSpace(v)
					case map[string]any:
						if id, okID := v["id"].(string); okID {
							projectID = strings.TrimSpace(id)
						}
					}
				}
				break
			}

			log.Debugf("Auto-discovery: onboarding in progress, attempt %d...", attempt)
			select {
			case <-autoCtx.Done():
				return &projectSelectionRequiredError{}
			case <-time.After(2 * time.Second):
			}
		}

		if projectID == "" {
			return &projectSelectionRequiredError{}
		}
		log.Infof("Auto-discovered project ID via onboarding: %s", projectID)
	}

	onboardReqBody := map[string]any{
		"tierId":                  tierID,
		"metadata":                metadata,
		"cloudaicompanionProject": projectID,
	}

	// Store the requested project as a fallback in case the response omits it.
	storage.ProjectID = projectID

	for {
		var onboardResp map[string]any
		if errOnboard := callGeminiCLI(ctx, httpClient, "onboardUser", onboardReqBody, &onboardResp); errOnboard != nil {
			return fmt.Errorf("onboard user: %w", errOnboard)
		}

		if done, okDone := onboardResp["done"].(bool); okDone && done {
			responseProjectID := ""
			if resp, okResp := onboardResp["response"].(map[string]any); okResp {
				switch projectValue := resp["cloudaicompanionProject"].(type) {
				case map[string]any:
					if id, okID := projectValue["id"].(string); okID {
						responseProjectID = strings.TrimSpace(id)
					}
				case string:
					responseProjectID = strings.TrimSpace(projectValue)
				}
			}

			finalProjectID := projectID
			if responseProjectID != "" {
				if explicitProject && !strings.EqualFold(responseProjectID, projectID) {
					log.Infof("Gemini onboarding: requested project %s maps to backend project %s", projectID, responseProjectID)
					log.Infof("Using backend project ID: %s", responseProjectID)
				}
				finalProjectID = responseProjectID
			}

			storage.ProjectID = strings.TrimSpace(finalProjectID)
			if storage.ProjectID == "" {
				storage.ProjectID = strings.TrimSpace(projectID)
			}
			if storage.ProjectID == "" {
				return fmt.Errorf("onboard user completed without project id")
			}
			log.Infof("Onboarding complete. Using Project ID: %s", storage.ProjectID)
			return nil
		}

		log.Println("Onboarding in progress, waiting 5 seconds...")
		time.Sleep(5 * time.Second)
	}
}

func callGeminiCLI(ctx context.Context, httpClient *http.Client, endpoint string, body any, result any) error {
	url := fmt.Sprintf("%s/%s:%s", geminiCLIEndpoint, geminiCLIVersion, endpoint)
	if strings.HasPrefix(endpoint, "operations/") {
		url = fmt.Sprintf("%s/%s", geminiCLIEndpoint, endpoint)
	}

	var reader io.Reader
	if body != nil {
		rawBody, errMarshal := json.Marshal(body)
		if errMarshal != nil {
			return fmt.Errorf("marshal request body: %w", errMarshal)
		}
		reader = bytes.NewReader(rawBody)
	}

	req, errRequest := http.NewRequestWithContext(ctx, http.MethodPost, url, reader)
	if errRequest != nil {
		return fmt.Errorf("create request: %w", errRequest)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))

	resp, errDo := httpClient.Do(req)
	if errDo != nil {
		return fmt.Errorf("execute request: %w", errDo)
	}
	defer func() {
		if errClose := resp.Body.Close(); errClose != nil {
			log.Errorf("response body close error: %v", errClose)
		}
	}()

	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(bodyBytes)))
	}

	if result == nil {
		_, _ = io.Copy(io.Discard, resp.Body)
		return nil
	}

	if errDecode := json.NewDecoder(resp.Body).Decode(result); errDecode != nil {
		return fmt.Errorf("decode response body: %w", errDecode)
	}

	return nil
}

func fetchGCPProjects(ctx context.Context, httpClient *http.Client) ([]interfaces.GCPProjectProjects, error) {
	req, errRequest := http.NewRequestWithContext(ctx, http.MethodGet, "https://cloudresourcemanager.googleapis.com/v1/projects", nil)
	if errRequest != nil {
		return nil, fmt.Errorf("could not create project list request: %w", errRequest)
	}

	resp, errDo := httpClient.Do(req)
	if errDo != nil {
		return nil, fmt.Errorf("failed to execute project list request: %w", errDo)
	}
	defer func() {
		if errClose := resp.Body.Close(); errClose != nil {
			log.Errorf("response body close error: %v", errClose)
		}
	}()

	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("project list request failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(bodyBytes)))
	}

	var projects interfaces.GCPProject
	if errDecode := json.NewDecoder(resp.Body).Decode(&projects); errDecode != nil {
		return nil, fmt.Errorf("failed to unmarshal project list: %w", errDecode)
	}

	return projects.Projects, nil
}

// promptForProjectSelection prints available projects and returns the chosen project ID.
func promptForProjectSelection(projects []interfaces.GCPProjectProjects, presetID string, promptFn func(string) (string, error)) string {
	trimmedPreset := strings.TrimSpace(presetID)
	if len(projects) == 0 {
		if trimmedPreset != "" {
			return trimmedPreset
		}
		fmt.Println("No Google Cloud projects are available for selection.")
		return ""
	}

	fmt.Println("Available Google Cloud projects:")
	defaultIndex := 0
	for idx, project := range projects {
		fmt.Printf("[%d] %s (%s)\n", idx+1, project.ProjectID, project.Name)
		if trimmedPreset != "" && project.ProjectID == trimmedPreset {
			defaultIndex = idx
		}
	}
	fmt.Println("Type 'ALL' to onboard every listed project.")

	defaultID := projects[defaultIndex].ProjectID

	if trimmedPreset != "" {
		if strings.EqualFold(trimmedPreset, "ALL") {
			return "ALL"
		}
		for _, project := range projects {
			if project.ProjectID == trimmedPreset {
				return trimmedPreset
			}
		}
		log.Warnf("Provided project ID %s not found in available projects; please choose from the list.", trimmedPreset)
	}

	for {
		promptMsg := fmt.Sprintf("Enter project ID [%s] or ALL: ", defaultID)
		answer, errPrompt := promptFn(promptMsg)
		if errPrompt != nil {
			log.Errorf("Project selection prompt failed: %v", errPrompt)
			return defaultID
		}
		answer = strings.TrimSpace(answer)
		if strings.EqualFold(answer, "ALL") {
			return "ALL"
		}
		if answer == "" {
			return defaultID
		}

		for _, project := range projects {
			if project.ProjectID == answer {
				return project.ProjectID
			}
		}

		if idx, errAtoi := strconv.Atoi(answer); errAtoi == nil {
			if idx >= 1 && idx <= len(projects) {
				return projects[idx-1].ProjectID
			}
		}

		fmt.Println("Invalid selection, enter a project ID or a number from the list.")
	}
}

func resolveProjectSelections(selection string, projects []interfaces.GCPProjectProjects) ([]string, error) {
	trimmed := strings.TrimSpace(selection)
	if trimmed == "" {
		return nil, nil
	}
	available := make(map[string]struct{}, len(projects))
	ordered := make([]string, 0, len(projects))
	for _, project := range projects {
		id := strings.TrimSpace(project.ProjectID)
		if id == "" {
			continue
		}
		if _, exists := available[id]; exists {
			continue
		}
		available[id] = struct{}{}
		ordered = append(ordered, id)
	}
	if strings.EqualFold(trimmed, "ALL") {
		if len(ordered) == 0 {
			return nil, fmt.Errorf("no projects available for ALL selection")
		}
		return append([]string(nil), ordered...), nil
	}
	parts := strings.Split(trimmed, ",")
	selections := make([]string, 0, len(parts))
	seen := make(map[string]struct{}, len(parts))
	for _, part := range parts {
		id := strings.TrimSpace(part)
		if id == "" {
			continue
		}
		if _, dup := seen[id]; dup {
			continue
		}
		if len(available) > 0 {
			if _, ok := available[id]; !ok {
				return nil, fmt.Errorf("project %s not found in available projects", id)
			}
		}
		seen[id] = struct{}{}
		selections = append(selections, id)
	}
	return selections, nil
}

func defaultProjectPrompt() func(string) (string, error) {
	reader := bufio.NewReader(os.Stdin)
	return func(prompt string) (string, error) {
		fmt.Print(prompt)
		line, errRead := reader.ReadString('\n')
		if errRead != nil {
			if errors.Is(errRead, io.EOF) {
				return strings.TrimSpace(line), nil
			}
			return "", errRead
		}
		return strings.TrimSpace(line), nil
	}
}

func showProjectSelectionHelp(email string, projects []interfaces.GCPProjectProjects) {
	if email != "" {
		log.Infof("Your account %s needs to specify a project ID.", email)
	} else {
		log.Info("You need to specify a project ID.")
	}

	if len(projects) > 0 {
		fmt.Println("========================================================================")
		for _, p := range projects {
			fmt.Printf("Project ID: %s\n", p.ProjectID)
			fmt.Printf("Project Name: %s\n", p.Name)
			fmt.Println("------------------------------------------------------------------------")
		}
	} else {
		fmt.Println("No active projects were returned for this account.")
	}

	fmt.Printf("Please run this command to login again with a specific project:\n\n%s --login --project_id <project_id>\n", os.Args[0])
}

func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projectID string) (bool, error) {
	serviceUsageURL := "https://serviceusage.googleapis.com"
	requiredServices := []string{
		// "geminicloudassist.googleapis.com", // Gemini Cloud Assist API
		"cloudaicompanion.googleapis.com", // Gemini for Google Cloud API
	}
	for _, service := range requiredServices {
		checkUrl := fmt.Sprintf("%s/v1/projects/%s/services/%s", serviceUsageURL, projectID, service)
		req, errRequest := http.NewRequestWithContext(ctx, http.MethodGet, checkUrl, nil)
		if errRequest != nil {
			return false, fmt.Errorf("failed to create request: %w", errRequest)
		}
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))
		resp, errDo := httpClient.Do(req)
		if errDo != nil {
			return false, fmt.Errorf("failed to execute request: %w", errDo)
		}

		if resp.StatusCode == http.StatusOK {
			bodyBytes, _ := io.ReadAll(resp.Body)
			if gjson.GetBytes(bodyBytes, "state").String() == "ENABLED" {
				_ = resp.Body.Close()
				continue
			}
		}
		_ = resp.Body.Close()

		enableUrl := fmt.Sprintf("%s/v1/projects/%s/services/%s:enable", serviceUsageURL, projectID, service)
		req, errRequest = http.NewRequestWithContext(ctx, http.MethodPost, enableUrl, strings.NewReader("{}"))
		if errRequest != nil {
			return false, fmt.Errorf("failed to create request: %w", errRequest)
		}
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("User-Agent", misc.GeminiCLIUserAgent(""))
		resp, errDo = httpClient.Do(req)
		if errDo != nil {
			return false, fmt.Errorf("failed to execute request: %w", errDo)
		}

		bodyBytes, _ := io.ReadAll(resp.Body)
		errMessage := string(bodyBytes)
		errMessageResult := gjson.GetBytes(bodyBytes, "error.message")
		if errMessageResult.Exists() {
			errMessage = errMessageResult.String()
		}
		if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
			_ = resp.Body.Close()
			continue
		} else if resp.StatusCode == http.StatusBadRequest {
			_ = resp.Body.Close()
			if strings.Contains(strings.ToLower(errMessage), "already enabled") {
				continue
			}
		}
		_ = resp.Body.Close()
		return false, fmt.Errorf("project activation required: %s", errMessage)
	}
	return true, nil
}

func updateAuthRecord(record *cliproxyauth.Auth, storage *gemini.GeminiTokenStorage) {
	if record == nil || storage == nil {
		return
	}

	finalName := gemini.CredentialFileName(storage.Email, storage.ProjectID, true)

	if record.Metadata == nil {
		record.Metadata = make(map[string]any)
	}
	record.Metadata["email"] = storage.Email
	record.Metadata["project_id"] = storage.ProjectID
	record.Metadata["auto"] = storage.Auto
	record.Metadata["checked"] = storage.Checked

	record.ID = finalName
	record.FileName = finalName
	record.Storage = storage
}
