package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent/drivercontract"
	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
)

const (
	openCodeSummarizeRequestTimeout = 2 * time.Minute
	openCodeSummarizeIdleTimeout    = 10 * time.Second
	openCodeErrorNameMetadataKey    = "opencode_error_name"
)

// openCodeSummarizeBoundaryFence joins the summarize HTTP completion with its
// source-derived SSE identity and success proofs: auto=false compaction part,
// bound summary assistant, fresh busy/Runner-idle, and session.compacted. It
// also preserves whether failure belongs to proactive maintenance or overflow
// recovery. Neither transport alone is sufficient to reopen prompt delivery.
type openCodeSummarizeBoundaryFence struct {
	sessionID           string
	sessionGeneration   uint64
	summarizeGeneration uint64
	proactive           bool
	idleCh              chan struct{}
	retiredCh           chan struct{}
	workerDone          chan struct{}
	compactionUserID    string
	summaryAssistantID  string
	compactionObserved  bool
	busyObserved        bool
	compactedObserved   bool
	httpSucceeded       bool
	idleObserved        bool
	idleClosed          bool
	retired             bool
}

func (d *OpenCodeDriver) openCodeSummarizeWorkerDone() <-chan struct{} {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.summarizeFence == nil {
		return nil
	}
	return d.summarizeFence.workerDone
}

// IsContextWindowExceeded implements drivercontract.ContextWindowExceededDriver.
// OpenCode reports provider context exhaustion through session.error text. Keep
// the phrases explicit for the same reason as Claude: generic "context" or
// "limit" errors can describe unrelated configuration failures.
func (d *OpenCodeDriver) IsContextWindowExceeded(ev drivercontract.ParsedEvent) bool {
	if ev.Kind != consts.ParsedEventKindError {
		return false
	}
	return isOpenCodeContextErrorEvent(ev.Message, ev.Metadata)
}

func isOpenCodeContextError(msg string) bool {
	normalized := normalizeClaudeErrorText(msg)
	if normalized == "" {
		return false
	}
	return strings.Contains(normalized, "contextoverflowerror") ||
		strings.Contains(normalized, "prompt is too long") ||
		strings.Contains(normalized, "exceeds the context window") ||
		strings.Contains(normalized, "exceeds context window") ||
		strings.Contains(normalized, "context window limit reached") ||
		strings.Contains(normalized, "reached its context window limit") ||
		strings.Contains(normalized, "context length exceeded") ||
		strings.Contains(normalized, "maximum context length") ||
		strings.Contains(normalized, "input too large") ||
		strings.Contains(normalized, "context window exceeded")
}

func isOpenCodeContextErrorEvent(message string, metadata map[string]any) bool {
	if name, _ := metadata[openCodeErrorNameMetadataKey].(string); strings.EqualFold(strings.TrimSpace(name), "ContextOverflowError") {
		return true
	}
	return isOpenCodeContextError(message)
}

func openCodeRuntimeErrorName(raw json.RawMessage) string {
	var fields struct {
		Name string `json:"name"`
		Type string `json:"type"`
	}
	if err := json.Unmarshal(raw, &fields); err != nil {
		return ""
	}
	return strings.TrimSpace(firstNonEmptyString(fields.Name, fields.Type))
}

