// Package permissionstatewritergen derives the direct Permission State writer
// inventory from the managed semantic-surface catalog and production Go
// mutation sites.
//
//go:generate go run ./cmd -root ../../.. -out ../../pkg/schemacatalog/z_permission_state_direct_writers.go
package permissionstatewritergen

import (
	"bytes"
	"encoding/json"
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/token"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

type catalogEntry struct {
	TableName               string                      `json:"tableName"`
	PermissionStateMutation *catalogMutationDeclaration `json:"permissionStateMutation,omitempty"`
	Columns                 []catalogColumn             `json:"columns"`
	ForeignKeys             []catalogForeignKey         `json:"foreignKeys"`
}

type catalogMutationDeclaration struct {
	SurfaceID string `json:"surfaceId"`
}

type catalogColumn struct {
	References string `json:"references"`
	OnDelete   string `json:"onDelete"`
}

type catalogForeignKey struct {
	References string `json:"references"`
	OnDelete   string `json:"onDelete"`
}

type managedCatalog struct {
	surfaces        map[string]string
	targets         map[string]string
	cascadeChildren map[string][]string
}

type directWriter struct {
	writerID   string
	surfaceIDs []string
}

type mutationOperation uint8

const (
	mutationInsert mutationOperation = 1 << iota
	mutationUpdate
	mutationDelete
)

// Generate returns gofmt-formatted source for the executable direct-writer
// projection. repositoryRoot must be the workspace root containing go.work.
func Generate(repositoryRoot string) ([]byte, error) {
	root, err := filepath.Abs(repositoryRoot)
	if err != nil {
		return nil, fmt.Errorf("permission state writer generator: resolve root: %w", err)
	}
	catalog, err := loadManagedCatalog(root)
	if err != nil {
		return nil, err
	}
	tableConstants, err := managedTableConstants(root, catalog.targets)
	if err != nil {
		return nil, err
	}
	writers, err := scanDirectWriters(root, catalog, tableConstants)
	if err != nil {
		return nil, err
	}
	return render(writers)
}

func loadManagedCatalog(root string) (managedCatalog, error) {
	path := filepath.Join(root, "nexuscrm-kernel", "pkg", "schemacatalog", "system_tables.json")
	raw, err := os.ReadFile(path)
	if err != nil {
		return managedCatalog{}, fmt.Errorf("permission state writer generator: read managed catalog: %w", err)
	}
	var entries []catalogEntry
	if err := json.Unmarshal(raw, &entries); err != nil {
		return managedCatalog{}, fmt.Errorf("permission state writer generator: parse managed catalog: %w", err)
	}
	surfaces := make(map[string]string)
	cascadeChildren := make(map[string][]string)
	managedTables := make(map[string]struct{})
	for _, entry := range entries {
		tableName := strings.TrimSpace(entry.TableName)
		if tableName == "" || tableName != entry.TableName {
			return managedCatalog{}, fmt.Errorf(
				"permission state writer generator: invalid managed table %q",
				entry.TableName,
			)
		}
		if _, duplicate := managedTables[tableName]; duplicate {
			return managedCatalog{}, fmt.Errorf(
				"permission state writer generator: duplicate managed table %s",
				tableName,
			)
		}
		managedTables[tableName] = struct{}{}
		if entry.PermissionStateMutation != nil {
			surfaceID := strings.TrimSpace(entry.PermissionStateMutation.SurfaceID)
			if surfaceID == "" || surfaceID != entry.PermissionStateMutation.SurfaceID {
				return managedCatalog{}, fmt.Errorf(
					"permission state writer generator: invalid managed declaration table=%q surface=%q",
					entry.TableName,
					entry.PermissionStateMutation.SurfaceID,
				)
			}
			surfaces[tableName] = surfaceID
		}
		for _, foreignKey := range entry.ForeignKeys {
			addCascadeChild(cascadeChildren, foreignKey.References, foreignKey.OnDelete, tableName)
		}
		for _, column := range entry.Columns {
			addCascadeChild(cascadeChildren, column.References, column.OnDelete, tableName)
		}
	}
	if len(surfaces) == 0 {
		return managedCatalog{}, fmt.Errorf("permission state writer generator: no managed surfaces")
	}
	for parent := range cascadeChildren {
		sort.Strings(cascadeChildren[parent])
	}
	targets := make(map[string]string, len(surfaces))
	for tableName, surfaceID := range surfaces {
		targets[tableName] = surfaceID
	}
	for changed := true; changed; {
		changed = false
		for parent, children := range cascadeChildren {
			if _, tracked := targets[parent]; tracked {
				continue
			}
			for _, child := range children {
				if _, tracked := targets[child]; !tracked {
					continue
				}
				targets[parent] = ""
				changed = true
				break
			}
		}
	}
	return managedCatalog{
		surfaces:        surfaces,
		targets:         targets,
		cascadeChildren: cascadeChildren,
	}, nil
}

func addCascadeChild(children map[string][]string, references, onDelete, child string) {
	if !strings.EqualFold(strings.TrimSpace(onDelete), "CASCADE") {
		return
	}
	references = strings.TrimSpace(references)
	parent, _, _ := strings.Cut(references, "(")
	parent = strings.TrimSpace(parent)
	if parent == "" {
		return
	}
	children[parent] = append(children[parent], child)
}

func managedTableConstants(root string, surfaces map[string]string) (map[string]string, error) {
	path := filepath.Join(root, "shared", "pkg", "constants", "z_generated_tables.go")
	fileSet := token.NewFileSet()
	file, err := parser.ParseFile(fileSet, path, nil, 0)
	if err != nil {
		return nil, fmt.Errorf("permission state writer generator: parse table constants: %w", err)
	}
	constants := make(map[string]string)
	for _, declaration := range file.Decls {
		general, ok := declaration.(*ast.GenDecl)
		if !ok {
			continue
		}
		for _, specification := range general.Specs {
			value, ok := specification.(*ast.ValueSpec)
			if !ok || len(value.Names) != len(value.Values) {
				continue
			}
			for index, name := range value.Names {
				tableName, ok := staticString(value.Values[index], nil, nil)
				if !ok {
					continue
				}
				if _, managed := surfaces[tableName]; managed {
					constants[name.Name] = tableName
				}
			}
		}
	}
	covered := make(map[string]bool, len(constants))
	for _, tableName := range constants {
		covered[tableName] = true
	}
	for tableName := range surfaces {
		if !covered[tableName] {
			return nil, fmt.Errorf(
				"permission state writer generator: managed table %s has no generated table constant",
				tableName,
			)
		}
	}
	return constants, nil
}

func scanDirectWriters(
	root string,
	catalog managedCatalog,
	tableConstants map[string]string,
) ([]directWriter, error) {
	writersByID := make(map[string]directWriter)
	err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			if skippedDirectory(entry.Name()) {
				return filepath.SkipDir
			}
			return nil
		}
		if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
			return nil
		}
		fileSet := token.NewFileSet()
		file, err := parser.ParseFile(fileSet, path, nil, 0)
		if err != nil {
			return err
		}
		packageStrings := packageStaticStrings(file, tableConstants)
		relative, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}
		relative = filepath.ToSlash(relative)
		for _, declaration := range file.Decls {
			function, ok := declaration.(*ast.FuncDecl)
			if !ok || function.Body == nil {
				continue
			}
			targets := mutationTargets(function.Body, tableConstants, catalog.targets, packageStrings)
			if len(targets) == 0 {
				continue
			}
			symbol := function.Name.Name
			if function.Recv != nil && len(function.Recv.List) == 1 {
				symbol = receiverName(function.Recv.List[0].Type) + "." + function.Name.Name
			}
			writerID := relative + "#" + symbol
			if previous, duplicate := writersByID[writerID]; duplicate {
				return fmt.Errorf(
					"permission state writer generator: writer %s is duplicated with surfaces %v",
					writerID,
					previous.surfaceIDs,
				)
			}
			surfaceIDs := affectedSurfaceIDs(targets, catalog)
			if len(surfaceIDs) == 0 {
				continue
			}
			writersByID[writerID] = directWriter{
				writerID:   writerID,
				surfaceIDs: surfaceIDs,
			}
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("permission state writer generator: scan production Go: %w", err)
	}
	writers := make([]directWriter, 0, len(writersByID))
	for _, writer := range writersByID {
		writers = append(writers, writer)
	}
	sort.Slice(writers, func(i, j int) bool {
		return writers[i].writerID < writers[j].writerID
	})
	if len(writers) == 0 {
		return nil, fmt.Errorf("permission state writer generator: no direct writers found")
	}
	return writers, nil
}

