package agent

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

const openCodeInterruptIdleTimeout = 10 * time.Second

type openCodeInterruptIdleFence struct {
	SessionID  string
	done       chan struct{}
	idleSeen   bool
	doneClosed bool
}

func (d *OpenCodeDriver) beginInterruptIdleFence(sessionID string) (*openCodeInterruptIdleFence, *http.Client, error) {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.httpClient == nil {
		d.httpClient = &http.Client{Timeout: 120 * time.Second}
	}
	currentSessionID := strings.TrimSpace(d.sessionID)
	trimmedSessionID := strings.TrimSpace(sessionID)
	if trimmedSessionID == "" {
		trimmedSessionID = currentSessionID
	}
	if trimmedSessionID == "" {
		return nil, nil, fmt.Errorf("opencode abort requires session id")
	}
	if currentSessionID != "" && currentSessionID != trimmedSessionID {
		return nil, nil, fmt.Errorf(
			"opencode abort session mismatch: current %s, requested %s",
			currentSessionID,
			trimmedSessionID,
		)
	}
	if d.sseDeliveryBlocked {
		return nil, nil, fmt.Errorf(
			"%w: opencode cannot safely abort without a connected SSE boundary stream",
			ErrAgentBusy,
		)
	}
	if d.summarizeInProgress {
		return nil, nil, fmt.Errorf(
			"%w: opencode cannot safely abort while session summarize is in progress",
			ErrAgentBusy,
		)
	}
	if awaitingRunner := d.promptAwaitingRunner; awaitingRunner != nil {
		return nil, nil, fmt.Errorf(
			"%w: opencode cannot safely abort prompt %s before Runner admission is observed",
			ErrAgentBusy,
			strings.TrimSpace(awaitingRunner.MessageID),
		)
	}
	if pendingRuntimeInputCount := len(d.pendingRuntimeInputs); pendingRuntimeInputCount > 0 {
		// Once Runner has been observed but before an exact assistant parent is
		// emitted, abort can leave the persisted user message unselected. There
		// is no driver event that can safely terminalize the Process ownership
		// record in that window.
		return nil, nil, fmt.Errorf(
			"%w: opencode cannot safely abort while %d correlated runtime input(s) await assistant receipt",
			ErrAgentBusy,
			pendingRuntimeInputCount,
		)
	}
	if d.interruptIdleFence != nil {
		return nil, nil, fmt.Errorf("%w: opencode abort already awaits its idle boundary", ErrAgentBusy)
	}
	if !d.turnActive && strings.TrimSpace(d.sessionStatus) == "idle" {
		return nil, d.httpClient, nil
	}
	fence := &openCodeInterruptIdleFence{
		SessionID: trimmedSessionID,
		done:      make(chan struct{}),
	}
	d.interruptIdleFence = fence
	return fence, d.httpClient, nil
}

