package cli

import (
	"context"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"
	"unicode"

	"go.uber.org/zap"

	"iop/apps/node/internal/adapters/cli/status"
	"iop/apps/node/internal/runtime"
	"iop/packages/go/config"
)

const (
	terminalInputDelay = 2 * time.Millisecond
	terminalRows       = 720
	terminalCols       = 1024
)

func emitRuntimeError(ctx context.Context, sink runtime.EventSink, runID, msg string) error {
	_ = sink.Emit(ctx, runtime.RuntimeEvent{
		RunID:     runID,
		Type:      runtime.EventTypeError,
		Error:     msg,
		Timestamp: time.Now(),
	})
	return fmt.Errorf("cli adapter: %s", msg)
}

type completionMatcher struct {
	line string
	re   *regexp.Regexp
}

func newCompletionMatcher(m config.CompletionMarkerConf) (completionMatcher, error) {
	var cm completionMatcher
	cm.line = m.Line
	if m.Regex != "" {
		re, err := regexp.Compile(m.Regex)
		if err != nil {
			return completionMatcher{}, fmt.Errorf("completion_marker regex: %w", err)
		}
		cm.re = re
	}
	return cm, nil
}

func (m completionMatcher) match(line string) bool {
	if m.line != "" && line == m.line {
		return true
	}
	if m.re != nil && m.re.MatchString(line) {
		return true
	}
	return false
}

type persistentRun struct {
	executor                *persistentExecutor
	ctx                     context.Context
	spec                    runtime.ExecutionSpec
	profile                 config.CLIProfileConf
	sink                    runtime.EventSink
	sess                    *profileSession
	matcher                 completionMatcher
	prompt                  string
	idleTimeout             time.Duration
	outputFilter            persistentOutputFilter
	waitForFilteredMessage  bool
	baselineAssistant       string
	outputTokens            int
	markerBuf               strings.Builder
	claudePromptReplayCount int
	idleTimer               *time.Timer
	idleC                   <-chan time.Time
}

func (r *persistentRun) setup() error {
	r.prompt = extractPrompt(r.spec.Input)
	r.idleTimeout = time.Duration(r.profile.ResponseIdleTimeoutMS) * time.Millisecond
	if r.idleTimeout <= 0 {
		r.idleTimeout = 1500 * time.Millisecond
	}
	targetName := cliTargetName(r.spec)
	r.waitForFilteredMessage = r.profile.Terminal && isClaudeTerminalProfile(targetName, r.profile)
	if r.waitForFilteredMessage {
		r.baselineAssistant, _ = latestClaudeAssistantMessageFromCleanOutput(cleanClaudeTerminalOutput(r.sess.getTail()))
	}
	r.outputFilter = newPersistentOutputFilter(targetName, r.profile, r.prompt, r.baselineAssistant)

	_ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{
		RunID:     r.spec.RunID,
		Type:      runtime.EventTypeStart,
		Timestamp: time.Now(),
	})

	if r.sess.core != nil {
		if err := r.sess.core.WritePrompt(r.ctx, r.prompt); err != nil {
			return fmt.Errorf("write prompt: %v", err)
		}
	} else {
		if err := writePrompt(r.ctx, r.sess.input, r.prompt, r.profile); err != nil {
			return fmt.Errorf("write prompt: %v", err)
		}
	}
	return nil
}

func (r *persistentRun) handleCancel() error {
	drainSessionUntilIdle(r.sess.output, r.idleTimeout, r.executor.cli.logger, r.sess.key)
	_ = r.sink.Emit(context.Background(), runtime.RuntimeEvent{
		RunID:     r.spec.RunID,
		Type:      runtime.EventTypeCancelled,
		Message:   cancelEventForContext(r.ctx.Err()),
		Timestamp: time.Now(),
	})
	return runtime.ErrRunCancelled
}