func affectedSurfaceIDs(
	targets map[string]mutationOperation,
	catalog managedCatalog,
) []string {
	surfaceSet := make(map[string]struct{})
	for tableName, operations := range targets {
		if surfaceID, ok := catalog.surfaces[tableName]; ok {
			surfaceSet[surfaceID] = struct{}{}
		}
		if operations&mutationDelete == 0 {
			continue
		}
		pending := append([]string(nil), catalog.cascadeChildren[tableName]...)
		visited := make(map[string]bool)
		for len(pending) > 0 {
			child := pending[0]
			pending = pending[1:]
			if visited[child] {
				continue
			}
			visited[child] = true
			if surfaceID, ok := catalog.surfaces[child]; ok {
				surfaceSet[surfaceID] = struct{}{}
			}
			pending = append(pending, catalog.cascadeChildren[child]...)
		}
	}
	surfaceIDs := make([]string, 0, len(surfaceSet))
	for surfaceID := range surfaceSet {
		surfaceIDs = append(surfaceIDs, surfaceID)
	}
	sort.Strings(surfaceIDs)
	return surfaceIDs
}

func skippedDirectory(name string) bool {
	switch name {
	case ".git", "node_modules", "testdata", "testhelpers", "vendor":
		return true
	default:
		return false
	}
}

