// Copyright (c) 2026 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.

//go:build tools

package gen

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/parser"
	"go/printer"
	"go/token"
	"os"
)

func Run(source string, output string) error {
	if source == "" {
		return fmt.Errorf("no source provided, use --source flag")
	}
	if output == "" {
		return fmt.Errorf("no output provided, use --output flag")
	}

	if err := processFile(source, output); err != nil {
		return fmt.Errorf("failed to process file %s: %w", source, err)
	}

	return nil
}

func processFile(filename string, outputFilename string) error {
	fset := token.NewFileSet()

	node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
	if err != nil {
		return fmt.Errorf("failed to parse file: %w", err)
	}

	var buf bytes.Buffer

	packageName := node.Name.Name

	buf.WriteString("// Code generated by go-accessor-gen. DO NOT EDIT.\n\n")
	buf.WriteString(fmt.Sprintf("package %s\n\n", packageName))

	if len(node.Imports) > 0 {
		buf.WriteString("import (\n")
		for _, imp := range node.Imports {
			if imp.Name != nil {
				buf.WriteString(fmt.Sprintf("\t%s %s\n", imp.Name.Name, imp.Path.Value))
			} else {
				buf.WriteString(fmt.Sprintf("\t%s\n", imp.Path.Value))
			}
		}
		buf.WriteString(")\n\n")
	}

	for _, decl := range node.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, typeSpecOk := spec.(*ast.TypeSpec)
			if !typeSpecOk {
				continue
			}

			structType, structTypeOk := typeSpec.Type.(*ast.StructType)
			if !structTypeOk {
				continue
			}

			structName := typeSpec.Name.Name

			for _, field := range structType.Fields.List {
				// field.Type is the type AST. We want to check if it's a pointer.
				starExpr, starExprOk := field.Type.(*ast.StarExpr)
				if !starExprOk {
					continue
				}

				var typeBuf bytes.Buffer

				if err = printer.Fprint(&typeBuf, fset, starExpr.X); err != nil {
					return fmt.Errorf("failed to print type: %w", err)
				}

				valueType := typeBuf.String()

				// Field names. A field line can have multiple names: `A, B *int`
				for _, name := range field.Names {
					fieldName := name.Name

					// Getter
					// func (s *Struct) FieldName() Type
					buf.WriteString(fmt.Sprintf("func (s *%s) Get%s() %s {\n", structName, fieldName, valueType))
					buf.WriteString(fmt.Sprintf("\tif s == nil || s.%s == nil {\n", fieldName))
					buf.WriteString(fmt.Sprintf("\t\treturn *new(%s)\n", valueType)) // Zero value
					buf.WriteString("\t}\n")
					buf.WriteString(fmt.Sprintf("\treturn *s.%s\n", fieldName))
					buf.WriteString("}\n\n")

					// Setter
					// func (s *Struct) SetFieldName(v Type)
					buf.WriteString(fmt.Sprintf("func (s *%s) Set%s(v %s) {\n", structName, fieldName, valueType))

					buf.WriteString(fmt.Sprintf("\ts.%s = &v\n", fieldName))
					buf.WriteString("}\n\n")
				}
			}
		}
	}

	data := bytes.TrimSpace(buf.Bytes())
	data = append(data, '\n')

	if err = os.WriteFile(outputFilename, data, 0o644); err != nil {
		return fmt.Errorf("failed to write file %s: %w", outputFilename, err)
	}

	return nil
}