func (r *persistentRun) handleOutput(out cliOutput) (bool, error) {
	if r.waitForFilteredMessage {
		if msg, cancelled := claudeTerminalCancelMessage(r.sess.getTail()); cancelled {
			r.executor.removePersistentSession(r.sess)
			_ = closeProfileSession(context.Background(), r.sess)
			_ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{
				RunID:     r.spec.RunID,
				Type:      runtime.EventTypeCancelled,
				Message:   msg,
				Timestamp: time.Now(),
			})
			return true, fmt.Errorf("%w: %s", runtime.ErrRunCancelled, msg)
		}
	}
	if delta := r.outputFilter.Filter(out.text); delta != "" {
		r.outputTokens += len(strings.Fields(delta))
		_ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{
			RunID:     r.spec.RunID,
			Type:      runtime.EventTypeDelta,
			Delta:     delta,
			Timestamp: time.Now(),
		})
	}
	var markerLines []string
	if out.markerLine != "" {
		markerLines = append(markerLines, out.markerLine)
	} else {
		markerLines = consumeCompleteLines(&r.markerBuf, out.text)
	}
	if r.matcher.matchAny(markerLines) {
		if delta := r.outputFilter.Flush(); delta != "" {
			r.outputTokens += len(strings.Fields(delta))
			_ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{
				RunID:     r.spec.RunID,
				Type:      runtime.EventTypeDelta,
				Delta:     delta,
				Timestamp: time.Now(),
			})
		}
		err := r.sink.Emit(r.ctx, runtime.RuntimeEvent{
			RunID:   r.spec.RunID,
			Type:    runtime.EventTypeComplete,
			Message: "completion-marker",
			Usage: &runtime.UsageStats{
				InputTokens:  len(strings.Fields(r.prompt)),
				OutputTokens: r.outputTokens,
			},
			Timestamp: time.Now(),
		})
		return true, err
	}
	if r.waitForFilteredMessage || r.outputFilter.HasOutput() || !r.profile.Terminal {
		if r.idleTimer == nil {
			r.idleTimer = time.NewTimer(r.idleTimeout)
			r.idleC = r.idleTimer.C
		} else {
			if !r.idleTimer.Stop() {
				select {
				case <-r.idleTimer.C:
				default:
				}
			}
			r.idleTimer.Reset(r.idleTimeout)
		}
	}
	return false, nil
}

func (r *persistentRun) handleIdleTimeout() (bool, error) {
	if delta := r.outputFilter.Flush(); delta != "" {
		r.outputTokens += len(strings.Fields(delta))
		_ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{
			RunID:     r.spec.RunID,
			Type:      runtime.EventTypeDelta,
			Delta:     delta,
			Timestamp: time.Now(),
		})
	} else if r.waitForFilteredMessage && !r.outputFilter.HasOutput() {
		if r.claudePromptReplayCount == 0 && claudeTerminalReadyForInput(r.sess.getTail()) {
			r.claudePromptReplayCount++
			r.executor.cli.logger.Info("cli adapter: replaying claude prompt after ready screen",
				zap.String("target", cliTargetName(r.spec)),
				zap.String("session", r.sess.key.sessionID))
			var err error
			if r.sess.core != nil {
				err = r.sess.core.WritePrompt(r.ctx, r.prompt)
			} else {
				err = writePrompt(r.ctx, r.sess.input, r.prompt, r.profile)
			}
			if err != nil {
				return true, emitRuntimeError(r.ctx, r.sink, r.spec.RunID, fmt.Sprintf("replay prompt: %v", err))
			}
		}
		r.idleTimer.Reset(r.idleTimeout)
		return false, nil
	}
	if r.waitForFilteredMessage && r.outputFilter.HasOutput() && r.outputFilter.CompletionMessage() == "" {
		r.idleTimer.Reset(r.idleTimeout)
		return false, nil
	}
	completeMessage := "idle-timeout"
	if msg := r.outputFilter.CompletionMessage(); msg != "" {
		completeMessage = msg
	}
	err := r.sink.Emit(r.ctx, runtime.RuntimeEvent{
		RunID:   r.spec.RunID,
		Type:    runtime.EventTypeComplete,
		Message: completeMessage,
		Usage: &runtime.UsageStats{
			InputTokens:  len(strings.Fields(r.prompt)),
			OutputTokens: r.outputTokens,
		},
		Timestamp: time.Now(),
	})
	return true, err
}