func packageStaticStrings(file *ast.File, tableConstants map[string]string) map[string]string {
	values := make(map[string]string)
	for _, declaration := range file.Decls {
		general, ok := declaration.(*ast.GenDecl)
		if !ok {
			continue
		}
		for _, specification := range general.Specs {
			value, ok := specification.(*ast.ValueSpec)
			if !ok || len(value.Names) != len(value.Values) {
				continue
			}
			for index, name := range value.Names {
				if text, ok := staticString(value.Values[index], values, tableConstants); ok {
					values[name.Name] = text
				}
			}
		}
	}
	return values
}

func mutationTables(
	body *ast.BlockStmt,
	tableConstants map[string]string,
	surfaces map[string]string,
	packageStrings map[string]string,
) []string {
	targets := mutationTargets(body, tableConstants, surfaces, packageStrings)
	tables := make([]string, 0, len(targets))
	for tableName := range targets {
		tables = append(tables, tableName)
	}
	sort.Strings(tables)
	return tables
}

func mutationTargets(
	body *ast.BlockStmt,
	tableConstants map[string]string,
	surfaces map[string]string,
	packageStrings map[string]string,
) map[string]mutationOperation {
	bindings := make(map[string]string, len(packageStrings))
	for name, value := range packageStrings {
		bindings[name] = value
	}
	collectLocalStrings(body, bindings, tableConstants)
	found := make(map[string]mutationOperation)
	ast.Inspect(body, func(node ast.Node) bool {
		call, ok := node.(*ast.CallExpr)
		if !ok {
			return true
		}
		if tableName, operation, ok := queryBuilderMutation(
			call,
			bindings,
			tableConstants,
			surfaces,
		); ok {
			found[tableName] |= operation
		}
		for tableName, operations := range formattedMutation(call, bindings, tableConstants, surfaces) {
			found[tableName] |= operations
		}
		for _, argument := range call.Args {
			text, ok := staticString(argument, bindings, tableConstants)
			if !ok {
				continue
			}
			for tableName := range surfaces {
				if operations := literalMutationOperations(text, tableName); operations != 0 {
					found[tableName] |= operations
				}
			}
		}
		return true
	})
	return found
}

