package controllers

import (
	"fmt"
	"strings"

	"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
	"github.com/jesseduffield/lazygit/pkg/commands/patch"
	"github.com/jesseduffield/lazygit/pkg/gocui"
	"github.com/jesseduffield/lazygit/pkg/gui/types"
)

type StagingController struct {
	baseController
	c *ControllerCommon

	context      types.IPatchExplorerContext
	otherContext types.IPatchExplorerContext

	// if true, we're dealing with the secondary context i.e. dealing with staged file changes
	staged bool
}

var _ types.IController = &StagingController{}

func NewStagingController(
	c *ControllerCommon,
	context types.IPatchExplorerContext,
	otherContext types.IPatchExplorerContext,
	staged bool,
) *StagingController {
	return &StagingController{
		baseController: baseController{},
		c:              c,
		context:        context,
		otherContext:   otherContext,
		staged:         staged,
	}
}

func (self *StagingController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
	return []*types.Binding{
		{
			Keys:            opts.GetKeys(opts.Config.Universal.Select),
			Handler:         self.ToggleStaged,
			Description:     self.c.Tr.Stage,
			Tooltip:         self.c.Tr.StageSelectionTooltip,
			DisplayOnScreen: true,
		},
		{
			Keys:            opts.GetKeys(opts.Config.Universal.Remove),
			Handler:         self.DiscardSelection,
			Description:     self.c.Tr.DiscardSelection,
			Tooltip:         self.c.Tr.DiscardSelectionTooltip,
			DisplayOnScreen: true,
		},
		{
			Keys:        opts.GetKeys(opts.Config.Universal.OpenFile),
			Handler:     self.OpenFile,
			Description: self.c.Tr.OpenFile,
			Tooltip:     self.c.Tr.OpenFileTooltip,
		},
		{
			Keys:        opts.GetKeys(opts.Config.Universal.Edit),
			Handler:     self.EditFile,
			Description: self.c.Tr.EditFile,
			Tooltip:     self.c.Tr.EditFileTooltip,
		},
		{
			Keys:            opts.GetKeys(opts.Config.Universal.Return),
			Handler:         self.Escape,
			Description:     self.c.Tr.ReturnToFilesPanel,
			DescriptionFunc: self.EscapeDescription,
			DisplayOnScreen: true,
		},
		{
			Keys:            opts.GetKeys(opts.Config.Universal.TogglePanel),
			Handler:         self.TogglePanel,
			Description:     self.c.Tr.ToggleStagingView,
			Tooltip:         self.c.Tr.ToggleStagingViewTooltip,
			DisplayOnScreen: true,
		},
		{
			Keys:        opts.GetKeys(opts.Config.Main.EditSelectHunk),
			Handler:     self.EditHunkAndRefresh,
			Description: self.c.Tr.EditHunk,
			Tooltip:     self.c.Tr.EditHunkTooltip,
		},
		{
			Keys:        opts.GetKeys(opts.Config.Files.CommitChanges),
			Handler:     self.c.Helpers().WorkingTree.HandleCommitPress,
			Description: self.c.Tr.Commit,
			Tooltip:     self.c.Tr.CommitTooltip,
		},
		{
			Keys:        opts.GetKeys(opts.Config.Files.CommitChangesWithoutHook),
			Handler:     self.c.Helpers().WorkingTree.HandleWIPCommitPress,
			Description: self.c.Tr.CommitChangesWithoutHook,
		},
		{
			Keys:        opts.GetKeys(opts.Config.Files.CommitChangesWithEditor),
			Handler:     self.c.Helpers().WorkingTree.HandleCommitEditorPress,
			Description: self.c.Tr.CommitChangesWithEditor,
		},
		{
			Keys:        opts.GetKeys(opts.Config.Files.FindBaseCommitForFixup),
			Handler:     self.c.Helpers().FixupHelper.HandleFindBaseCommitForFixupPress,
			Description: self.c.Tr.FindBaseCommitForFixup,
			Tooltip:     self.c.Tr.FindBaseCommitForFixupTooltip,
		},
	}
}

func (self *StagingController) Context() types.Context {
	return self.context
}

func (self *StagingController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
	return []*gocui.ViewMouseBinding{}
}

func (self *StagingController) GetOnFocus() func(types.OnFocusOpts) {
	return func(opts types.OnFocusOpts) {
		wrap := self.c.UserConfig().Gui.WrapLinesInStagingView
		self.c.Views().Staging.Wrap = wrap
		self.c.Views().StagingSecondary.Wrap = wrap

		self.c.Helpers().Staging.RefreshStagingPanel(opts)
	}
}

func (self *StagingController) GetOnFocusLost() func(types.OnFocusLostOpts) {
	return func(opts types.OnFocusLostOpts) {
		self.context.SetState(nil)

		if opts.NewContextKey != self.otherContext.GetKey() {
			self.c.Views().Staging.Wrap = true
			self.c.Views().StagingSecondary.Wrap = true
		}
	}
}

func (self *StagingController) OpenFile() error {
	self.context.GetMutex().Lock()
	defer self.context.GetMutex().Unlock()

	path := self.FilePath()

	if path == "" {
		return nil
	}

	return self.c.Helpers().Files.OpenFile(path)
}

func (self *StagingController) EditFile() error {
	self.context.GetMutex().Lock()
	defer self.context.GetMutex().Unlock()

	path := self.FilePath()

	if path == "" {
		return nil
	}

	lineNumber := self.context.GetState().CurrentLineNumber()
	lineNumber = self.c.Helpers().Diff.AdjustLineNumber(path, lineNumber, self.context.GetViewName())
	return self.c.Helpers().Files.EditFileAtLine(path, lineNumber)
}

