package bkosmi

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/chromedp/chromedp"
	"github.com/sirupsen/logrus"
)

const (
	// Check token expiry 7 days before it expires
	tokenExpiryCheckBuffer = 7 * 24 * time.Hour
)

// BrowserAuthManager handles automated browser-based authentication
type BrowserAuthManager struct {
	email          string
	password       string
	token          string
	tokenExpiry    time.Time
	log            *logrus.Entry
	lastCheckTime  time.Time
	checkInterval  time.Duration
}

// NewBrowserAuthManager creates a new browser-based authentication manager
func NewBrowserAuthManager(email, password string, log *logrus.Entry) *BrowserAuthManager {
	return &BrowserAuthManager{
		email:         email,
		password:      password,
		log:           log,
		checkInterval: 24 * time.Hour, // Check daily for token expiry
	}
}

// GetToken returns a valid token, obtaining a new one via browser if needed
func (b *BrowserAuthManager) GetToken() (string, error) {
	// Check if we need to obtain or refresh the token
	if b.token == "" || b.shouldRefreshToken() {
		b.log.Info("Obtaining authentication token via browser automation...")
		if err := b.loginViaBrowser(); err != nil {
			return "", &AuthError{
				Op:     "browser_login",
				Reason: "failed to obtain token via browser",
				Err:    err,
			}
		}
	}

	return b.token, nil
}

// shouldRefreshToken checks if the token needs to be refreshed
func (b *BrowserAuthManager) shouldRefreshToken() bool {
	// No token yet
	if b.token == "" {
		return true
	}

	// Token expired or about to expire
	if time.Now().After(b.tokenExpiry.Add(-tokenExpiryCheckBuffer)) {
		b.log.Info("Token expired or expiring soon, will refresh")
		return true
	}

	// Periodic check (daily) to verify token is still valid
	if time.Since(b.lastCheckTime) > b.checkInterval {
		b.log.Debug("Performing periodic token validity check")
		b.lastCheckTime = time.Now()
		// For now, we trust the expiry time. Could add a validation check here.
	}

	return false
}

