package auth

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	gitlabauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gitlab"
	"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
	"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
	"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
	"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
	coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
	log "github.com/sirupsen/logrus"
)

const (
	gitLabLoginModeMetadataKey           = "login_mode"
	gitLabLoginModeOAuth                 = "oauth"
	gitLabLoginModePAT                   = "pat"
	gitLabBaseURLMetadataKey             = "base_url"
	gitLabOAuthClientIDMetadataKey       = "oauth_client_id"
	gitLabOAuthClientSecretMetadataKey   = "oauth_client_secret"
	gitLabPersonalAccessTokenMetadataKey = "personal_access_token"
)

var gitLabRefreshLead = 5 * time.Minute

type GitLabAuthenticator struct {
	CallbackPort int
}

func NewGitLabAuthenticator() *GitLabAuthenticator {
	return &GitLabAuthenticator{CallbackPort: gitlabauth.DefaultCallbackPort}
}

func (a *GitLabAuthenticator) Provider() string {
	return "gitlab"
}

func (a *GitLabAuthenticator) RefreshLead() *time.Duration {
	return &gitLabRefreshLead
}

func (a *GitLabAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
	if cfg == nil {
		return nil, fmt.Errorf("cliproxy auth: configuration is required")
	}
	if ctx == nil {
		ctx = context.Background()
	}
	if opts == nil {
		opts = &LoginOptions{}
	}

	switch strings.ToLower(strings.TrimSpace(opts.Metadata[gitLabLoginModeMetadataKey])) {
	case "", gitLabLoginModeOAuth:
		return a.loginOAuth(ctx, cfg, opts)
	case gitLabLoginModePAT:
		return a.loginPAT(ctx, cfg, opts)
	default:
		return nil, fmt.Errorf("gitlab auth: unsupported login mode %q", opts.Metadata[gitLabLoginModeMetadataKey])
	}
}