func (e *persistentExecutor) Execute(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf, sink runtime.EventSink) error {
	sess, err := e.resolveSession(ctx, spec, profile)
	if err != nil {
		return emitReturnedError(ctx, sink, spec.RunID, err)
	}

	matcher, err := newCompletionMatcher(profile.CompletionMarker)
	if err != nil {
		return emitRuntimeError(ctx, sink, spec.RunID, err.Error())
	}

	sess.mu.Lock()
	defer sess.mu.Unlock()

	run := &persistentRun{
		executor: e,
		ctx:      ctx,
		spec:     spec,
		profile:  profile,
		sink:     sink,
		sess:     sess,
		matcher:  matcher,
	}

	if err := run.setup(); err != nil {
		// Writing the prompt aborts with the context error when the run is
		// cancelled or times out mid-setup. Emit a single cancelled event and
		// return ErrRunCancelled so cancellation is not reported as a setup error.
		if ctx.Err() != nil {
			_ = sink.Emit(context.Background(), runtime.RuntimeEvent{
				RunID:     spec.RunID,
				Type:      runtime.EventTypeCancelled,
				Message:   cancelEventForContext(ctx.Err()),
				Timestamp: time.Now(),
			})
			return runtime.ErrRunCancelled
		}
		return emitRuntimeError(ctx, sink, spec.RunID, err.Error())
	}

	defer func() {
		if run.idleTimer != nil {
			run.idleTimer.Stop()
		}
	}()

	for {
		select {
		case <-ctx.Done():
			return run.handleCancel()
		case out, ok := <-sess.output:
			if !ok {
				err := drainPersistentDone(sess)
				return e.emitPersistentExit(ctx, sink, spec.RunID, cliTargetName(spec), profile, sess, err)
			}
			done, err := run.handleOutput(out)
			if done {
				return err
			}
		case <-run.idleC:
			done, err := run.handleIdleTimeout()
			if done {
				return err
			}
		case err := <-sess.done:
			return e.emitPersistentExit(ctx, sink, spec.RunID, cliTargetName(spec), profile, sess, err)
		}
	}
}

func (m completionMatcher) matchAny(lines []string) bool {
	for _, line := range lines {
		if m.match(line) {
			return true
		}
	}
	return false
}

func consumeCompleteLines(buf *strings.Builder, text string) []string {
	if text == "" {
		return nil
	}
	buf.WriteString(text)
	raw := buf.String()
	start := 0
	var lines []string
	for i, r := range raw {
		if r != '\n' {
			continue
		}
		lines = append(lines, strings.TrimRight(raw[start:i], "\r"))
		start = i + 1
	}
	if start > 0 {
		buf.Reset()
		buf.WriteString(raw[start:])
	} else if len(raw) > 8192 {
		buf.Reset()
		buf.WriteString(raw[len(raw)-8192:])
	}
	return lines
}

func promptTerminator(profile config.CLIProfileConf) string {
	if profile.Terminal {
		return "\r"
	}
	return "\n"
}

func writePrompt(ctx context.Context, input io.Writer, prompt string, profile config.CLIProfileConf) error {
	if !profile.Terminal {
		_, err := io.WriteString(input, prompt+promptTerminator(profile))
		return err
	}
	for _, r := range prompt {
		if _, err := io.WriteString(input, string(r)); err != nil {
			return err
		}
		timer := time.NewTimer(terminalInputDelay)
		select {
		case <-ctx.Done():
			timer.Stop()
			return ctx.Err()
		case <-timer.C:
		}
	}
	_, err := io.WriteString(input, promptTerminator(profile))
	return err
}

// resolveSession returns an existing session for the given key or creates one
// when SessionMode allows it.
func (e *persistentExecutor) resolveSession(ctx context.Context, spec runtime.ExecutionSpec, profile config.CLIProfileConf) (*profileSession, error) {
	target := cliTargetName(spec)
	key := newSessionKey(spec)
	e.mu.Lock()
	defer e.mu.Unlock()

	if sess, ok := e.sessions[key]; ok {
		return sess, nil
	}
	if spec.SessionMode == runtime.SessionModeRequireExisting {
		return nil, fmt.Errorf("cli adapter: no persistent session for target %q session %q", target, key.sessionID)
	}
	sess, err := startProfileSession(ctx, key, profile, spec.Workspace, e.cli.logger)
	if err != nil {
		return nil, err
	}
	e.sessions[key] = sess
	return sess, nil
}

func appendBounded(buf *strings.Builder, s string, max int) {
	buf.WriteString(s)
	raw := buf.String()
	if len(raw) <= max {
		return
	}
	buf.Reset()
	buf.WriteString(raw[len(raw)-max:])
}

func shouldAcceptClaudeWorkspaceTrust(raw string) bool {
	compact := compactTerminalText(raw)
	return strings.Contains(compact, "quicksafetycheckisthisaproject") &&
		strings.Contains(compact, "yesitrustthisfolder")
}

func shouldAcceptClaudeBypassWarning(raw string) bool {
	compact := compactTerminalText(raw)
	return strings.Contains(compact, "claudecoderunninginbypasspermissionsmode") &&
		strings.Contains(compact, "yesiaccept")
}

func claudeTerminalCancelMessage(raw string) (string, bool) {
	compact := compactTerminalText(raw)
	switch {
	case strings.Contains(compact, "youvehityoursessionlimit"):
		if line := findClaudeTerminalLine(raw, "session limit"); line != "" {
			return line, true
		}
		return "Claude session limit reached", true
	case strings.Contains(compact, "stopandwaitforlimittoreset") && strings.Contains(compact, "upgradeyourplan"):
		return "Claude session limit reached; upgrade was not selected", true
	default:
		return "", false
	}
}

