package controllers

import (
	"fmt"

	"github.com/jesseduffield/lazygit/pkg/gocui"
	"github.com/jesseduffield/lazygit/pkg/gui/types"
	"github.com/samber/lo"
)

type PatchBuildingController struct {
	baseController
	c *ControllerCommon
}

var _ types.IController = &PatchBuildingController{}

func NewPatchBuildingController(
	c *ControllerCommon,
) *PatchBuildingController {
	return &PatchBuildingController{
		baseController: baseController{},
		c:              c,
	}
}

func (self *PatchBuildingController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
	return []*types.Binding{
		{
			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.Select),
			Handler:         self.ToggleSelectionAndRefresh,
			Description:     self.c.Tr.ToggleSelectionForPatch,
			DisplayOnScreen: true,
		},
		{
			Keys:              opts.GetKeys(opts.Config.Universal.Remove),
			Handler:           self.discardSelection,
			GetDisabledReason: self.getDisabledReasonForDiscard,
			Description:       self.c.Tr.RemoveSelectionFromPatch,
			Tooltip:           self.c.Tr.RemoveSelectionFromPatchTooltip,
			DisplayOnScreen:   true,
		},
		{
			Keys:            opts.GetKeys(opts.Config.Universal.Return),
			Handler:         self.Escape,
			Description:     self.c.Tr.ExitCustomPatchBuilder,
			DescriptionFunc: self.EscapeDescription,
			DisplayOnScreen: true,
		},
	}
}

func (self *PatchBuildingController) Context() types.Context {
	return self.c.Contexts().CustomPatchBuilder
}

func (self *PatchBuildingController) context() types.IPatchExplorerContext {
	return self.c.Contexts().CustomPatchBuilder
}

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

func (self *PatchBuildingController) GetOnFocus() func(types.OnFocusOpts) {
	return func(opts types.OnFocusOpts) {
		// no need to change wrap on the secondary view because it can't be interacted with
		self.c.Views().PatchBuilding.Wrap = self.c.UserConfig().Gui.WrapLinesInStagingView

		self.c.Helpers().PatchBuilding.RefreshPatchBuildingPanel(opts)
	}
}

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

		self.c.Views().PatchBuilding.Wrap = true

		if self.c.Git().Patch.PatchBuilder.IsEmpty() {
			self.c.Git().Patch.PatchBuilder.Reset()
		}
	}
}

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

	path := self.c.Contexts().CommitFiles.GetSelectedPath()

	if path == "" {
		return nil
	}

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

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

	path := self.c.Contexts().CommitFiles.GetSelectedPath()

	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 *PatchBuildingController) ToggleSelectionAndRefresh() error {
	if err := self.toggleSelection(); err != nil {
		return err
	}

	self.c.Refresh(types.RefreshOptions{
		Scope: []types.RefreshableView{types.PATCH_BUILDING, types.COMMIT_FILES},
	})
	return nil
}

func (self *PatchBuildingController) toggleSelection() error {
	self.context().GetMutex().Lock()
	defer self.context().GetMutex().Unlock()

	filename := self.c.Contexts().CommitFiles.GetSelectedPath()
	if filename == "" {
		return nil
	}

	state := self.context().GetState()

	// Get added/deleted lines in the selected patch range
	lineIndicesToToggle := state.LineIndicesOfAddedOrDeletedLinesInSelectedPatchRange()
	if len(lineIndicesToToggle) == 0 {
		// Only context lines or header lines selected, so nothing to do
		return nil
	}

	includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
	if err != nil {
		return err
	}

	toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange
	firstSelectedChangeLineIsStaged := lo.Contains(includedLineIndices, lineIndicesToToggle[0])
	if firstSelectedChangeLineIsStaged {
		toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange
	}

	// add range of lines to those set for the file
	if err := toggleFunc(filename, lineIndicesToToggle); err != nil {
		// might actually want to return an error here
		self.c.Log.Error(err)
	}

	if state.SelectingRange() {
		state.SetLineSelectMode()
	}

	state.SelectNextStageableLineOfSameIncludedState(self.context().GetIncludedLineIndices(), firstSelectedChangeLineIsStaged)

	return nil
}

func (self *PatchBuildingController) getDisabledReasonForDiscard() *types.DisabledReason {
	if !self.c.Git().Patch.PatchBuilder.CanRebase {
		return &types.DisabledReason{Text: self.c.Tr.CanOnlyDiscardFromLocalCommits, ShowErrorInPanel: true}
	}
	if self.c.Git().Status.WorkingTreeState().Any() {
		return &types.DisabledReason{Text: self.c.Tr.CantPatchWhileRebasingError, ShowErrorInPanel: true}
	}
	if self.c.UserConfig().Git.DiffContextSize == 0 {
		text := fmt.Sprintf(self.c.Tr.Actions.NotEnoughContextToRemoveLines,
			self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
		return &types.DisabledReason{Text: text, ShowErrorInPanel: true}
	}
	return nil
}

func (self *PatchBuildingController) discardSelection() error {
	prompt := lo.Ternary(self.c.Git().Patch.PatchBuilder.IsEmpty(),
		self.c.Tr.DiscardLinesFromCommitPrompt,
		self.c.Tr.DiscardLinesFromCommitPromptWithReset)

	self.c.Confirm(types.ConfirmOpts{
		Title:  self.c.Tr.DiscardLinesFromCommitTitle,
		Prompt: prompt,
		HandleConfirm: func() error {
			return self.discardSelectionFromCommit()
		},
	})

	return nil
}

func (self *PatchBuildingController) discardSelectionFromCommit() error {
	// Reset the current patch if there is one.
	if !self.c.Git().Patch.PatchBuilder.IsEmpty() {
		self.c.Git().Patch.PatchBuilder.Reset()
	}

	if err := self.toggleSelection(); err != nil {
		return err
	}

	if self.c.Git().Patch.PatchBuilder.IsEmpty() {
		return nil
	}

	return self.c.WithWaitingStatusSync(self.c.Tr.RebasingStatus, func() error {
		commitIndex := self.getPatchCommitIndex()
		self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit)
		err := self.c.Git().Patch.DeletePatchesFromCommit(self.c.Model().Commits, commitIndex)
		self.c.Helpers().PatchBuilding.Escape()
		return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
			err, types.RefreshOptions{Mode: types.SYNC})
	})
}

func (self *PatchBuildingController) getPatchCommitIndex() int {
	for index, commit := range self.c.Model().Commits {
		if commit.Hash() == self.c.Git().Patch.PatchBuilder.To {
			return index
		}
	}
	return -1
}

func (self *PatchBuildingController) Escape() error {
	context := self.c.Contexts().CustomPatchBuilder
	state := context.GetState()

	if state.SelectingRange() || state.SelectingHunkEnabledByUser() {
		state.SetLineSelectMode()
		self.c.PostRefreshUpdate(context)
		return nil
	}

	self.c.Helpers().PatchBuilding.Escape()
	return nil
}

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

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

	return self.c.Tr.ExitCustomPatchBuilder
}