func collectLocalStrings(body *ast.BlockStmt, bindings, tableConstants map[string]string) {
	ast.Inspect(body, func(node ast.Node) bool {
		switch value := node.(type) {
		case *ast.AssignStmt:
			if len(value.Lhs) != len(value.Rhs) {
				return true
			}
			for index, left := range value.Lhs {
				name, ok := left.(*ast.Ident)
				if !ok {
					continue
				}
				if text, ok := staticString(value.Rhs[index], bindings, tableConstants); ok {
					bindings[name.Name] = text
				}
			}
		case *ast.DeclStmt:
			general, ok := value.Decl.(*ast.GenDecl)
			if !ok {
				return true
			}
			for _, specification := range general.Specs {
				declaration, ok := specification.(*ast.ValueSpec)
				if !ok || len(declaration.Names) != len(declaration.Values) {
					continue
				}
				for index, name := range declaration.Names {
					if text, ok := staticString(declaration.Values[index], bindings, tableConstants); ok {
						bindings[name.Name] = text
					}
				}
			}
		}
		return true
	})
}

func queryBuilderMutation(
	call *ast.CallExpr,
	bindings, tableConstants, surfaces map[string]string,
) (string, mutationOperation, bool) {
	selector, ok := call.Fun.(*ast.SelectorExpr)
	if !ok || len(call.Args) == 0 {
		return "", 0, false
	}
	tableName, protected := protectedTableExpression(
		call.Args[0],
		bindings,
		tableConstants,
		surfaces,
	)
	if !protected {
		return "", 0, false
	}
	switch selector.Sel.Name {
	case "Insert", "BulkInsertOrdered":
		return tableName, mutationInsert, true
	case "Update":
		return tableName, mutationUpdate, true
	case "Delete":
		return tableName, mutationDelete, true
	default:
		return "", 0, false
	}
}

func formattedMutation(
	call *ast.CallExpr,
	bindings, tableConstants, surfaces map[string]string,
) map[string]mutationOperation {
	selector, ok := call.Fun.(*ast.SelectorExpr)
	if !ok || selector.Sel.Name != "Sprintf" || len(call.Args) == 0 {
		return nil
	}
	formatText, ok := staticString(call.Args[0], bindings, tableConstants)
	if !ok || !hasMutationPlaceholder(formatText) {
		return nil
	}
	values := make([]any, 0, len(call.Args)-1)
	for _, argument := range call.Args[1:] {
		if tableName, ok := protectedTableExpression(
			argument,
			bindings,
			tableConstants,
			surfaces,
		); ok {
			values = append(values, tableName)
			continue
		}
		if text, ok := staticString(argument, bindings, tableConstants); ok {
			values = append(values, text)
			continue
		}
		values = append(values, expressionName(argument))
	}
	rendered := fmt.Sprintf(formatText, values...)
	tables := make(map[string]mutationOperation)
	for tableName := range surfaces {
		if operations := literalMutationOperations(rendered, tableName); operations != 0 {
			tables[tableName] = operations
		}
	}
	return tables
}

func protectedTableExpression(
	expression ast.Expr,
	bindings, tableConstants, surfaces map[string]string,
) (string, bool) {
	if call, ok := expression.(*ast.CallExpr); ok && len(call.Args) == 1 {
		return protectedTableExpression(call.Args[0], bindings, tableConstants, surfaces)
	}
	tableName, ok := staticString(expression, bindings, tableConstants)
	if !ok {
		return "", false
	}
	_, managed := surfaces[tableName]
	return tableName, managed
}

func staticString(expression ast.Expr, bindings, tableConstants map[string]string) (string, bool) {
	switch value := expression.(type) {
	case *ast.BasicLit:
		if value.Kind != token.STRING {
			return "", false
		}
		text, err := strconv.Unquote(value.Value)
		return text, err == nil
	case *ast.BinaryExpr:
		if value.Op != token.ADD {
			return "", false
		}
		left, leftOK := staticString(value.X, bindings, tableConstants)
		right, rightOK := staticString(value.Y, bindings, tableConstants)
		return left + right, leftOK && rightOK
	case *ast.Ident:
		if bindings == nil {
			return "", false
		}
		text, ok := bindings[value.Name]
		return text, ok
	case *ast.SelectorExpr:
		if tableConstants == nil {
			return "", false
		}
		text, ok := tableConstants[value.Sel.Name]
		return text, ok
	default:
		return "", false
	}
}

