package linter

import (
	"bytes"
	"slices"
	"strings"

	"alpinelinux.org/go/atools/apkbuild"
	"mvdan.cc/sh/v3/syntax"
)

var (
	AL2 LintingViolation = LintingViolation{
		Code:        "AL2",
		Description: "return 1 is not required as set -e is used",
		Certainty:   Certain,
		Severity:    Minor,
		Skip:        "UNNECESSARY_RETURN_ONE",
	}

	AL9 LintingViolation = LintingViolation{
		Code:        "AL9",
		Description: "do not use the function keyword",
		Certainty:   Certain,
		Severity:    Serious,
		Skip:        "FUNCTION_KEYWORD",
	}

	AL10 LintingViolation = LintingViolation{
		Code:        "AL10",
		Description: "do not use space before function parenthesis",
		Certainty:   Certain,
		Severity:    Style,
		Skip:        "SPACE_BEFORE_FUNCTION_PARENTHESIS",
	}

	AL11 LintingViolation = LintingViolation{
		Code:        "AL11",
		Description: "use one space after function parenthesis",
		Certainty:   Certain,
		Severity:    Style,
		Skip:        "SPACE_AFTER_FUNCTION_PARENTHESIS",
	}

	AL12 LintingViolation = LintingViolation{
		Code:        "AL12",
		Description: "do not use a newline before function opening brace",
		Certainty:   Certain,
		Severity:    Style,
		Skip:        "NEWLINE_OPENING_BRACE",
	}

	AL13 LintingViolation = LintingViolation{
		Code:        "AL13",
		Description: `cd "builddir" can be removed in phase '%s'`,
		Certainty:   Certain,
		Severity:    Minor,
		Skip:        "SUPERFLUOUS_CD_BUILDDIR",
	}

	AL54 LintingViolation = LintingViolation{
		Code:        "AL54",
		Description: `prepare() is missing call to 'default_prepare'`,
		Certainty:   Certain,
		Severity:    Serious,
		Skip:        "MISSING_DEFAULT_PREPARE",
	}

	AL65 LintingViolation = LintingViolation{
		Code:        "AL65",
		Description: `non-local 'install' variable in function '%s'. Add install scripts to global install variable instead.`,
		Certainty:   Certain,
		Severity:    Important,
		Skip:        "NON_LOCAL_INSTALL_IN_FUNCTION",
	}
)

func init() {
	Register(&AL2, CheckUnnecessaryReturnOne)
	Register(&AL9, CheckFunctionKeyword)
	Register(&AL10, CheckSpaceBeforeFunctionParenthesis)
	Register(&AL11, CheckSpaceAfterFunctionParenthesis)
	Register(&AL12, CheckNewlineOpeningBrace)
	Register(&AL13, CheckSuperfluousCDBuilddir)
	Register(&AL54, CheckMissingDefaultPrepare)
	Register(&AL65, CheckNonLocalInstallVariableInFunction)
}

func CheckUnnecessaryReturnOne(apkbuild *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	for _, function := range apkbuild.Functions {
		for _, stmt := range function.Stmts {
			switch cmd := stmt.Cmd.(type) {
			case *syntax.BinaryCmd:
				if cmd.Op == syntax.OrStmt {
					violations = append(violations, checkUnnecessaryReturnOneInCmd(cmd.Y.Cmd, apkbuild)...)
				}
			}
		}
	}
	return
}

func checkUnnecessaryReturnOneInCmd(cmd syntax.Command, apkbuild *apkbuild.Apkbuild) (violations []LintingViolation) {
	switch n := cmd.(type) {
	case *syntax.CallExpr:
		buf := bytes.Buffer{}
		printer := syntax.NewPrinter()
		printer.Print(&buf, n)

		if buf.String() == "return 1" {
			violations = append(violations, AL2.Instantiate(
				apkbuild.File.Name,
				n.Pos().Line(),
				n.Pos().Col(),
			))
		}
	}
	return
}

func CheckFunctionKeyword(apkbuild *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	for _, f := range apkbuild.Functions {
		if f.FuncDecl.RsrvWord {
			violations = append(violations, AL9.Instantiate(
				apkbuild.File.Name,
				f.FuncDecl.Position.Line(),
				f.FuncDecl.Position.Col(),
			))
		}
	}
	return
}

