package app

import (
	"context"
	"fmt"
	"time"

	"gokin/internal/agent"
	"gokin/internal/config"
	"gokin/internal/logging"
	"gokin/internal/permission"
	"gokin/internal/plan"
	"gokin/internal/ui"
)

// PermissionTimeout is the maximum time to wait for a permission response.
const PermissionTimeout = 5 * time.Minute

// promptPermission is called by the permission manager to ask the user for permission.
// It sends a request to the TUI and waits for a response with timeout.
func (a *App) promptPermission(ctx context.Context, req *permission.Request) (permission.Decision, error) {
	if a.program == nil {
		return permission.DecisionAllow, nil
	}

	// Send permission request to TUI
	a.safeSendToProgram(ui.PermissionRequestMsg{
		ToolName:  req.ToolName,
		Args:      req.Args,
		RiskLevel: req.RiskLevel.String(),
		Reason:    req.Reason,
	})

	// Warning timer - remind user after 30 seconds, then every 60 seconds
	const warningDelay = 30 * time.Second
	const repeatDelay = 60 * time.Second
	warningTimer := time.NewTimer(warningDelay)
	defer warningTimer.Stop()

	permTimer := time.NewTimer(PermissionTimeout)
	defer permTimer.Stop()

	// Wait for response from TUI with timeout and periodic warnings
	for {
		select {
		case decision := <-a.permResponseChan:
			return decision, nil
		case <-ctx.Done():
			return permission.DecisionDeny, ctx.Err()
		case <-warningTimer.C:
			// Send warning to UI
			a.safeSendToProgram(ui.StatusUpdateMsg{
				Type:    ui.StatusStreamIdle,
				Message: fmt.Sprintf("Waiting for permission: %s...", req.ToolName),
			})
			// Reset timer for next reminder
			warningTimer.Reset(repeatDelay)
		case <-permTimer.C:
			logging.Warn("permission prompt timed out", "tool", req.ToolName)
			return permission.DecisionDeny, fmt.Errorf("permission prompt timed out after %v", PermissionTimeout)
		}
	}
}

// handlePermissionDecision is called by the TUI when the user makes a permission decision.
func (a *App) handlePermissionDecision(decision ui.PermissionDecision) {
	// Convert UI decision to permission.Decision
	var permDecision permission.Decision
	switch decision {
	case ui.PermissionAllow:
		permDecision = permission.DecisionAllow
	case ui.PermissionAllowSession:
		permDecision = permission.DecisionAllowSession
	case ui.PermissionDeny:
		permDecision = permission.DecisionDeny
	case ui.PermissionDenySession:
		permDecision = permission.DecisionDenySession
	default:
		permDecision = permission.DecisionDeny
	}

	// Send decision to the waiting promptPermission call with timeout
	timer := time.NewTimer(30 * time.Second)
	defer timer.Stop()

	select {
	case a.permResponseChan <- permDecision:
	case <-timer.C:
		logging.Warn("permission response channel timeout - no listener")
	}
}

// QuestionTimeout is the maximum time to wait for a question response.
const QuestionTimeout = 5 * time.Minute

// promptQuestion is called by the AskUserTool to ask the user a question.
func (a *App) promptQuestion(ctx context.Context, question string, options []string, defaultOpt string) (string, error) {
	if a.program == nil {
		return "", fmt.Errorf("program not running")
	}

	// Send question request to TUI
	a.safeSendToProgram(ui.QuestionRequestMsg{
		Question: question,
		Options:  options,
		Default:  defaultOpt,
	})

	// Wait for response from TUI with timeout to prevent deadlock
	questionTimer := time.NewTimer(QuestionTimeout)
	defer questionTimer.Stop()

	select {
	case answer := <-a.questionResponseChan:
		return answer, nil
	case <-ctx.Done():
		return "", ctx.Err()
	case <-questionTimer.C:
		logging.Warn("question prompt timed out")
		return "", fmt.Errorf("question prompt timed out after %v", QuestionTimeout)
	}
}

// handleQuestionAnswer is called by the TUI when the user answers a question.
func (a *App) handleQuestionAnswer(answer string) {
	// Send answer to the waiting promptQuestion call with timeout
	timer := time.NewTimer(30 * time.Second)
	defer timer.Stop()

	select {
	case a.questionResponseChan <- answer:
	case <-timer.C:
		logging.Warn("question response channel timeout - no listener")
	}
}