func expressionName(expression ast.Expr) string {
	switch value := expression.(type) {
	case *ast.SelectorExpr:
		return value.Sel.Name
	case *ast.Ident:
		return value.Name
	case *ast.BasicLit:
		return value.Value
	default:
		return "value"
	}
}

func hasMutationPlaceholder(text string) bool {
	normalized := strings.Join(strings.Fields(text), " ")
	verb := `%(?:\[[0-9]+\])?[a-z]`
	patterns := []string{
		`(?i)\bINSERT(?:\s+IGNORE)?\s+INTO\s+(?:` + "`" + `)?` + verb,
		`(?i)\bUPDATE\s+(?:` + "`" + `)?` + verb,
		`(?i)\bDELETE\s+FROM\s+(?:` + "`" + `)?` + verb,
		`(?i)\bREPLACE\s+INTO\s+(?:` + "`" + `)?` + verb,
		`(?i)\bTRUNCATE(?:\s+TABLE)?\s+(?:` + "`" + `)?` + verb,
		`(?i)\bDROP\s+TABLE(?:\s+IF\s+EXISTS)?\s+(?:` + "`" + `)?` + verb,
	}
	for _, pattern := range patterns {
		if regexp.MustCompile(pattern).MatchString(normalized) {
			return true
		}
	}
	return false
}

func literalMutationOperations(text, tableName string) mutationOperation {
	normalized := strings.Join(strings.Fields(text), " ")
	quoted := regexp.QuoteMeta(tableName)
	patterns := []struct {
		operation  mutationOperation
		expression string
	}{
		{
			operation:  mutationInsert,
			expression: `(?i)\bINSERT(?:\s+IGNORE)?\s+INTO\s+` + "`?" + quoted + "`?" + `(?:\b|\s|\()`,
		},
		{
			operation:  mutationUpdate,
			expression: `(?i)\bUPDATE\s+` + "`?" + quoted + "`?" + `(?:\b|\s)`,
		},
		{
			operation:  mutationDelete,
			expression: `(?i)\bDELETE\s+FROM\s+` + "`?" + quoted + "`?" + `(?:\b|\s)`,
		},
		{
			operation:  mutationInsert | mutationDelete,
			expression: `(?i)\bREPLACE\s+INTO\s+` + "`?" + quoted + "`?" + `(?:\b|\s|\()`,
		},
		{
			operation:  mutationDelete,
			expression: `(?i)\bTRUNCATE(?:\s+TABLE)?\s+` + "`?" + quoted + "`?" + `(?:\b|\s)`,
		},
		{
			operation:  mutationDelete,
			expression: `(?i)\bDROP\s+TABLE(?:\s+IF\s+EXISTS)?\s+` + "`?" + quoted + "`?" + `(?:\b|\s)`,
		},
	}
	var operations mutationOperation
	for _, pattern := range patterns {
		if regexp.MustCompile(pattern.expression).MatchString(normalized) {
			operations |= pattern.operation
		}
	}
	return operations
}

func receiverName(expression ast.Expr) string {
	switch value := expression.(type) {
	case *ast.Ident:
		return value.Name
	case *ast.StarExpr:
		return receiverName(value.X)
	case *ast.IndexExpr:
		return receiverName(value.X)
	case *ast.IndexListExpr:
		return receiverName(value.X)
	default:
		return "receiver"
	}
}

func render(writers []directWriter) ([]byte, error) {
	var output bytes.Buffer
	output.WriteString("// Code generated by permissionstatewritergen; DO NOT EDIT.\n\n")
	output.WriteString("package schemacatalog\n\n")
	output.WriteString("var managedPermissionStateDirectWriterDeclarations = []permissionStateDirectWriterDeclaration{\n")
	for _, writer := range writers {
		fmt.Fprintf(&output, "\t{\n\t\tWriterID: %q,\n\t\tSurfaceIDs: []string{\n", writer.writerID)
		for _, surfaceID := range writer.surfaceIDs {
			fmt.Fprintf(&output, "\t\t\t%q,\n", surfaceID)
		}
		output.WriteString("\t\t},\n\t},\n")
	}
	output.WriteString("}\n")
	formatted, err := format.Source(output.Bytes())
	if err != nil {
		return nil, fmt.Errorf("permission state writer generator: format generated source: %w", err)
	}
	return formatted, nil
}