func (a *GitLabAuthenticator) loginOAuth(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
	client := gitlabauth.NewAuthClient(cfg)
	baseURL := a.resolveString(opts, gitLabBaseURLMetadataKey, gitlabauth.DefaultBaseURL)
	clientID, err := a.requireInput(opts, gitLabOAuthClientIDMetadataKey, "Enter GitLab OAuth application client ID: ")
	if err != nil {
		return nil, err
	}
	clientSecret, err := a.optionalInput(opts, gitLabOAuthClientSecretMetadataKey, "Enter GitLab OAuth application client secret (press Enter for public PKCE app): ")
	if err != nil {
		return nil, err
	}

	callbackPort := a.CallbackPort
	if opts.CallbackPort > 0 {
		callbackPort = opts.CallbackPort
	}
	redirectURI := gitlabauth.RedirectURL(callbackPort)

	pkceCodes, err := gitlabauth.GeneratePKCECodes()
	if err != nil {
		return nil, err
	}
	state, err := misc.GenerateRandomState()
	if err != nil {
		return nil, fmt.Errorf("gitlab state generation failed: %w", err)
	}

	oauthServer := gitlabauth.NewOAuthServer(callbackPort)
	if err := oauthServer.Start(); err != nil {
		return nil, err
	}
	defer func() {
		stopCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		if stopErr := oauthServer.Stop(stopCtx); stopErr != nil {
			log.Warnf("gitlab oauth server stop error: %v", stopErr)
		}
	}()

	authURL, err := client.GenerateAuthURL(baseURL, clientID, redirectURI, state, pkceCodes)
	if err != nil {
		return nil, err
	}

	if !opts.NoBrowser {
		fmt.Println("Opening browser for GitLab Duo authentication")
		if !browser.IsAvailable() {
			log.Warn("No browser available; please open the URL manually")
			util.PrintSSHTunnelInstructions(callbackPort)
			fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
		} else if err = browser.OpenURL(authURL); err != nil {
			log.Warnf("Failed to open browser automatically: %v", err)
			util.PrintSSHTunnelInstructions(callbackPort)
			fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
		}
	} else {
		util.PrintSSHTunnelInstructions(callbackPort)
		fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
	}

	fmt.Println("Waiting for GitLab OAuth callback...")

	callbackCh := make(chan *gitlabauth.OAuthResult, 1)
	callbackErrCh := make(chan error, 1)
	go func() {
		result, waitErr := oauthServer.WaitForCallback(5 * time.Minute)
		if waitErr != nil {
			callbackErrCh <- waitErr
			return
		}
		callbackCh <- result
	}()

	var result *gitlabauth.OAuthResult
	var manualPromptTimer *time.Timer
	var manualPromptC <-chan time.Time
	if opts.Prompt != nil {
		manualPromptTimer = time.NewTimer(15 * time.Second)
		manualPromptC = manualPromptTimer.C
		defer manualPromptTimer.Stop()
	}

waitForCallback:
	for {
		select {
		case result = <-callbackCh:
			break waitForCallback
		case err = <-callbackErrCh:
			return nil, err
		case <-manualPromptC:
			manualPromptC = nil
			if manualPromptTimer != nil {
				manualPromptTimer.Stop()
			}
			input, promptErr := opts.Prompt("Paste the GitLab callback URL (or press Enter to keep waiting): ")
			if promptErr != nil {
				return nil, promptErr
			}
			parsed, parseErr := misc.ParseOAuthCallback(input)
			if parseErr != nil {
				return nil, parseErr
			}
			if parsed == nil {
				continue
			}
			result = &gitlabauth.OAuthResult{
				Code:  parsed.Code,
				State: parsed.State,
				Error: parsed.Error,
			}
			break waitForCallback
		}
	}

	if result.Error != "" {
		return nil, fmt.Errorf("gitlab oauth returned error: %s", result.Error)
	}
	if result.State != state {
		return nil, fmt.Errorf("gitlab auth: state mismatch")
	}

	tokenResp, err := client.ExchangeCodeForTokens(ctx, baseURL, clientID, clientSecret, redirectURI, result.Code, pkceCodes.CodeVerifier)
	if err != nil {
		return nil, err
	}
	accessToken := strings.TrimSpace(tokenResp.AccessToken)
	if accessToken == "" {
		return nil, fmt.Errorf("gitlab auth: missing access token")
	}

	user, err := client.GetCurrentUser(ctx, baseURL, accessToken)
	if err != nil {
		return nil, err
	}
	direct, err := client.FetchDirectAccess(ctx, baseURL, accessToken)
	if err != nil {
		return nil, err
	}

	identifier := gitLabAccountIdentifier(user)
	fileName := fmt.Sprintf("gitlab-%s.json", sanitizeGitLabFileName(identifier))
	metadata := buildGitLabAuthMetadata(baseURL, gitLabLoginModeOAuth, tokenResp, direct)
	metadata["auth_kind"] = "oauth"
	metadata[gitLabOAuthClientIDMetadataKey] = clientID
	if strings.TrimSpace(clientSecret) != "" {
		metadata[gitLabOAuthClientSecretMetadataKey] = clientSecret
	}
	metadata["username"] = strings.TrimSpace(user.Username)
	if email := strings.TrimSpace(primaryGitLabEmail(user)); email != "" {
		metadata["email"] = email
	}
	metadata["name"] = strings.TrimSpace(user.Name)

	fmt.Println("GitLab Duo authentication successful")

	return &coreauth.Auth{
		ID:       fileName,
		Provider: a.Provider(),
		FileName: fileName,
		Label:    identifier,
		Metadata: metadata,
	}, nil
}