func (d *OpenCodeDriver) cancelInterruptIdleFence(fence *openCodeInterruptIdleFence) bool {
	if fence == nil {
		return false
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.interruptIdleFence == fence {
		d.interruptIdleFence = nil
		if fence.idleSeen {
			d.activateInterruptCleanupDrainLocked()
		}
		if !fence.doneClosed {
			fence.doneClosed = true
			close(fence.done)
		}
	}
	return fence.idleSeen
}

// completeInterruptIdleFence transfers the first observed idle into a
// post-interrupt cleanup drain only after the abort request itself completed.
// The drain remains across a live revert until a fresh busy/retry proves all
// older SSE cleanup frames precede the new run.
func (d *OpenCodeDriver) completeInterruptIdleFence(fence *openCodeInterruptIdleFence) bool {
	if fence == nil {
		return false
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.interruptIdleFence == fence {
		if !fence.idleSeen {
			return false
		}
		d.interruptIdleFence = nil
		d.activateInterruptCleanupDrainLocked()
	}
	return fence.idleSeen
}

func (d *OpenCodeDriver) activateInterruptCleanupDrainLocked() {
	d.interruptCleanupDrain = true
	d.sessionStatus = "idle"
	d.turnActive = false
	d.lastRetryStatusKey = ""
	d.pendingSessionError = nil
	d.pendingSessionErrorFinalIdle = false
	d.autoCompactionUserMessageID = ""
	d.autoCompactionAssistantMessageID = ""
	// Partial text belongs to the aborted/soon-to-be-reverted turn. Keeping it
	// would concatenate stale output with the next prompt's deltas.
	d.textBuf.Reset()
	d.resetOpenCodeRunnerMessageAdmissionLocked()
}

func (d *OpenCodeDriver) consumeInterruptIdleFence(sessionID string) bool {
	trimmedSessionID := strings.TrimSpace(sessionID)
	d.mu.Lock()
	if d.generationRetiring {
		d.mu.Unlock()
		return false
	}
	fence := d.interruptIdleFence
	if fence == nil || strings.TrimSpace(fence.SessionID) != trimmedSessionID {
		d.mu.Unlock()
		return false
	}
	d.sessionStatus = "idle"
	d.turnActive = false
	d.lastRetryStatusKey = ""
	d.pendingSessionError = nil
	d.pendingSessionErrorFinalIdle = false
	d.autoCompactionUserMessageID = ""
	d.autoCompactionAssistantMessageID = ""
	d.textBuf.Reset()
	d.resetOpenCodeRunnerMessageAdmissionLocked()
	if !fence.idleSeen {
		fence.idleSeen = true
		if !fence.doneClosed {
			fence.doneClosed = true
			close(fence.done)
		}
	}
	d.mu.Unlock()
	d.emitRuntimeDebug("opencode abort idle boundary consumed by interrupt fence", map[string]any{
		"session_id": trimmedSessionID,
	})
	return true
}

func (d *OpenCodeDriver) InterruptActiveRun(ctx context.Context, sessionID string) error {
	if ctx == nil {
		ctx = context.Background()
	}
	d.deliveryMu.Lock()
	defer d.deliveryMu.Unlock()
	fence, client, err := d.beginInterruptIdleFence(sessionID)
	if err != nil {
		return err
	}
	if fence == nil {
		return nil
	}
	trimmedSessionID := strings.TrimSpace(fence.SessionID)
	endpoint := fmt.Sprintf("/session/%s/abort", trimmedSessionID)
	req, err := d.newRequest(ctx, http.MethodPost, endpoint, nil)
	if err != nil {
		d.cancelInterruptIdleFence(fence)
		return err
	}
	d.addDirectoryHeader(req, d.getWorkDir())
	resp, err := client.Do(req)
	if err != nil {
		observedIdle := d.cancelInterruptIdleFence(fence)
		d.emitRuntimeDebug("opencode abort transport failed", map[string]any{
			"session_id": trimmedSessionID,
			"error":      err,
		})
		if observedIdle {
			// The authoritative idle won the race with the HTTP transport
			// result. Process treats a nil interrupt result as that boundary;
			// returning the transport error would lose the fence-consumed idle.
			return nil
		}
		return fmt.Errorf("opencode abort: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		observedIdle := d.cancelInterruptIdleFence(fence)
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		d.emitRuntimeDebug("opencode abort status failed", map[string]any{
			"session_id":          trimmedSessionID,
			"status":              resp.StatusCode,
			"response_body_bytes": len(respBody),
			"response_body":       "<omitted>",
		})
		if observedIdle {
			return nil
		}
		return fmt.Errorf("opencode abort: status %d: %s", resp.StatusCode, string(respBody))
	}
	_, _ = io.Copy(io.Discard, resp.Body)
	d.emitRuntimeDebug("opencode abort accepted", map[string]any{"session_id": trimmedSessionID})

	timer := time.NewTimer(openCodeInterruptIdleTimeout)
	defer timer.Stop()
	select {
	case <-fence.done:
		if !d.completeInterruptIdleFence(fence) {
			return fmt.Errorf("%w: opencode abort idle fence completed without an idle boundary", ErrAgentBusy)
		}
		return nil
	case <-ctx.Done():
		if d.cancelInterruptIdleFence(fence) {
			return nil
		}
		return fmt.Errorf("opencode abort awaiting idle boundary: %w", ctx.Err())
	case <-timer.C:
		if d.cancelInterruptIdleFence(fence) {
			return nil
		}
		return fmt.Errorf(
			"%w: opencode abort did not produce an observable idle boundary within %s",
			ErrAgentBusy,
			openCodeInterruptIdleTimeout,
		)
	}
}

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

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

	body := map[string]interface{}{
		"messageID": trimmedMessageID,
	}
	bodyJSON, _ := json.Marshal(body)

	endpoint := fmt.Sprintf("/session/%s/revert", 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 {
		d.emitRuntimeDebug("opencode revert transport failed", map[string]any{
			"session_id": trimmedSessionID,
			"message_id": trimmedMessageID,
			"error":      err,
		})
		return fmt.Errorf("opencode revert: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		d.emitRuntimeDebug("opencode revert status failed", map[string]any{
			"session_id":          trimmedSessionID,
			"message_id":          trimmedMessageID,
			"status":              resp.StatusCode,
			"response_body_bytes": len(respBody),
			"response_body":       "<omitted>",
		})
		return fmt.Errorf("opencode revert: status %d: %s", resp.StatusCode, string(respBody))
	}
	_, _ = io.Copy(io.Discard, resp.Body)
	d.emitRuntimeDebug("opencode revert accepted", map[string]any{
		"session_id": trimmedSessionID,
		"message_id": trimmedMessageID,
	})
	return nil
}
