// Package opencode provides a CodeAgent implementation for the OpenCode server.
package opencode

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

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// Adapter implements port.CodeAgent using OpenCode's HTTP server.
type Adapter struct {
	client *Client

	// Track active sessions for cancellation
	activeSessions map[string]context.CancelFunc
	sessionsMu     sync.Mutex
}

// NewAdapter creates a new OpenCode adapter.
func NewAdapter(cfg ClientConfig) *Adapter {
	return &Adapter{
		client:         NewClient(cfg),
		activeSessions: make(map[string]context.CancelFunc),
	}
}

// Ensure Adapter implements port.CodeAgent at compile time.
var _ port.CodeAgent = (*Adapter)(nil)

// Name returns a human-readable name for this agent.
func (a *Adapter) Name() string {
	return "OpenCode"
}

// Provider returns the agent provider identifier.
func (a *Adapter) Provider() domain.AgentProvider {
	return domain.AgentProviderOpenCode
}

// Execute runs an OpenCode command and streams events to the handler.
func (a *Adapter) Execute(ctx context.Context, req *domain.AgentRequest, handler domain.AgentEventHandler) (*domain.AgentResult, error) {
	if req.Prompt == "" {
		return nil, fmt.Errorf("prompt is required")
	}

	// Create cancellable context
	execCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Apply timeout if specified
	if req.Timeout > 0 {
		var timeoutCancel context.CancelFunc
		execCtx, timeoutCancel = context.WithTimeout(execCtx, req.Timeout)
		defer timeoutCancel()
	}

	startTime := time.Now()

	// Get or create session
	sessionID := req.SessionID
	if sessionID == "" {
		session, err := a.client.CreateSession(execCtx, &CreateSessionRequest{
			Title: fmt.Sprintf("rdev-%d", time.Now().Unix()),
		})
		if err != nil {
			return &domain.AgentResult{
				ExitCode: 1,
				Error:    fmt.Errorf("create session: %w", err),
			}, nil
		}
		sessionID = session.ID
	}

	// Track session for potential cancellation
	a.sessionsMu.Lock()
	a.activeSessions[sessionID] = cancel
	a.sessionsMu.Unlock()

	defer func() {
		a.sessionsMu.Lock()
		delete(a.activeSessions, sessionID)
		a.sessionsMu.Unlock()
	}()

	// Emit session started event
	handler(domain.AgentEvent{
		Type:      domain.AgentEventOutput,
		Timestamp: time.Now(),
		Content:   "Session started",
		Metadata: map[string]any{
			"session_id": sessionID,
		},
	})

	// Subscribe to SSE events for real-time updates
	eventChan, err := a.client.SubscribeEvents(execCtx)
	if err != nil {
		// Non-fatal: we can still use sync API
		handler(domain.AgentEvent{
			Type:      domain.AgentEventError,
			Timestamp: time.Now(),
			Content:   fmt.Sprintf("SSE subscription failed: %v (using sync mode)", err),
		})
	}

	// Start event consumer if we have SSE
	var eventWg sync.WaitGroup
	if eventChan != nil {
		eventWg.Add(1)
		go func() {
			defer eventWg.Done()
			a.consumeEvents(execCtx, eventChan, handler)
		}()
	}

	// Build message request
	msgReq := &SendMessageRequest{
		Parts: []MessagePart{
			{Type: "text", Content: req.Prompt},
		},
	}

	// Set model if specified
	if req.Model != "" {
		msgReq.Model = req.Model
	}

	// Set tools if specified
	if len(req.AllowedTools) > 0 {
		msgReq.Tools = req.AllowedTools
	}

	// Send message (synchronous call that waits for response)
	resp, err := a.client.SendMessage(execCtx, sessionID, msgReq)
	if err != nil {
		// Check if cancelled
		if execCtx.Err() == context.Canceled {
			return &domain.AgentResult{
				SessionID:  sessionID,
				ExitCode:   1,
				DurationMs: time.Since(startTime).Milliseconds(),
				Error:      domain.ErrCommandCancelled,
			}, nil
		}

		return &domain.AgentResult{
			SessionID:  sessionID,
			ExitCode:   1,
			DurationMs: time.Since(startTime).Milliseconds(),
			Error:      fmt.Errorf("send message: %w", err),
		}, nil
	}

	// Process response parts
	var finalOutput strings.Builder
	var hasError bool
	var errorContent string
	for _, part := range resp.Parts {
		event := a.partToEvent(part)
		handler(event)

		if part.Type == "text" && part.Content != "" {
			finalOutput.WriteString(part.Content)
		}
		if part.Type == "error" {
			hasError = true
			errorContent = part.Content
		}
	}

	// Emit completion event
	duration := time.Since(startTime)
	completionStatus := "success"
	if hasError {
		completionStatus = "error"
	}
	handler(domain.AgentEvent{
		Type:      domain.AgentEventComplete,
		Timestamp: time.Now(),
		Metadata: map[string]any{
			"status":      completionStatus,
			"duration_ms": duration.Milliseconds(),
		},
	})

	// Wait for SSE consumer to finish (with timeout)
	if eventChan != nil {
		cancel() // Signal event consumer to stop
		waitDone := make(chan struct{})
		go func() {
			eventWg.Wait()
			close(waitDone)
		}()

		select {
		case <-waitDone:
		case <-time.After(2 * time.Second):
			// Event consumer didn't stop in time, continue anyway
		}
	}

	result := &domain.AgentResult{
		SessionID:   sessionID,
		ExitCode:    0,
		DurationMs:  duration.Milliseconds(),
		FinalOutput: finalOutput.String(),
	}

	// Set error state if error parts were found
	if hasError {
		result.ExitCode = 1
		if errorContent != "" {
			result.Error = fmt.Errorf("agent error: %s", errorContent)
		}
	}

	return result, nil
}