// QueueContextCompaction queues OpenCode's native session summarize operation
// for the next safe idle boundary.
func (d *OpenCodeDriver) QueueContextCompaction(ev drivercontract.ParsedEvent) bool {
	if parsedBoolMetadata(ev.Metadata, contextCompactionFailedMetadataKey) ||
		!d.IsContextWindowExceeded(ev) {
		return false
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.summarizeInProgress || d.generationRetiring {
		return false
	}
	d.compactQueued = true
	return true
}

// StartIdleCompaction starts OpenCode's native session summarization. The HTTP
// endpoint may perform model work, so the request runs asynchronously after the
// process layer has durably marked the session unsafe. Completion or failure is
// translated back into the normal runtime event stream.
func (d *OpenCodeDriver) StartIdleCompaction(ctx context.Context, sessionID string) (bool, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	trimmedSessionID := strings.TrimSpace(sessionID)
	if trimmedSessionID == "" {
		return false, nil
	}
	d.deliveryMu.Lock()
	defer d.deliveryMu.Unlock()
	d.mu.Lock()
	currentSessionID := strings.TrimSpace(d.sessionID)
	if currentSessionID != "" && currentSessionID != trimmedSessionID {
		d.mu.Unlock()
		return false, nil
	}
	if !d.compactQueued || d.summarizeInProgress || !d.openCodeSummarizeStartReadyLocked(trimmedSessionID) {
		d.mu.Unlock()
		return false, nil
	}
	d.compactQueued = false
	d.summarizeInProgress = true
	d.summarizeGeneration++
	summarizeGeneration := d.summarizeGeneration
	sessionGeneration := d.sessionGeneration
	fence := &openCodeSummarizeBoundaryFence{
		sessionID:           trimmedSessionID,
		sessionGeneration:   sessionGeneration,
		summarizeGeneration: summarizeGeneration,
		idleCh:              make(chan struct{}),
		retiredCh:           make(chan struct{}),
		workerDone:          make(chan struct{}),
	}
	d.summarizeFence = fence
	d.resetOpenCodeRunnerMessageAdmissionLocked()
	d.turnActive = true
	d.sessionStatus = "busy"
	d.mu.Unlock()

	go d.runSessionSummarize(
		context.WithoutCancel(ctx),
		trimmedSessionID,
		sessionGeneration,
		summarizeGeneration,
		fence,
	)
	return true, nil
}

// StartProactiveIdleCompaction starts OpenCode's native session summarization
// proactively at an idle boundary when the latest observed input-context size
// crosses the configured threshold. This is between-turn compaction: no
// continuation is injected and the next user turn continues from the compacted
// context. Overflow-triggered compaction (compactQueued) always takes priority
// so an exhausted context is retried rather than merely summarized.
func (d *OpenCodeDriver) StartProactiveIdleCompaction(ctx context.Context, sessionID string) (bool, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	trimmedSessionID := strings.TrimSpace(sessionID)
	if trimmedSessionID == "" {
		return false, nil
	}
	d.deliveryMu.Lock()
	defer d.deliveryMu.Unlock()
	d.mu.Lock()
	currentSessionID := strings.TrimSpace(d.sessionID)
	if currentSessionID != "" && currentSessionID != trimmedSessionID {
		d.mu.Unlock()
		return false, nil
	}
	if d.compactQueued ||
		d.summarizeInProgress ||
		!d.proactiveCompactionDueLocked() ||
		!d.openCodeSummarizeStartReadyLocked(trimmedSessionID) {
		d.mu.Unlock()
		return false, nil
	}
	d.summarizeInProgress = true
	d.summarizeGeneration++
	summarizeGeneration := d.summarizeGeneration
	sessionGeneration := d.sessionGeneration
	fence := &openCodeSummarizeBoundaryFence{
		sessionID:           trimmedSessionID,
		sessionGeneration:   sessionGeneration,
		summarizeGeneration: summarizeGeneration,
		proactive:           true,
		idleCh:              make(chan struct{}),
		retiredCh:           make(chan struct{}),
		workerDone:          make(chan struct{}),
	}
	d.summarizeFence = fence
	d.resetOpenCodeRunnerMessageAdmissionLocked()
	d.turnActive = true
	d.sessionStatus = "busy"
	d.mu.Unlock()
	go d.runSessionSummarize(
		context.WithoutCancel(ctx),
		trimmedSessionID,
		sessionGeneration,
		summarizeGeneration,
		fence,
	)
	return true, nil
}

// proactiveSummarizeInProgressLocked reports whether sessionID belongs to the
// current proactive summarize operation. The caller must hold d.mu.
func (d *OpenCodeDriver) proactiveSummarizeInProgressLocked(sessionID string) bool {
	fence := d.summarizeFence
	return d.summarizeInProgress &&
		fence != nil &&
		fence.proactive &&
		!fence.retired &&
		fence.sessionGeneration == d.sessionGeneration &&
		fence.summarizeGeneration == d.summarizeGeneration &&
		strings.TrimSpace(fence.sessionID) == strings.TrimSpace(sessionID)
}

// openCodeSummarizeStartReadyLocked verifies that no prompt mutation or
// retirement can overlap a native summarize. The caller must hold d.mu.
func (d *OpenCodeDriver) openCodeSummarizeStartReadyLocked(sessionID string) bool {
	if d.generationRetiring || d.sseDeliveryBlocked {
		return false
	}
	if strings.TrimSpace(d.sessionID) != strings.TrimSpace(sessionID) {
		return false
	}
	if d.promptAwaitingRunner != nil || d.turnActive || strings.TrimSpace(d.sessionStatus) != "idle" {
		return false
	}
	for _, pending := range d.pendingRuntimeInputs {
		pendingSessionID := strings.TrimSpace(pending.SessionID)
		if pendingSessionID == "" || pendingSessionID == strings.TrimSpace(sessionID) {
			return false
		}
	}
	return true
}

// proactiveCompactionDueLocked reports whether a proactive summarize should fire
// now. The caller must hold d.mu.
func (d *OpenCodeDriver) proactiveCompactionDueLocked() bool {
	if d.proactiveCompactionThreshold == 0 {
		return false
	}
	if d.lastInputContextTokens < d.proactiveCompactionThreshold {
		return false
	}
	if !d.lastSummarizeCompletedAt.IsZero() && time.Since(d.lastSummarizeCompletedAt) < proactiveIdleCompactionCooldown {
		return false
	}
	return true
}

func (d *OpenCodeDriver) runSessionSummarize(
	ctx context.Context,
	sessionID string,
	sessionGeneration uint64,
	summarizeGeneration uint64,
	fence *openCodeSummarizeBoundaryFence,
) {
	defer close(fence.workerDone)
	ctx, cancel := context.WithTimeout(ctx, openCodeSummarizeRequestTimeout)
	defer cancel()
	err := d.summarizeOpenCodeSession(ctx, sessionID)
	if err != nil {
		d.retireOpenCodeSummarizeGeneration(
			sessionID,
			sessionGeneration,
			summarizeGeneration,
			fence,
			err,
		)
		return
	}

	d.mu.Lock()
	if d.sessionGeneration != sessionGeneration ||
		d.summarizeGeneration != summarizeGeneration ||
		strings.TrimSpace(d.sessionID) != strings.TrimSpace(sessionID) ||
		d.summarizeFence != fence ||
		d.generationRetiring {
		d.mu.Unlock()
		return
	}
	fence.httpSucceeded = true
	idleTimeout := d.summarizeIdleTimeout
	d.mu.Unlock()
	if idleTimeout <= 0 {
		idleTimeout = openCodeSummarizeIdleTimeout
	}
	timer := time.NewTimer(idleTimeout)
	defer timer.Stop()
	select {
	case <-fence.idleCh:
	case <-fence.retiredCh:
		return
	case <-timer.C:
		d.retireOpenCodeSummarizeGeneration(
			sessionID,
			sessionGeneration,
			summarizeGeneration,
			fence,
			fmt.Errorf(
				"complete compaction SSE boundary (compaction part, session.compacted, and Runner idle) was not observed within %s",
				idleTimeout,
			),
		)
		return
	}

	d.mu.Lock()
	if d.sessionGeneration != sessionGeneration ||
		d.summarizeGeneration != summarizeGeneration ||
		strings.TrimSpace(d.sessionID) != strings.TrimSpace(sessionID) ||
		d.summarizeFence != fence ||
		d.generationRetiring ||
		!fence.idleObserved ||
		!fence.compactedObserved ||
		!fence.compactionObserved {
		d.mu.Unlock()
		d.emitRuntimeDebug("opencode stale summarize completion ignored", map[string]any{
			"session_id":           sessionID,
			"session_generation":   sessionGeneration,
			"summarize_generation": summarizeGeneration,
		})
		return
	}
	d.summarizeFence = nil
	d.summarizeInProgress = false
	d.sessionStatus = "idle"
	d.turnActive = false
	d.resetOpenCodeRunnerMessageAdmissionLocked()
	d.lastSummarizeCompletedAt = time.Now()
	d.mu.Unlock()
	d.emitContextCompactionCompleted(sessionID)
	d.emitRuntimeIdle(sessionID)
}

func (d *OpenCodeDriver) retireOpenCodeSummarizeGeneration(
	sessionID string,
	sessionGeneration uint64,
	summarizeGeneration uint64,
	fence *openCodeSummarizeBoundaryFence,
	err error,
) {
	d.mu.Lock()
	if d.sessionGeneration != sessionGeneration ||
		d.summarizeGeneration != summarizeGeneration ||
		strings.TrimSpace(d.sessionID) != strings.TrimSpace(sessionID) ||
		d.summarizeFence != fence {
		d.mu.Unlock()
		d.emitRuntimeDebug("opencode stale summarize failure ignored", map[string]any{
			"session_id":           sessionID,
			"session_generation":   sessionGeneration,
			"summarize_generation": summarizeGeneration,
		})
		return
	}
	serverCancel := d.beginOpenCodeGenerationRetirementLocked()
	d.mu.Unlock()

	// Invalidate and stop the runtime before publishing a potentially blocking
	// diagnostic. A full translatedLines buffer must never prevent retirement.
	if serverCancel != nil {
		serverCancel()
	}
	if fence.proactive {
		slog.Warn("opencode proactive summarize failed; retiring runtime generation",
			"session_id", sessionID, "error", err)
	} else {
		slog.Warn("opencode summarize failed; retiring runtime generation",
			"session_id", sessionID, "error", err)
	}
	d.emitSummarizeError(sessionID, fence.proactive, err)
}

func (d *OpenCodeDriver) emitSummarizeError(sessionID string, proactive bool, err error) {
	if d.translatedLines == nil || err == nil {
		return
	}
	metadata := map[string]any{
		contextCompactionFailedMetadataKey: true,
	}
	if proactive {
		metadata[contextCompactionMaintenanceMetadataKey] = true
	}
	d.emitContextCompactionFailureBoundary(translatedEvent{
		Kind:      consts.ParsedEventKindError,
		SessionID: sessionID,
		Message:   fmt.Sprintf("opencode summarize: %v", err),
		Metadata:  metadata,
	})
}

// emitContextCompactionFailureBoundary publishes one terminal compaction pair.
// Overflow recovery consumes the failure marker; proactive maintenance also
// carries its purpose so Process does not restart a successfully completed
// user turn. The caller retires the server generation before publishing, so a
// late SSE idle cannot race or duplicate this boundary.
func (d *OpenCodeDriver) emitContextCompactionFailureBoundary(ev translatedEvent) {
	if d.translatedLines == nil {
		return
	}
	if ev.Metadata == nil {
		ev.Metadata = make(map[string]any, 1)
	}
	ev.Metadata[contextCompactionFailedMetadataKey] = true
	data, _ := json.Marshal(ev)
	d.translatedLines <- string(data)
	turnEndMetadata := map[string]any{
		contextCompactionFailedMetadataKey: true,
	}
	if parsedBoolMetadata(ev.Metadata, contextCompactionMaintenanceMetadataKey) {
		turnEndMetadata[contextCompactionMaintenanceMetadataKey] = true
	}
	d.emitTurnEndWithMetadata(ev.SessionID, turnEndMetadata)
}

func (d *OpenCodeDriver) summarizeOpenCodeSession(ctx context.Context, sessionID string) error {
	d.mu.Lock()
	if d.httpClient == nil {
		d.httpClient = &http.Client{Timeout: 120 * time.Second}
	}
	client := d.httpClient
	providerID := d.providerID
	model := d.launchModel
	d.mu.Unlock()

	trimmedSessionID := strings.TrimSpace(sessionID)
	if trimmedSessionID == "" {
		return fmt.Errorf("opencode summarize requires session id")
	}

	body := map[string]interface{}{}
	if trimmedModel := normalizeOpenCodeModelForProvider(model, providerID); trimmedModel != "" {
		if parsed := parseOpenCodeModel(trimmedModel, providerID); parsed != nil {
			body["providerID"] = parsed["providerID"]
			body["modelID"] = parsed["modelID"]
		}
	}
	bodyJSON, _ := json.Marshal(body)

	endpoint := fmt.Sprintf("/session/%s/summarize", trimmedSessionID)
	req, err := d.newRequest(ctx, http.MethodPost, endpoint, strings.NewReader(string(bodyJSON)))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	d.addDirectoryHeader(req, d.getWorkDir())

	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("summarize: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		return fmt.Errorf("summarize: status %d: %s", resp.StatusCode, string(respBody))
	}
	_, _ = io.Copy(io.Discard, resp.Body)
	return nil
}
