package main

import (
	"encoding/json"
	"fmt"
	"go/ast"
	"go/token"
	"strconv"
	"strings"

	"github.com/fatih/color"
	"github.com/hexops/gotextdiff"
	"github.com/hexops/gotextdiff/myers"
	"github.com/hexops/gotextdiff/span"
	ordered "gitlab.com/c0b/go-ordered-json"
	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/passes/inspect"
	"golang.org/x/tools/go/analysis/singlechecker"
	"golang.org/x/tools/go/ast/inspector"
)

func main() {
	singlechecker.Main(Analyzer)
}

var Analyzer = &analysis.Analyzer{
	Name:     "awsIAMPolicyLinter",
	Doc:      "Checks whether the aws iam policy document is formatted.",
	Run:      run,
	Requires: []*analysis.Analyzer{inspect.Analyzer},
}

func run(pass *analysis.Pass) (interface{}, error) {
	inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	nodeFilter := []ast.Node{
		(*ast.ValueSpec)(nil),
	}

	inspector.Preorder(nodeFilter, func(node ast.Node) {
		valSpec, ok := node.(*ast.ValueSpec)
		if !ok {
			fmt.Println("node is not ValueSpec")
			return
		}
		if valSpec.Doc == nil {
			return
		}

		isPolicyDoc := false
		for _, comment := range valSpec.Doc.List {
			t := strings.TrimSpace(strings.TrimPrefix(comment.Text, "//"))

			if t == "aws:iam:policyDocument" {
				isPolicyDoc = true
			}
		}

		if !isPolicyDoc {
			return
		}
		for i, val := range valSpec.Values {
			lit, ok := val.(*ast.BasicLit)
			if !ok {
				pass.Reportf(node.Pos(), "%s is not BasicLit",
					valSpec.Names[i].Name)
			}
			if lit.Kind != token.STRING {
				pass.Reportf(node.Pos(), "%s is not string",
					valSpec.Names[i].Name)
			}
			s, err := strconv.Unquote(lit.Value)
			if err != nil {
				pass.Reportf(node.Pos(), "%s is not vaild string: %w",
					valSpec.Names[i].Name, err)
			}
			diff, err := validate(valSpec.Names[i].Name, s)
			if err != nil {
				pass.Reportf(node.Pos(), "%s is not vaild json: %v",
					valSpec.Names[i].Name, err)
			}
			if diff != "" {
				pass.Reportf(node.Pos(), "policy document %s is not well formatted, diff:\n%s",
					valSpec.Names[i].Name, diff)
			}
		}
	})

	return nil, nil
}

func validate(name, s string) (string, error) {
	m := ordered.NewOrderedMap()
	if err := json.Unmarshal([]byte(s), &m); err != nil {
		return "", fmt.Errorf("is not vaild json string: %w", err)
	}
	bs, err := json.MarshalIndent(m, "", "  ")
	if err != nil {
		return "", fmt.Errorf("can't marshal to json with indent: %w", err)
	}
	formatted := string(bs)

	edits := myers.ComputeEdits(span.URIFromPath(name+":original"), s, formatted)
	unified := gotextdiff.ToUnified(name+":original", name+":expected", s, edits)

	u := PrintUnified(&unified)
	return u, nil
}

func PrintUnified(u *gotextdiff.Unified) string {
	if len(u.Hunks) == 0 {
		return ""
	}
	red := color.New(color.FgRed)
	green := color.New(color.FgGreen)
	blue := color.New(color.FgHiBlue)

	builder := strings.Builder{}

	red.Fprintf(&builder, "--- %s\n", u.From)
	green.Fprintf(&builder, "+++ %s\n", u.To)
	for _, hunk := range u.Hunks {
		fromCount, toCount := 0, 0
		for _, l := range hunk.Lines {
			switch l.Kind {
			case gotextdiff.Delete:
				fromCount++
			case gotextdiff.Insert:
				toCount++
			default:
				fromCount++
				toCount++
			}
		}
		blue.Fprint(&builder, "@@")
		if fromCount > 1 {
			red.Fprintf(&builder, " -%d,%d", hunk.FromLine, fromCount)
		} else {
			red.Fprintf(&builder, " -%d", hunk.FromLine)
		}
		if toCount > 1 {
			green.Fprintf(&builder, " +%d,%d", hunk.ToLine, toCount)
		} else {
			green.Fprintf(&builder, " +%d", hunk.ToLine)
		}
		blue.Fprint(&builder, " @@\n")
		for _, l := range hunk.Lines {
			switch l.Kind {
			case gotextdiff.Delete:
				red.Fprintf(&builder, "-%s", l.Content)
			case gotextdiff.Insert:
				green.Fprintf(&builder, "+%s", l.Content)
			default:
				fmt.Fprintf(&builder, " %s", l.Content)
			}
			if !strings.HasSuffix(l.Content, "\n") {
				fmt.Fprintf(&builder, "\n\\ No newline at end\n")
			}
		}
	}
	return builder.String()
}