func (a *GitLabAuthenticator) loginPAT(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
	client := gitlabauth.NewAuthClient(cfg)
	baseURL := a.resolveString(opts, gitLabBaseURLMetadataKey, gitlabauth.DefaultBaseURL)
	token, err := a.requireInput(opts, gitLabPersonalAccessTokenMetadataKey, "Enter GitLab personal access token: ")
	if err != nil {
		return nil, err
	}

	user, err := client.GetCurrentUser(ctx, baseURL, token)
	if err != nil {
		return nil, err
	}
	_, err = client.GetPersonalAccessTokenSelf(ctx, baseURL, token)
	if err != nil {
		return nil, err
	}
	direct, err := client.FetchDirectAccess(ctx, baseURL, token)
	if err != nil {
		return nil, err
	}

	identifier := gitLabAccountIdentifier(user)
	fileName := fmt.Sprintf("gitlab-%s-pat.json", sanitizeGitLabFileName(identifier))
	metadata := buildGitLabAuthMetadata(baseURL, gitLabLoginModePAT, nil, direct)
	metadata["auth_kind"] = "personal_access_token"
	metadata[gitLabPersonalAccessTokenMetadataKey] = strings.TrimSpace(token)
	metadata["token_preview"] = maskGitLabToken(token)
	metadata["username"] = strings.TrimSpace(user.Username)
	if email := strings.TrimSpace(primaryGitLabEmail(user)); email != "" {
		metadata["email"] = email
	}
	metadata["name"] = strings.TrimSpace(user.Name)

	fmt.Println("GitLab Duo PAT authentication successful")

	return &coreauth.Auth{
		ID:       fileName,
		Provider: a.Provider(),
		FileName: fileName,
		Label:    identifier + " (PAT)",
		Metadata: metadata,
	}, nil
}

func buildGitLabAuthMetadata(baseURL, mode string, tokenResp *gitlabauth.TokenResponse, direct *gitlabauth.DirectAccessResponse) map[string]any {
	metadata := map[string]any{
		"type":                     "gitlab",
		"auth_method":              strings.TrimSpace(mode),
		gitLabBaseURLMetadataKey:   gitlabauth.NormalizeBaseURL(baseURL),
		"last_refresh":             time.Now().UTC().Format(time.RFC3339),
		"refresh_interval_seconds": 240,
	}
	if tokenResp != nil {
		metadata["access_token"] = strings.TrimSpace(tokenResp.AccessToken)
		if refreshToken := strings.TrimSpace(tokenResp.RefreshToken); refreshToken != "" {
			metadata["refresh_token"] = refreshToken
		}
		if tokenType := strings.TrimSpace(tokenResp.TokenType); tokenType != "" {
			metadata["token_type"] = tokenType
		}
		if scope := strings.TrimSpace(tokenResp.Scope); scope != "" {
			metadata["scope"] = scope
		}
		if expiry := gitlabauth.TokenExpiry(time.Now(), tokenResp); !expiry.IsZero() {
			metadata["oauth_expires_at"] = expiry.Format(time.RFC3339)
		}
	}
	mergeGitLabDirectAccessMetadata(metadata, direct)
	return metadata
}

func mergeGitLabDirectAccessMetadata(metadata map[string]any, direct *gitlabauth.DirectAccessResponse) {
	if metadata == nil || direct == nil {
		return
	}
	if base := strings.TrimSpace(direct.BaseURL); base != "" {
		metadata["duo_gateway_base_url"] = base
	}
	if token := strings.TrimSpace(direct.Token); token != "" {
		metadata["duo_gateway_token"] = token
	}
	if direct.ExpiresAt > 0 {
		expiry := time.Unix(direct.ExpiresAt, 0).UTC()
		metadata["duo_gateway_expires_at"] = expiry.Format(time.RFC3339)
		now := time.Now().UTC()
		if ttl := expiry.Sub(now); ttl > 0 {
			interval := int(ttl.Seconds()) / 2
			switch {
			case interval < 60:
				interval = 60
			case interval > 240:
				interval = 240
			}
			metadata["refresh_interval_seconds"] = interval
		}
	}
	if len(direct.Headers) > 0 {
		headers := make(map[string]string, len(direct.Headers))
		for key, value := range direct.Headers {
			key = strings.TrimSpace(key)
			value = strings.TrimSpace(value)
			if key == "" || value == "" {
				continue
			}
			headers[key] = value
		}
		if len(headers) > 0 {
			metadata["duo_gateway_headers"] = headers
		}
	}
	if direct.ModelDetails != nil {
		modelDetails := map[string]any{}
		if provider := strings.TrimSpace(direct.ModelDetails.ModelProvider); provider != "" {
			modelDetails["model_provider"] = provider
			metadata["model_provider"] = provider
		}
		if model := strings.TrimSpace(direct.ModelDetails.ModelName); model != "" {
			modelDetails["model_name"] = model
			metadata["model_name"] = model
		}
		if len(modelDetails) > 0 {
			metadata["model_details"] = modelDetails
		}
	}
}