func claudeTerminalReadyForInput(raw string) bool {
	return claudeScreenHasInputPrompt(status.RenderVisibleScreen(raw, terminalRows, terminalCols))
}

func findClaudeTerminalLine(raw, needle string) string {
	needle = strings.ToLower(needle)
	for _, line := range strings.Split(cleanClaudeTerminalOutput(raw), "\n") {
		line = strings.Join(strings.Fields(line), " ")
		if strings.Contains(strings.ToLower(line), needle) {
			return line
		}
	}
	return ""
}

func compactTerminalText(s string) string {
	s = cleanClaudeTerminalOutput(s)
	var b strings.Builder
	inEscape := false
	for _, r := range s {
		if r == '\x1b' {
			inEscape = true
			continue
		}
		if inEscape {
			if r >= '@' && r <= '~' {
				inEscape = false
			}
			continue
		}
		if unicode.IsLetter(r) || unicode.IsDigit(r) {
			b.WriteRune(unicode.ToLower(r))
		}
	}
	return b.String()
}

func (e *persistentExecutor) emitPersistentExit(ctx context.Context, sink runtime.EventSink, runID, targetName string, profile config.CLIProfileConf, sess *profileSession, err error) error {
	e.removePersistentSession(sess)

	cmdSummary := fmt.Sprintf("%s %s", profile.Command, strings.Join(profile.Args, " "))
	tail := sess.getTail()

	var msg string
	if err != nil {
		msg = fmt.Sprintf("persistent session process exited: %s, target=%s, session=%s, command=%q", err.Error(), targetName, sess.key.sessionID, cmdSummary)
	} else {
		msg = fmt.Sprintf("persistent session process exited unexpectedly: target=%s, session=%s, command=%q", targetName, sess.key.sessionID, cmdSummary)
	}

	if tail != "" {
		msg = fmt.Sprintf("%s, recent output: %q", msg, tail)
	}
	return emitRuntimeError(ctx, sink, runID, msg)
}

func (e *persistentExecutor) removePersistentSession(sess *profileSession) {
	e.mu.Lock()
	if s, found := e.sessions[sess.key]; found && s == sess {
		delete(e.sessions, sess.key)
	}
	e.mu.Unlock()
}

func (e *persistentExecutor) stopAllSessions(_ context.Context) error {
	var firstErr error
	for key, sess := range e.sessions {
		if err := closeProfileSession(context.Background(), sess); err != nil && firstErr == nil {
			firstErr = fmt.Errorf("cli adapter: close session %q/%q: %w", key.target, key.sessionID, err)
		}
	}
	e.sessions = make(map[sessionKey]*profileSession)
	return firstErr
}

func (e *persistentExecutor) Sessions() []sessionListEntry {
	e.mu.Lock()
	defer e.mu.Unlock()
	snaps := make([]sessionListEntry, 0, len(e.sessions))
	for k := range e.sessions {
		snaps = append(snaps, sessionListEntry{"persistent", k.target, k.sessionID, k.workspace})
	}
	return snaps
}

// Terminate closes every workspace variant of the given target/sessionID. The
// terminate command carries no workspace, so it terminates all logical sessions
// that share the same target and session id regardless of bound workspace.
func (e *persistentExecutor) Terminate(ctx context.Context, target, sessionID string) (bool, error) {
	sid := normalizeSessionID(sessionID)
	e.mu.Lock()
	var matched []*profileSession
	for k, sess := range e.sessions {
		if k.target == target && k.sessionID == sid {
			matched = append(matched, sess)
			delete(e.sessions, k)
		}
	}
	e.mu.Unlock()
	if len(matched) == 0 {
		return false, nil
	}
	var firstErr error
	for _, sess := range matched {
		if err := closeProfileSession(ctx, sess); err != nil && firstErr == nil {
			firstErr = err
		}
	}
	return true, firstErr
}

func (e *persistentExecutor) Stop(ctx context.Context) error {
	e.mu.Lock()
	sessionsCopy := make(map[sessionKey]*profileSession, len(e.sessions))
	for key, sess := range e.sessions {
		sessionsCopy[key] = sess
	}
	e.sessions = make(map[sessionKey]*profileSession)
	e.mu.Unlock()

	var firstErr error
	for key, sess := range sessionsCopy {
		if err := closeProfileSession(context.Background(), sess); err != nil && firstErr == nil {
			firstErr = fmt.Errorf("cli adapter: close session %q/%q: %w", key.target, key.sessionID, err)
		}
	}
	return firstErr
}
