package browser

import (
	"context"
	"fmt"
	"os/exec"
	"runtime"
	"sync"
	"sync/atomic"
	"time"
)

// State represents the browser state.
type State uint32

const (
	StateIdle State = iota
	StateStarting
	StateRunning
	StateStopping
	StateStopped
	StateFailed
)

func (s State) String() string {
	switch s {
	case StateIdle:
		return "idle"
	case StateStarting:
		return "starting"
	case StateRunning:
		return "running"
	case StateStopping:
		return "stopping"
	case StateStopped:
		return "stopped"
	case StateFailed:
		return "failed"
	default:
		return "unknown"
	}
}

// Config holds browser configuration.
type Config struct {
	ID         string // Browser instance ID
	URL        string // Initial URL to open
	Headless   bool   // Run in headless mode (default: true)
	BinaryPath string // Path to Chrome binary (auto-detected if empty)
	ProxyURL   string // Proxy server URL (optional)
	Path       string // Project path for session scoping
	WindowSize string // Window size (default: "1920,1080")
}

// Browser represents a running browser instance.
type Browser struct {
	config Config
	state  atomic.Uint32
	pid    atomic.Int32
	cmd    *exec.Cmd
	cancel context.CancelFunc
	done   chan struct{}
	err    error
	errMu  sync.RWMutex

	startedAt atomic.Pointer[time.Time]
}

// Info contains information about a browser instance.
type Info struct {
	ID         string `json:"id"`
	State      string `json:"state"`
	PID        int    `json:"pid,omitempty"`
	URL        string `json:"url,omitempty"`
	Headless   bool   `json:"headless"`
	BinaryPath string `json:"binary_path,omitempty"`
	ProxyURL   string `json:"proxy_url,omitempty"`
	Path       string `json:"path,omitempty"`
	StartedAt  string `json:"started_at,omitempty"`
	Error      string `json:"error,omitempty"`
}

// New creates a new browser instance with the given configuration.
func New(config Config) *Browser {
	if config.WindowSize == "" {
		config.WindowSize = "1920,1080"
	}
	return &Browser{
		config: config,
		done:   make(chan struct{}),
	}
}

// Start starts the browser.
func (b *Browser) Start(ctx context.Context) error {
	if !b.compareAndSwapState(StateIdle, StateStarting) {
		return fmt.Errorf("browser already started")
	}

	// Find Chrome binary
	binaryPath := b.config.BinaryPath
	if binaryPath == "" {
		discoverer := DefaultDiscoverer()
		binaryPath = discoverer.Find()
		if binaryPath == "" {
			b.setState(StateFailed)
			b.setError(fmt.Errorf("Chrome not found: install Chrome or specify binary_path"))
			close(b.done)
			return b.Error()
		}
	}

	// Build Chrome arguments
	args := b.buildArgs()

	ctx, cancel := context.WithCancel(ctx)
	b.cancel = cancel

	b.cmd = exec.CommandContext(ctx, binaryPath, args...)

	if err := b.cmd.Start(); err != nil {
		b.setState(StateFailed)
		b.setError(fmt.Errorf("failed to start Chrome: %w", err))
		close(b.done)
		return b.Error()
	}

	b.pid.Store(int32(b.cmd.Process.Pid))
	now := time.Now()
	b.startedAt.Store(&now)
	b.setState(StateRunning)

	// Monitor process in goroutine
	go func() {
		defer close(b.done)
		err := b.cmd.Wait()
		if err != nil && ctx.Err() == nil {
			// Process exited with error (not cancelled)
			b.setError(fmt.Errorf("Chrome exited: %w", err))
			b.setState(StateFailed)
		} else {
			b.setState(StateStopped)
		}
	}()

	return nil
}

// buildArgs constructs the Chrome command-line arguments.
func (b *Browser) buildArgs() []string {
	args := []string{
		"--disable-background-networking",
		"--disable-default-apps",
		"--disable-extensions",
		"--disable-sync",
		"--disable-dev-shm-usage", // Docker/container support
		"--no-first-run",
		"--no-default-browser-check",
		fmt.Sprintf("--window-size=%s", b.config.WindowSize),
		// Prevent OS credential/keyring popups that freeze headless sessions
		"--password-store=basic",
	}

	if runtime.GOOS == "darwin" {
		args = append(args, "--use-mock-keychain")
	}

	if b.config.Headless {
		args = append(args,
			"--headless=new", // Chrome 109+ headless mode
			"--disable-gpu",
		)
	}

	if b.config.ProxyURL != "" {
		args = append(args, fmt.Sprintf("--proxy-server=%s", b.config.ProxyURL))
	}

	// Add URL as last argument
	if b.config.URL != "" {
		args = append(args, b.config.URL)
	}

	return args
}

// Stop stops the browser.
func (b *Browser) Stop(ctx context.Context) error {
	state := b.State()
	if state == StateStopped || state == StateFailed || state == StateIdle {
		return nil // Already stopped
	}

	if !b.compareAndSwapState(StateRunning, StateStopping) {
		// Try from starting state
		if !b.compareAndSwapState(StateStarting, StateStopping) {
			return nil // Already stopping or stopped
		}
	}

	if b.cancel != nil {
		b.cancel()
	}

	if b.cmd != nil && b.cmd.Process != nil {
		if err := b.cmd.Process.Kill(); err != nil {
			return fmt.Errorf("failed to kill browser: %w", err)
		}
	}

	// Wait for done or context timeout
	select {
	case <-b.done:
	case <-ctx.Done():
		return ctx.Err()
	}

	b.setState(StateStopped)
	return nil
}

// State returns the current browser state.
func (b *Browser) State() State {
	return State(b.state.Load())
}

// PID returns the process ID if running.
func (b *Browser) PID() int {
	return int(b.pid.Load())
}

// ID returns the browser ID.
func (b *Browser) ID() string {
	return b.config.ID
}

// Path returns the project path for this browser.
func (b *Browser) Path() string {
	return b.config.Path
}

// Error returns the last error if any.
func (b *Browser) Error() error {
	b.errMu.RLock()
	defer b.errMu.RUnlock()
	return b.err
}

// Done returns a channel that's closed when the browser exits.
func (b *Browser) Done() <-chan struct{} {
	return b.done
}

// Info returns information about the browser.
func (b *Browser) Info() Info {
	info := Info{
		ID:         b.config.ID,
		State:      b.State().String(),
		PID:        b.PID(),
		URL:        b.config.URL,
		Headless:   b.config.Headless,
		BinaryPath: b.config.BinaryPath,
		ProxyURL:   b.config.ProxyURL,
		Path:       b.config.Path,
	}

	if startedAt := b.startedAt.Load(); startedAt != nil {
		info.StartedAt = startedAt.Format(time.RFC3339)
	}

	b.errMu.RLock()
	if b.err != nil {
		info.Error = b.err.Error()
	}
	b.errMu.RUnlock()

	return info
}

func (b *Browser) setState(s State) {
	b.state.Store(uint32(s))
}

func (b *Browser) compareAndSwapState(old, new State) bool {
	return b.state.CompareAndSwap(uint32(old), uint32(new))
}

func (b *Browser) setError(err error) {
	b.errMu.Lock()
	b.err = err
	b.errMu.Unlock()
}
