// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

//go:build gorules

package gorules

// to apply changes in this file, please do: ./build/bin/golangci-lint cache clean
import (
	"github.com/quasilyte/go-ruleguard/dsl"
)

func txDeferRollback(m dsl.Matcher) {
	// Common pattern for long-living transactions:
	//	tx, err := db.Begin()
	//	if err != nil {
	//		return err
	//	}
	//	defer tx.Rollback()
	//
	//	... code which uses database in transaction
	//
	//	err := tx.Commit()
	//	if err != nil {
	//		return err
	//	}

	m.Match(
		`$tx, $err := $db.BeginRw($ctx); $chk; $rollback`,
		`$tx, $err = $db.BeginRw($ctx); $chk; $rollback`,
		`$tx, $err := $db.Begin($ctx); $chk; $rollback`,
		`$tx, $err = $db.Begin($ctx); $chk; $rollback`,
		`$tx, $err := $db.BeginRo($ctx); $chk; $rollback`,
		`$tx, $err = $db.BeginRo($ctx); $chk; $rollback`,
		`$tx, $err = $db.BeginTemporalRw($ctx); $chk; $rollback`,
		`$tx, $err := $db.BeginTemporalRw($ctx); $chk; $rollback`,
		`$tx, $err = $db.BeginTemporalRo($ctx); $chk; $rollback`,
		`$tx, $err := $db.BeginTemporalRo($ctx); $chk; $rollback`,
		`$tx, $err := $db.BeginRwNosync($ctx); $chk; $rollback`,
		`$tx, $err = $db.BeginRwNosync($ctx); $chk; $rollback`,
	).
		Where(!m["rollback"].Text.Matches(`defer .*\.Rollback()`) && !m["rollback"].Text.Matches(`t\.Cleanup\(.*\.Rollback\)`)).
		//At(m["rollback"]).
		Report(`Add "defer $tx.Rollback()" or "t.Cleanup($tx.Rollback)" right after transaction creation error check. 
			If you are in the loop - consider using "$db.View" or "$db.Update" or extract whole transaction to function.
			Without rollback in defer - app can deadlock on error or panic.
			Rules are in ./rules.go file.
			`)
}

func cursorDeferClose(m dsl.Matcher) {
	m.Match(
		`$c, $err = $db.Cursor($table); $chk; $close`,
		`$c, $err := $db.Cursor($table); $chk; $close`,
		`$c, $err = $db.RwCursor($table); $chk; $close`,
		`$c, $err := $db.RwCursor($table); $chk; $close`,
		`$c, $err = $db.CursorDupSort($table); $chk; $close`,
		`$c, $err := $db.CursorDupSort($table); $chk; $close`,
		`$c, $err = $db.RwCursorDupSort($table); $chk; $close`,
		`$c, $err := $db.RwCursorDupSort($table); $chk; $close`,
	).
		Where(!m["close"].Text.Matches(`defer .*\.Close()`) && !m["close"].Text.Matches(`t\.Cleanup\(.*\.Close\)`)).
		//At(m["close"]).
		Report(`Add "defer $c.Close()" or "t.Cleanup($c.Close)" right after cursor creation error check`)
}

func streamDeferClose(m dsl.Matcher) {
	m.Match(
		`$c, $err = $db.Range($params); $chk; $close`,
		`$c, $err := $db.Range($params); $chk; $close`,
		`$c, $err = $db.RangeDupSort($params); $chk; $close`,
		`$c, $err := $db.RangeDupSort($params); $chk; $close`,
		`$c, $err = $db.Prefix($params); $chk; $close`,
		`$c, $err := $db.Prefix($params); $chk; $close`,
	).
		Where(!m["close"].Text.Matches(`defer .*\.Close()`) && !m["close"].Text.Matches(`t\.Cleanup\(.*\.Close\)`)).
		//At(m["close"]).
		Report(`Add "defer $c.Close()" or "t.Cleanup($c.Close)" right after cursor creation error check`)
}

func closeCollector(m dsl.Matcher) {
	m.Match(
		`$c := etl.NewCollector($*_); $close`,
		`$c := etl.NewCollectorWithAllocator($*_); $close`,
	).
		Where(!m["close"].Text.Matches(`defer .*\.Close()`)).
		Report(`Add "defer $c.Close()" right after collector creation`)
}

func closeLockedDir(m dsl.Matcher) {
	m.Match(`$c := dir.OpenRw($*_); $close`).
		Where(!m["close"].Text.Matches(`defer .*\.Close()`)).
		Report(`Add "defer $c.Close()" after locked.OpenDir`)
}

func passValuesByContext(m dsl.Matcher) {
	m.Match(`ctx.WithValue($*_)`).Report(`Don't pass app-level parameters by context, pass them as-is or as typed objects`)
}

func mismatchingUnlock(m dsl.Matcher) {
	// By default, an entire match position is used as a location.
	// This can be changed by the At() method that binds the location
	// to the provided named submatch.
	//
	// In the rules below text editor would get mismatching method
	// name locations:
	//
	//   defer mu.RUnlock()
	//            ^^^^^^^

	m.Match(`$mu.Lock(); defer $mu.$unlock()`).
		Where(m["unlock"].Text == "RUnlock").
		At(m["unlock"]).
		Report(`Did you mean $mu.Unlock()?
			Rules are in ./rules.go file.`)

	m.Match(`$mu.RLock(); defer $mu.$unlock()`).
		Where(m["unlock"].Text == "Unlock").
		At(m["unlock"]).
		Report(`Did you mean $mu.RUnlock()?
			Rules are in ./rules.go file.`)
}

func forbidOsRemove(m dsl.Matcher) {
	m.Match(
		`os.Remove($*_)`,
		`os.RemoveAll($*_)`,
	).
		Report(`Don't call os.Remove/RemoveAll directly; use dir.RemoveFile/RemoveAll instead (erigon/common/dir)`)
}

func filepathWalkToCheckToSkipNonExistingFiles(m dsl.Matcher) {
	m.Match(`filepath.Walk($dir, $cb)`).Report(`report("Use filepath.WalkDir or fs.WalkDir, because Walk does not skip removed files and does much more syscalls")`)
}

func osCreateBlankAssign(m dsl.Matcher) {
	m.Match(
		`_, $err := os.Create($path)`,
		`_, $err = os.Create($path)`,
		`_, _ = os.Create($path)`,
		`_, $err := os.CreateTemp($dir, $pattern)`,
		`_, $err = os.CreateTemp($dir, $pattern)`,
		`_, _ = os.CreateTemp($dir, $pattern)`,
		`_, $err := os.OpenFile($path, $flag, $perm)`,
		`_, $err = os.OpenFile($path, $flag, $perm)`,
		`_, _ = os.OpenFile($path, $flag, $perm)`,
	).
		Report(`os.Create/OpenFile result assigned to _ leaks a file descriptor. Assign to a variable and close it.
			Rules are in ./rules.go file.`)
}