func CheckSpaceBeforeFunctionParenthesis(apkbuild *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	for _, function := range apkbuild.Functions {
		line := apkbuild.Lines[function.FuncDecl.Position.Line()-1]
		if line[function.FuncDecl.Name.ValueEnd.Col()-1] == ' ' {
			violations = append(violations, AL10.Instantiate(
				apkbuild.File.Name,
				function.FuncDecl.Position.Line(),
				function.FuncDecl.Position.Col(),
			))
		}
	}
	return
}

func CheckSpaceAfterFunctionParenthesis(apkbuild *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	for _, function := range apkbuild.Functions {
		funcBody, ok := function.FuncDecl.Body.Cmd.(*syntax.Block)
		if ok && funcBody.Lbrace.Line() == funcBody.Rbrace.Line() {
			// single-line function, let's be more lenient with withespace
			continue
		}
		line := apkbuild.Lines[function.FuncDecl.Position.Line()-1]
		closingParensIndex := strings.Index(line, ")")
		if closingParensIndex == -1 {
			// No parens found, invalid syntax, skip this function
			continue
		}

		if len(line) < closingParensIndex+2 {
			// Line not long enough to include space + bracket
			// Skip this check as a bracket on a new line is covered by
			// AL12
			continue
		}

		if line[closingParensIndex+1] != ' ' ||
			line[closingParensIndex+2] == ' ' {

			violations = append(violations, AL11.Instantiate(
				apkbuild.File.Name,
				function.FuncDecl.Name.ValueEnd.Line(),
				uint(closingParensIndex)+2,
			))
		}
	}
	return
}

func CheckNewlineOpeningBrace(apkbuild *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	for _, function := range apkbuild.Functions {
		bodyBlock, ok := function.FuncDecl.Body.Cmd.(*syntax.Block)
		if !ok {
			// function without block?
			continue
		}

		if function.FuncDecl.Position.Line() != bodyBlock.Lbrace.Line() {
			violations = append(violations, AL12.Instantiate(
				apkbuild.File.Name,
				bodyBlock.Lbrace.Line(),
				bodyBlock.Lbrace.Col(),
			))
		}
	}
	return
}

func CheckSuperfluousCDBuilddir(a *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	for f := range a.FuncIterator() {
		if !slices.Contains([]string{"prepare", "build", "package", "check"}, f.Name) {
			// Only for these functions will abuild automatically call `cd "$builddir"`
			continue
		}
		for stmt := range f.StmtsIterator() {
			if cmd, ok := stmt.Cmd.(*syntax.CallExpr); ok {
				call := apkbuild.ParseCallExpr(cmd)
				if call.Command != "cd" {
					continue
				}
				if len(call.Args) > 0 && call.Args[0] == "$builddir" {
					violations = append(violations, AL13.Instantiate(
						a.File.Name,
						call.Expr.Pos().Line(),
						call.Expr.Pos().Col(),
						f.Name,
					))
				} else {
					break
				}
			}
		}
	}
	return
}

func CheckMissingDefaultPrepare(a *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	for f := range a.FuncIterator() {
		if f.Name != "prepare" {
			continue
		}

		for stmt := range f.StmtsIterator() {
			if cmd, ok := stmt.Cmd.(*syntax.CallExpr); ok {
				call := apkbuild.ParseCallExpr(cmd)
				if call.Command == "default_prepare" {
					return
				}
			}
		}

		violations = append(violations, AL54.Instantiate(
			a.File.Name,
			f.FuncDecl.Position.Line(),
			f.FuncDecl.Position.Col(),
		))
	}

	return
}

func CheckNonLocalInstallVariableInFunction(a *apkbuild.Apkbuild, getenv func(string) string) (violations []LintingViolation) {
	for _, f := range a.Functions {
		f.Variables.GetFirstAssignment("install").IfSome(func(v apkbuild.Variable) {
			if v.Local {
				return
			}
			violations = append(violations, AL65.Instantiate(
				a.File.Name,
				v.Assignment.Pos().Line(),
				v.Assignment.Pos().Col(),
				f.Name,
			))
		})
	}
	return violations
}