// consumeEvents processes SSE events and dispatches them to the handler.
func (a *Adapter) consumeEvents(ctx context.Context, events <-chan SSEEvent, handler domain.AgentEventHandler) {
	for {
		select {
		case <-ctx.Done():
			return
		case event, ok := <-events:
			if !ok {
				return
			}
			agentEvent := a.sseToEvent(event)
			if agentEvent.Type != "" {
				handler(agentEvent)
			}
		}
	}
}

// sseToEvent converts an SSE event to a domain.AgentEvent.
func (a *Adapter) sseToEvent(sse SSEEvent) domain.AgentEvent {
	event := domain.AgentEvent{
		Timestamp: time.Now(),
		Metadata:  make(map[string]any),
	}

	// Parse event type
	switch sse.Event {
	case "server.connected":
		event.Type = domain.AgentEventOutput
		event.Content = "Connected to OpenCode server"
		return event

	case "message.created", "message.updated":
		event.Type = domain.AgentEventOutput
		// Try to parse data for content
		var data map[string]any
		if json.Unmarshal([]byte(sse.Data), &data) == nil {
			if content, ok := data["content"].(string); ok {
				event.Content = content
			}
		}
		return event

	case "tool.started":
		event.Type = domain.AgentEventToolUse
		var data map[string]any
		if json.Unmarshal([]byte(sse.Data), &data) == nil {
			if name, ok := data["name"].(string); ok {
				event.ToolName = name
				event.Content = name
			}
			if input, ok := data["input"].(map[string]any); ok {
				event.ToolInput = input
			}
		}
		return event

	case "tool.completed":
		event.Type = domain.AgentEventToolResult
		var data map[string]any
		if json.Unmarshal([]byte(sse.Data), &data) == nil {
			if output, ok := data["output"].(string); ok {
				event.Content = output
			}
		}
		return event

	case "session.completed":
		event.Type = domain.AgentEventComplete
		return event

	case "error":
		event.Type = domain.AgentEventError
		event.Content = sse.Data
		return event
	}

	// Unknown event type
	return domain.AgentEvent{}
}

// partToEvent converts a message part to a domain.AgentEvent.
func (a *Adapter) partToEvent(part MessagePart) domain.AgentEvent {
	event := domain.AgentEvent{
		Timestamp: time.Now(),
		Metadata:  make(map[string]any),
	}

	switch part.Type {
	case "text":
		event.Type = domain.AgentEventOutput
		event.Content = part.Content

	case "tool_use":
		event.Type = domain.AgentEventToolUse
		event.ToolName = part.Name
		event.Content = part.Name
		if input, ok := part.Input.(map[string]any); ok {
			event.ToolInput = input
		}

	case "tool_result":
		event.Type = domain.AgentEventToolResult
		event.Content = part.Content

	default:
		event.Type = domain.AgentEventOutput
		event.Content = part.Content
	}

	return event
}

// Cancel attempts to cancel a running session.
func (a *Adapter) Cancel(ctx context.Context, sessionID string) error {
	a.sessionsMu.Lock()
	cancel, exists := a.activeSessions[sessionID]
	if exists {
		// Call cancel while holding lock and delete to prevent double-cancel
		cancel()
		delete(a.activeSessions, sessionID)
	}
	a.sessionsMu.Unlock()

	// Abort on the server side regardless of local session state
	return a.client.AbortSession(ctx, sessionID)
}

// Capabilities returns what this agent supports.
func (a *Adapter) Capabilities() domain.AgentCapabilities {
	return domain.AgentCapabilities{
		Provider:                    domain.AgentProviderOpenCode,
		SupportsSessionContinuation: true,
		SupportsModelSelection:      true, // OpenCode supports multiple providers
		SupportsToolControl:         true,
		SupportedModels: []string{
			"claude-sonnet-4-20250514",
			"claude-opus-4-20250514",
			"gpt-4o",
			"gpt-4-turbo",
			"gemini-pro",
		},
		DefaultModel:      "claude-sonnet-4-20250514",
		MaxPromptLength:   0, // Unlimited
		SupportsStreaming: true,
	}
}

// DefaultAvailabilityTimeout is the maximum time to wait when checking agent availability.
// This timeout prevents blocking the caller when the server is slow or unresponsive.
const DefaultAvailabilityTimeout = 5 * time.Second

// Available checks if the OpenCode server is healthy.
func (a *Adapter) Available(ctx context.Context) bool {
	ctx, cancel := context.WithTimeout(ctx, DefaultAvailabilityTimeout)
	defer cancel()

	health, err := a.client.Health(ctx)
	if err != nil {
		return false
	}
	return health.Healthy
}