func (a *GitLabAuthenticator) resolveString(opts *LoginOptions, key, fallback string) string {
	if opts != nil && opts.Metadata != nil {
		if value := strings.TrimSpace(opts.Metadata[key]); value != "" {
			return value
		}
	}
	for _, envKey := range gitLabEnvKeys(key) {
		if raw, ok := os.LookupEnv(envKey); ok {
			if trimmed := strings.TrimSpace(raw); trimmed != "" {
				return trimmed
			}
		}
	}
	if strings.TrimSpace(fallback) != "" {
		return fallback
	}
	return ""
}

func (a *GitLabAuthenticator) requireInput(opts *LoginOptions, key, prompt string) (string, error) {
	if value := a.resolveString(opts, key, ""); value != "" {
		return value, nil
	}
	if opts != nil && opts.Prompt != nil {
		value, err := opts.Prompt(prompt)
		if err != nil {
			return "", err
		}
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed, nil
		}
	}
	return "", fmt.Errorf("gitlab auth: missing required %s", key)
}

func (a *GitLabAuthenticator) optionalInput(opts *LoginOptions, key, prompt string) (string, error) {
	if value := a.resolveString(opts, key, ""); value != "" {
		return value, nil
	}
	if opts != nil && opts.Prompt != nil {
		value, err := opts.Prompt(prompt)
		if err != nil {
			return "", err
		}
		return strings.TrimSpace(value), nil
	}
	return "", nil
}

func primaryGitLabEmail(user *gitlabauth.User) string {
	if user == nil {
		return ""
	}
	if value := strings.TrimSpace(user.Email); value != "" {
		return value
	}
	return strings.TrimSpace(user.PublicEmail)
}

func gitLabAccountIdentifier(user *gitlabauth.User) string {
	if user == nil {
		return "user"
	}
	for _, value := range []string{user.Username, primaryGitLabEmail(user), user.Name} {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return "user"
}

func sanitizeGitLabFileName(value string) string {
	value = strings.TrimSpace(strings.ToLower(value))
	if value == "" {
		return "user"
	}
	var builder strings.Builder
	lastDash := false
	for _, r := range value {
		switch {
		case r >= 'a' && r <= 'z':
			builder.WriteRune(r)
			lastDash = false
		case r >= '0' && r <= '9':
			builder.WriteRune(r)
			lastDash = false
		case r == '-' || r == '_' || r == '.':
			builder.WriteRune(r)
			lastDash = false
		default:
			if !lastDash {
				builder.WriteRune('-')
				lastDash = true
			}
		}
	}
	result := strings.Trim(builder.String(), "-")
	if result == "" {
		return "user"
	}
	return result
}

func maskGitLabToken(token string) string {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return ""
	}
	if len(trimmed) <= 8 {
		return trimmed
	}
	return trimmed[:4] + "..." + trimmed[len(trimmed)-4:]
}

func gitLabEnvKeys(key string) []string {
	switch strings.TrimSpace(key) {
	case gitLabBaseURLMetadataKey:
		return []string{"GITLAB_BASE_URL"}
	case gitLabOAuthClientIDMetadataKey:
		return []string{"GITLAB_OAUTH_CLIENT_ID"}
	case gitLabOAuthClientSecretMetadataKey:
		return []string{"GITLAB_OAUTH_CLIENT_SECRET"}
	case gitLabPersonalAccessTokenMetadataKey:
		return []string{"GITLAB_PERSONAL_ACCESS_TOKEN"}
	default:
		return nil
	}
}