// PlanApprovalTimeout is the maximum time to wait for a plan approval response.
const PlanApprovalTimeout = 10 * time.Minute

// promptPlanApproval is called by the plan manager to request user approval.
func (a *App) promptPlanApproval(ctx context.Context, p *plan.Plan) (plan.ApprovalDecision, error) {
	if a.program == nil {
		return plan.ApprovalApproved, nil
	}

	// Convert plan steps to UI format
	steps := make([]ui.PlanStepInfo, len(p.Steps))
	for i, step := range p.Steps {
		steps[i] = ui.PlanStepInfo{
			ID:          step.ID,
			Title:       step.Title,
			Description: step.Description,
		}
	}

	// Build plan approval request
	msg := ui.PlanApprovalRequestMsg{
		Title:       p.Title,
		Description: p.Description,
		Steps:       steps,
	}

	// Send plan approval request to TUI
	a.safeSendToProgram(msg)

	// Wait for response from TUI with timeout to prevent deadlock
	planTimer := time.NewTimer(PlanApprovalTimeout)
	defer planTimer.Stop()

	select {
	case decision := <-a.planApprovalChan:
		return decision, nil
	case <-ctx.Done():
		return plan.ApprovalRejected, ctx.Err()
	case <-planTimer.C:
		logging.Warn("plan approval prompt timed out")
		return plan.ApprovalRejected, fmt.Errorf("plan approval prompt timed out after %v", PlanApprovalTimeout)
	}
}

// handlePlanApproval is called by the TUI when the user makes a plan approval decision.
func (a *App) handlePlanApproval(decision ui.PlanApprovalDecision) {
	a.handlePlanApprovalWithFeedback(decision, "")
}

// handlePlanApprovalWithFeedback is called by the TUI when the user makes a plan approval decision with feedback.
func (a *App) handlePlanApprovalWithFeedback(decision ui.PlanApprovalDecision, feedback string) {
	// Convert UI decision to plan.ApprovalDecision
	var planDecision plan.ApprovalDecision
	switch decision {
	case ui.PlanApproved:
		planDecision = plan.ApprovalApproved
		// Claude Code semantics: approving a plan hands control back to the
		// agent for execution — which means lifting the read-only
		// restriction. If we stayed in plan mode the model would still only
		// see read tools and couldn't carry out the plan it just proposed.
		// User can re-enter plan mode via Shift+Tab or /plan for the next
		// task.
		a.disablePlanModeAfterApproval()
	case ui.PlanRejected:
		planDecision = plan.ApprovalRejected
		// Save the rejected plan for context
		if currentPlan := a.planManager.GetCurrentPlan(); currentPlan != nil {
			a.planManager.SaveRejectedPlan(currentPlan)
		}
	case ui.PlanModifyRequested:
		planDecision = plan.ApprovalModified
		// Save the rejected plan for context
		if currentPlan := a.planManager.GetCurrentPlan(); currentPlan != nil {
			a.planManager.SaveRejectedPlan(currentPlan)
		}
		// Handle feedback - send as a new message to the model
		if feedback != "" {
			a.planManager.SetFeedback(feedback)
			feedbackMsg := fmt.Sprintf("Please modify the plan according to this feedback:\n\n%s", feedback)
			// Process feedback as a new message
			go a.handleSubmit(feedbackMsg)
		}
	default:
		planDecision = plan.ApprovalRejected
	}

	// Send decision to the waiting promptPlanApproval call with timeout
	timer := time.NewTimer(30 * time.Second)
	defer timer.Stop()

	select {
	case a.planApprovalChan <- planDecision:
	case <-timer.C:
		logging.Warn("plan approval response channel timeout - no listener")
	}
}

// handlePlanProgressUpdate is called when plan execution progress is made.
func (a *App) handlePlanProgressUpdate(progress *plan.ProgressUpdate) {
	if a.program == nil {
		return
	}

	// Send progress update to TUI
	a.safeSendToProgram(ui.PlanProgressMsg{
		PlanID:        progress.PlanID,
		CurrentStepID: progress.CurrentStepID,
		CurrentTitle:  progress.CurrentTitle,
		TotalSteps:    progress.TotalSteps,
		Completed:     progress.Completed,
		Progress:      progress.Progress,
		Status:        progress.Status,
		Reason:        progress.Reason,
	})
}