// loginViaBrowser uses chromedp to automate login and extract token
func (b *BrowserAuthManager) loginViaBrowser() error {
	// Set up Chrome options
	opts := append(chromedp.DefaultExecAllocatorOptions[:],
		chromedp.Flag("headless", true),
		chromedp.Flag("disable-gpu", true),
		chromedp.Flag("no-sandbox", true),
		chromedp.Flag("disable-dev-shm-usage", true),
	)
	
	// Create allocator context
	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
	defer cancel()
	
	// Create context with timeout
	ctx, cancel := chromedp.NewContext(allocCtx)
	defer cancel()

	// Set a reasonable timeout for the entire login process
	ctx, cancel = context.WithTimeout(ctx, 90*time.Second)
	defer cancel()

	var token string

	// Run the browser automation tasks
	err := chromedp.Run(ctx,
		// Navigate to Kosmi
		chromedp.Navigate("https://app.kosmi.io"),
		
		// Wait for page to load completely
		chromedp.WaitReady("body"),
		chromedp.Sleep(2*time.Second),
		
		// Click Login button (find by text content using JS with error handling)
		chromedp.ActionFunc(func(ctx context.Context) error {
			// First, log what buttons we can see
			var buttonTexts []string
			chromedp.Evaluate(`
				Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim())
			`, &buttonTexts).Do(ctx)
			b.log.Debugf("Found buttons: %v", buttonTexts)
			
			var found bool
			if err := chromedp.Evaluate(`
				(() => {
					const buttons = Array.from(document.querySelectorAll('button'));
					// Try both "Login" and "Log in"
					const btn = buttons.find(el => {
						const text = el.textContent.trim();
						return text === 'Login' || text === 'Log in';
					});
					if (btn) {
						btn.click();
						return true;
					}
					return false;
				})()
			`, &found).Do(ctx); err != nil {
				return err
			}
			if !found {
				return fmt.Errorf("Login button not found (found buttons: %v)", buttonTexts)
			}
			return nil
		}),
		
		// Wait for login modal
		chromedp.Sleep(3*time.Second),
		
		// Click "Login with Email" button
		chromedp.ActionFunc(func(ctx context.Context) error {
			// Log what buttons we can see now
			var buttonTexts []string
			chromedp.Evaluate(`
				Array.from(document.querySelectorAll('button')).map(el => el.textContent.trim())
			`, &buttonTexts).Do(ctx)
			b.log.Debugf("After clicking Log in, found buttons: %v", buttonTexts)
			
			var found bool
			if err := chromedp.Evaluate(`
				(() => {
					const btn = Array.from(document.querySelectorAll('button')).find(el => el.textContent.includes('Email'));
					if (btn) {
						btn.click();
						return true;
					}
					return false;
				})()
			`, &found).Do(ctx); err != nil {
				return err
			}
			if !found {
				return fmt.Errorf("Login with Email button not found (found buttons: %v)", buttonTexts)
			}
			return nil
		}),
		
		// Wait for email form
		chromedp.Sleep(3*time.Second),
		chromedp.ActionFunc(func(ctx context.Context) error {
			b.log.Debug("Waiting for password input...")
			return nil
		}),
		chromedp.WaitVisible(`input[type="password"]`, chromedp.ByQuery),
		
		chromedp.ActionFunc(func(ctx context.Context) error {
			b.log.Debug("Password input found, preparing to fill form...")
			return nil
		}),
		
		// Click on the email input to focus it
		chromedp.Click(`input[placeholder*="Email"], input[placeholder*="Username"]`, chromedp.ByQuery),
		chromedp.Sleep(200*time.Millisecond),
		
		// Type email character by character
		chromedp.ActionFunc(func(ctx context.Context) error {
			b.log.Debugf("Typing email: %s", b.email)
			return chromedp.SendKeys(`input[placeholder*="Email"], input[placeholder*="Username"]`, b.email, chromedp.ByQuery).Do(ctx)
		}),
		
		chromedp.Sleep(500*time.Millisecond),
		
		// Click on the password input to focus it
		chromedp.Click(`input[type="password"]`, chromedp.ByQuery),
		chromedp.Sleep(200*time.Millisecond),
		
		// Type password character by character
		chromedp.ActionFunc(func(ctx context.Context) error {
			b.log.Debugf("Typing password (length: %d)", len(b.password))
			return chromedp.SendKeys(`input[type="password"]`, b.password, chromedp.ByQuery).Do(ctx)
		}),
		
		// Verify password was filled correctly
		chromedp.ActionFunc(func(ctx context.Context) error {
			var actualLength int
			chromedp.Evaluate(`
				(() => {
					const passwordInput = document.querySelector('input[type="password"]');
					return passwordInput ? passwordInput.value.length : 0;
				})()
			`, &actualLength).Do(ctx)
			
			b.log.Debugf("Password filled (actual length: %d, expected: %d)", actualLength, len(b.password))
			
			if actualLength != len(b.password) {
				return fmt.Errorf("password length mismatch: got %d, expected %d", actualLength, len(b.password))
			}
			return nil
		}),
		
		chromedp.Sleep(500*time.Millisecond),
		
		// Wait a moment for form validation
		chromedp.Sleep(1*time.Second),
		
		// Click the login submit button (be very specific)
		chromedp.ActionFunc(func(ctx context.Context) error {
			b.log.Debug("Attempting to click submit button...")
			var result string
			if err := chromedp.Evaluate(`
				(() => {
					const buttons = Array.from(document.querySelectorAll('button'));
					
					// Find the submit button in the login form
					// It should be visible, enabled, and contain "Login" but not be the main nav button
					const submitBtn = buttons.find(el => {
						const text = el.textContent.trim();
						const isLoginBtn = text === 'Login' || text.startsWith('Login');
						const isEnabled = !el.disabled;
						const isVisible = el.offsetParent !== null;
						const isInForm = el.closest('form') !== null || el.closest('[role="dialog"]') !== null;
						
						return isLoginBtn && isEnabled && isVisible && isInForm;
					});
					
					if (submitBtn) {
						submitBtn.click();
						return 'CLICKED: ' + submitBtn.textContent.trim();
					}
					
					return 'NOT_FOUND';
				})()
			`, &result).Do(ctx); err != nil {
				return err
			}
			
			b.log.Debugf("Submit button result: %s", result)
			
			if result == "NOT_FOUND" {
				return fmt.Errorf("Login submit button not found or not clickable")
			}
			
			b.log.Debug("Submit button clicked")
			return nil
		}),
		
		// Wait for login to complete (page will reload/redirect)
		chromedp.Sleep(5*time.Second),
		
		// Check if login succeeded by looking for error messages
		chromedp.ActionFunc(func(ctx context.Context) error {
			var errorText string
			chromedp.Evaluate(`
				(() => {
					const errorEl = document.querySelector('[role="alert"], .error, .alert-error');
					return errorEl ? errorEl.textContent : '';
				})()
			`, &errorText).Do(ctx)
			
			if errorText != "" {
				return fmt.Errorf("login failed with error: %s", errorText)
			}
			
			b.log.Debug("No error messages found, checking token...")
			return nil
		}),
		
		// Extract token from localStorage
		chromedp.Evaluate(`localStorage.getItem('token')`, &token),
		
		// Verify the token is not anonymous by checking if user info exists
		chromedp.ActionFunc(func(ctx context.Context) error {
			var userInfo string
			chromedp.Evaluate(`
				(() => {
					try {
						const token = localStorage.getItem('token');
						if (!token) return 'NO_TOKEN';
						
						// Decode JWT payload (middle part)
						const parts = token.split('.');
						if (parts.length !== 3) return 'INVALID_TOKEN';
						
						const payload = JSON.parse(atob(parts[1]));
						return JSON.stringify({
							sub: payload.sub,
							typ: payload.typ,
							isAnon: payload.sub ? false : true
						});
					} catch (e) {
						return 'ERROR: ' + e.message;
					}
				})()
			`, &userInfo).Do(ctx)
			
			b.log.Debugf("Token info from browser: %s", userInfo)
			return nil
		}),
	)

	if err != nil {
		return fmt.Errorf("browser automation failed: %w", err)
	}

	if token == "" {
		return fmt.Errorf("no token found in localStorage after login")
	}

	b.token = token
	b.log.Infof("✅ Successfully obtained token via browser automation")
	b.log.Infof("   Email used: %s", b.email)
	b.log.Infof("   Token (first 50 chars): %s...", token[:min(50, len(token))])
	b.log.Infof("   Token (last 50 chars): ...%s", token[max(0, len(token)-50):])

	// Parse token to get expiry
	if err := b.parseTokenExpiry(); err != nil {
		b.log.Warnf("Failed to parse token expiry: %v", err)
		// Default to 1 year if we can't parse
		b.tokenExpiry = time.Now().Add(365 * 24 * time.Hour)
	}

	b.lastCheckTime = time.Now()
	
	expiresIn := time.Until(b.tokenExpiry)
	b.log.Infof("Token expires in: %v", expiresIn.Round(24*time.Hour))

	return nil
}