func (self *StagingController) Escape() error {
	if self.context.GetState().SelectingRange() || self.context.GetState().SelectingHunkEnabledByUser() {
		self.context.GetState().SetLineSelectMode()
		self.c.PostRefreshUpdate(self.context)
		return nil
	}

	self.c.Context().Pop()
	return nil
}

func (self *StagingController) EscapeDescription() string {
	if state := self.context.GetState(); state != nil {
		if state.SelectingRange() {
			return self.c.Tr.DismissRangeSelect
		}

		if state.SelectingHunkEnabledByUser() {
			return self.c.Tr.SelectLineByLine
		}
	}

	return self.c.Tr.ReturnToFilesPanel
}

func (self *StagingController) TogglePanel() error {
	if self.otherContext.GetState() != nil {
		self.c.Context().Push(self.otherContext, types.OnFocusOpts{})
	}

	return nil
}

func (self *StagingController) ToggleStaged() error {
	if self.c.UserConfig().Git.DiffContextSize == 0 {
		return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
			self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
	}

	return self.applySelectionAndRefresh(self.staged)
}

func (self *StagingController) DiscardSelection() error {
	if self.c.UserConfig().Git.DiffContextSize == 0 {
		return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToDiscard,
			self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
	}

	return self.c.ConfirmIf(!self.staged && !self.c.UserConfig().Gui.SkipDiscardChangeWarning,
		types.ConfirmOpts{
			Title:         self.c.Tr.DiscardChangeTitle,
			Prompt:        self.c.Tr.DiscardChangePrompt,
			HandleConfirm: func() error { return self.applySelectionAndRefresh(true) },
		})
}

func (self *StagingController) applySelectionAndRefresh(reverse bool) error {
	if err := self.applySelection(reverse); err != nil {
		return err
	}

	// Block input until the refresh has landed: it rebuilds the staging panel
	// and moves the selection to the next stageable change, and a quick second
	// keypress must act on that, not on the stale pre-refresh diff.
	self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.STAGING}})
	return nil
}

func (self *StagingController) applySelection(reverse bool) error {
	self.context.GetMutex().Lock()
	defer self.context.GetMutex().Unlock()

	state := self.context.GetState()
	path := self.FilePath()
	if path == "" {
		return nil
	}

	firstLineIdx, lastLineIdx := state.SelectedPatchRange()
	patchToApply := patch.
		Parse(state.GetDiff()).
		Transform(patch.TransformOpts{
			Reverse:             reverse,
			IncludedLineIndices: patch.ExpandRange(firstLineIdx, lastLineIdx),
			FileNameOverride:    path,
		}).
		FormatPlain()

	if patchToApply == "" {
		return nil
	}

	// apply the patch then refresh this panel
	// create a new temp file with the patch, then call git apply with that patch
	self.c.LogAction(self.c.Tr.Actions.ApplyPatch)
	err := self.c.Git().Patch.ApplyPatch(
		patchToApply,
		git_commands.ApplyPatchOpts{
			Reverse: reverse,
			Cached:  !reverse || self.staged,
		},
	)
	if err != nil {
		return err
	}

	if state.SelectingRange() {
		firstLine, _ := state.SelectedViewRange()
		state.SelectLine(firstLine)
	}

	return nil
}

func (self *StagingController) EditHunkAndRefresh() error {
	if err := self.editHunk(); err != nil {
		return err
	}

	// Block input like applySelectionAndRefresh does; the refresh rebuilds the
	// staging panel from the post-edit diff.
	self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.STAGING}})
	return nil
}

func (self *StagingController) editHunk() error {
	self.context.GetMutex().Lock()
	defer self.context.GetMutex().Unlock()

	state := self.context.GetState()
	path := self.FilePath()
	if path == "" {
		return nil
	}

	hunkStartIdx, hunkEndIdx := state.CurrentHunkBounds()
	patchText := patch.
		Parse(state.GetDiff()).
		Transform(patch.TransformOpts{
			Reverse:             self.staged,
			IncludedLineIndices: patch.ExpandRange(hunkStartIdx, hunkEndIdx),
			FileNameOverride:    path,
		}).
		FormatPlain()

	patchFilepath, err := self.c.Git().Patch.SaveTemporaryPatch(patchText)
	if err != nil {
		return err
	}

	lineOffset := 3
	lineIdxInHunk := state.GetSelectedPatchLineIdx() - hunkStartIdx
	if err := self.c.Helpers().Files.EditFileAtLineAndWait(patchFilepath, lineIdxInHunk+lineOffset); err != nil {
		return err
	}

	editedPatchText, err := self.c.Git().File.Cat(patchFilepath)
	if err != nil {
		return err
	}

	self.c.LogAction(self.c.Tr.Actions.ApplyPatch)

	lineCount := strings.Count(editedPatchText, "\n") + 1
	newPatchText := patch.
		Parse(editedPatchText).
		Transform(patch.TransformOpts{
			IncludedLineIndices: patch.ExpandRange(0, lineCount),
			FileNameOverride:    path,
		}).
		FormatPlain()

	if err := self.c.Git().Patch.ApplyPatch(
		newPatchText,
		git_commands.ApplyPatchOpts{
			Reverse: self.staged,
			Cached:  true,
		},
	); err != nil {
		return err
	}

	return nil
}

func (self *StagingController) FilePath() string {
	return self.c.Contexts().Files.GetSelectedPath()
}