// handleModelSelect is called by the TUI when the user selects a model.
func (a *App) handleModelSelect(modelID string) {
	if a.config == nil {
		return
	}
	a.config.Model.Name = modelID
	a.config.Model.Provider = config.DetectProvider(modelID)
	// Clear any stale model.preset so the explicit selection isn't reverted by
	// MigrateConfig/NormalizeConfig on the ApplyConfig below.
	a.config.Model.Preset = ""

	// Apply MaxOutputTokens from preset for new provider
	if preset, ok := config.ModelPresets[a.config.Model.Provider]; ok {
		a.config.Model.MaxOutputTokens = preset.MaxOutputTokens
	}

	if err := a.ApplyConfig(a.config); err != nil {
		logging.Warn("failed to apply model selection", "error", err)
	}
}

// DiffDecisionTimeout is the maximum time to wait for a diff decision response.
const DiffDecisionTimeout = 5 * time.Minute

func drainDiffDecisionChan(ch <-chan ui.DiffDecision) {
	if ch == nil {
		return
	}
	for {
		select {
		case <-ch:
		default:
			return
		}
	}
}

func drainMultiDiffDecisionChan(ch <-chan map[string]ui.DiffDecision) {
	if ch == nil {
		return
	}
	for {
		select {
		case <-ch:
		default:
			return
		}
	}
}

// promptDiffDecision is called by tools to request user approval for file changes.
// It sends a request to the TUI and waits for a response with timeout.
func (a *App) promptDiffDecision(ctx context.Context, filePath, oldContent, newContent, toolName string, isNewFile bool) (ui.DiffDecision, error) {
	a.diffPromptMu.Lock()
	defer a.diffPromptMu.Unlock()

	a.mu.Lock()
	batch := a.diffBatchDecision
	a.mu.Unlock()
	if batch == ui.DiffApplyAll {
		return ui.DiffApply, nil
	}
	if batch == ui.DiffRejectAll {
		return ui.DiffReject, nil
	}

	if a.program == nil {
		return ui.DiffApply, nil
	}

	drainDiffDecisionChan(a.diffResponseChan)

	// Send diff preview request to TUI
	a.safeSendToProgram(ui.DiffPreviewRequestMsg{
		FilePath:   filePath,
		OldContent: oldContent,
		NewContent: newContent,
		ToolName:   toolName,
		IsNewFile:  isNewFile,
	})

	// Wait for response from TUI with timeout to prevent deadlock
	diffTimer := time.NewTimer(DiffDecisionTimeout)
	defer diffTimer.Stop()

	select {
	case decision := <-a.diffResponseChan:
		if decision == ui.DiffApplyAll {
			a.mu.Lock()
			a.diffBatchDecision = ui.DiffApplyAll
			a.mu.Unlock()
			return ui.DiffApply, nil
		}
		if decision == ui.DiffRejectAll {
			a.mu.Lock()
			a.diffBatchDecision = ui.DiffRejectAll
			a.mu.Unlock()
			return ui.DiffReject, nil
		}
		return decision, nil
	case <-ctx.Done():
		return ui.DiffReject, ctx.Err()
	case <-diffTimer.C:
		logging.Warn("diff decision prompt timed out", "file", filePath)
		return ui.DiffReject, fmt.Errorf("diff decision prompt timed out after %v", DiffDecisionTimeout)
	}
}

// handleDiffDecision is called by the TUI when the user makes a diff preview decision.
func (a *App) handleDiffDecision(decision ui.DiffDecision) {
	// Send decision to the waiting promptDiffDecision call with timeout
	timer := time.NewTimer(30 * time.Second)
	defer timer.Stop()

	select {
	case a.diffResponseChan <- decision:
	case <-timer.C:
		logging.Warn("diff response channel timeout - no listener")
	}
}