// parseTokenExpiry extracts the expiry time from the JWT token
func (b *BrowserAuthManager) parseTokenExpiry() error {
	// JWT format: header.payload.signature
	parts := strings.Split(b.token, ".")
	if len(parts) != 3 {
		return fmt.Errorf("invalid JWT format")
	}

	// Decode the payload (base64url without padding)
	payload := parts[1]
	// Add padding if needed
	switch len(payload) % 4 {
	case 2:
		payload += "=="
	case 3:
		payload += "="
	}
	
	// Replace URL-safe characters
	payload = strings.ReplaceAll(payload, "-", "+")
	payload = strings.ReplaceAll(payload, "_", "/")
	
	decoded, err := base64.StdEncoding.DecodeString(payload)
	if err != nil {
		return fmt.Errorf("failed to decode JWT payload: %w", err)
	}

	// Parse JSON
	var claims struct {
		Exp int64  `json:"exp"`
		Sub string `json:"sub"`
		Typ string `json:"typ"`
	}
	if err := json.Unmarshal(decoded, &claims); err != nil {
		return fmt.Errorf("failed to parse JWT claims: %w", err)
	}

	if claims.Exp == 0 {
		return fmt.Errorf("no expiry in token")
	}

	b.tokenExpiry = time.Unix(claims.Exp, 0)
	b.log.Infof("   Token user ID (sub): %s", claims.Sub)
	b.log.Infof("   Token type (typ): %s", claims.Typ)
	return nil
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// IsAuthenticated checks if we have a valid token
func (b *BrowserAuthManager) IsAuthenticated() bool {
	return b.token != "" && time.Now().Before(b.tokenExpiry)
}

// GetUserID returns the user ID from the token (if available)
func (b *BrowserAuthManager) GetUserID() string {
	if b.token == "" {
		return ""
	}
	
	parts := strings.Split(b.token, ".")
	if len(parts) != 3 {
		return ""
	}
	
	payload := parts[1]
	switch len(payload) % 4 {
	case 2:
		payload += "=="
	case 3:
		payload += "="
	}
	
	payload = strings.ReplaceAll(payload, "-", "+")
	payload = strings.ReplaceAll(payload, "_", "/")
	
	decoded, err := base64.StdEncoding.DecodeString(payload)
	if err != nil {
		return ""
	}
	
	var claims struct {
		Sub string `json:"sub"`
	}
	if err := json.Unmarshal(decoded, &claims); err != nil {
		return ""
	}
	
	return claims.Sub
}