func (a *App) promptMultiDiffDecision(ctx context.Context, files []ui.DiffFile) (map[string]ui.DiffDecision, error) {
	a.diffPromptMu.Lock()
	defer a.diffPromptMu.Unlock()

	if len(files) == 0 {
		return map[string]ui.DiffDecision{}, nil
	}

	drainMultiDiffDecisionChan(a.multiDiffResponseChan)

	if a.program == nil {
		decisions := make(map[string]ui.DiffDecision, len(files))
		for _, file := range files {
			decisions[file.FilePath] = ui.DiffApply
		}
		return decisions, nil
	}

	a.safeSendToProgram(ui.MultiDiffPreviewRequestMsg{Files: files})

	diffTimer := time.NewTimer(DiffDecisionTimeout)
	defer diffTimer.Stop()

	select {
	case decisions := <-a.multiDiffResponseChan:
		return decisions, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	case <-diffTimer.C:
		logging.Warn("multi diff decision prompt timed out", "files", len(files))
		return nil, fmt.Errorf("multi diff decision prompt timed out after %v", DiffDecisionTimeout)
	}
}

func (a *App) handleMultiDiffDecision(decisions map[string]ui.DiffDecision) {
	timer := time.NewTimer(30 * time.Second)
	defer timer.Stop()

	select {
	case a.multiDiffResponseChan <- decisions:
	case <-timer.C:
		logging.Warn("multi diff response channel timeout - no listener")
	}
}

func (a *App) reviewWorkspaceChanges(ctx context.Context, changes []agent.WorkspaceChangePreview) (bool, error) {
	if len(changes) == 0 {
		return true, nil
	}

	if len(changes) > 1 {
		files := make([]ui.DiffFile, 0, len(changes))
		for _, change := range changes {
			files = append(files, ui.DiffFile{
				FilePath:   change.FilePath,
				OldContent: change.OldContent,
				NewContent: change.NewContent,
				IsNewFile:  change.IsNewFile,
			})
		}

		decisions, err := a.promptMultiDiffDecision(ctx, files)
		if err != nil {
			return false, err
		}
		for _, decision := range decisions {
			if decision != ui.DiffApply {
				return false, nil
			}
		}
		return true, nil
	}

	a.mu.Lock()
	prevBatch := a.diffBatchDecision
	a.diffBatchDecision = ui.DiffPending
	a.mu.Unlock()
	defer func() {
		a.mu.Lock()
		a.diffBatchDecision = prevBatch
		a.mu.Unlock()
	}()

	for _, change := range changes {
		decision, err := a.promptDiffDecision(
			ctx,
			change.FilePath,
			change.OldContent,
			change.NewContent,
			"apply_back",
			change.IsNewFile,
		)
		if err != nil {
			return false, err
		}
		if decision == ui.DiffReject || decision == ui.DiffRejectAll {
			return false, nil
		}
	}

	return true, nil
}

// handleApplyCodeBlock handles applying a code block to a file.
func (a *App) handleApplyCodeBlock(filename, content string) {
	if filename == "" || content == "" {
		return
	}

	// Use the write tool to apply the code block
	writeTool, ok := a.registry.Get("write")
	if !ok {
		logging.Debug("write tool not found")
		return
	}

	// Execute the write tool
	go func() {
		defer func() {
			if r := recover(); r != nil {
				logging.Warn("write tool goroutine recovered from panic", "error", r)
			}
		}()
		ctx := a.ctx
		args := map[string]any{
			"file_path": filename,
			"content":   content,
		}

		result, err := writeTool.Execute(ctx, args)
		if err != nil {
			a.safeSendToProgram(ui.ErrorMsg(err))
			return
		}

		if !result.Success {
			a.safeSendToProgram(ui.ErrorMsg(fmt.Errorf("%s", result.Error)))
		}
	}()
}

// diffHandlerAdapter wraps App to implement tools.DiffHandler interface.
type diffHandlerAdapter struct {
	app *App
}

func (d *diffHandlerAdapter) PromptDiff(ctx context.Context, filePath, oldContent, newContent, toolName string, isNewFile bool) (bool, error) {
	decision, err := d.app.promptDiffDecision(ctx, filePath, oldContent, newContent, toolName, isNewFile)
	if err != nil {
		return false, err
	}
	return decision == ui.DiffApply, nil
}
