package main

import (
	_ "embed"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"text/template"

	logger "git.site.quack-lab.dev/dave/cylogger"
	"github.com/PuerkitoBio/goquery"
)

//go:embed class.tmpl
var templatestr string
var classTemplate *template.Template

var fns = template.FuncMap{
	"plus1": func(x int) int {
		return x + 1
	},
	"sub": func(x, y int) int {
		return x - y
	},
	"truncateComment": func(comment string, maxLen int) string {
		if len(comment) <= maxLen {
			return comment
		}
		// Find the last space before maxLen
		truncated := comment[:maxLen]
		lastSpace := strings.LastIndex(truncated, " ")
		if lastSpace > maxLen/2 {
			return truncated[:lastSpace]
		}
		return truncated
	},
}

func init() {
	log := logger.Default.WithPrefix("init")
	log.Info("Initializing template")
	var err error
	classTemplate, err = template.New("class").Funcs(fns).Parse(templatestr)
	if err != nil {
		logger.Error("Error parsing template: %v", err)
		return
	}
	log.Info("Template initialized successfully")
}

type (
	Class struct {
		ClassName    string
		Fields       []Field
		Methods      []Method
		Constructors []Constructor
	}
	Field struct {
		Name    string
		Type    string
		Comment string
	}
	Method struct {
		Name    string
		Params  []Param
		Returns []Return
		Comment string
	}
	Param struct {
		Name    string
		Type    string
		Comment string
	}
	Return struct {
		Type    string
		Comment string
	}
	Constructor struct {
		Params  []Param
		Comment string
	}
)

func (c *Class) GetOutFile(root string) (*os.File, error) {
	log := logger.Default.WithPrefix("GetOutFile")
	log.Debug("Getting output file for class: %s", c.ClassName)

	if c.ClassName == "" {
		log.Error("ClassName is empty")
		return nil, fmt.Errorf("ClassName is empty")
	}

	filename := fmt.Sprintf("%s.lua", c.ClassName)
	log.Trace("Original filename: %s", filename)

	filename = strings.ReplaceAll(filename, " ", "")
	filename = strings.ReplaceAll(filename, "-", "")
	filename = strings.ReplaceAll(filename, ",", "")
	filename = strings.ReplaceAll(filename, ":", "")
	log.Trace("Cleaned filename: %s", filename)

	filePath := filepath.Join(root, filename)
	filePath = filepath.Clean(filePath)
	log.Debug("Full file path: %s", filePath)

	f, err := os.Create(filePath)
	if err != nil {
		log.Error("Error creating file: %v", err)
		return nil, err
	}

	log.Info("Successfully created output file: %s", filePath)
	return f, nil
}

func (c *Class) Write(root string, tmpl *template.Template) error {
	log := logger.Default.WithPrefix("Write")
	log.Info("Writing class %s to output", c.ClassName)

	outfile, err := c.GetOutFile(root)
	if err != nil {
		log.Error("Error creating output file %v: %v", c.ClassName, err)
		return fmt.Errorf("error creating output file %v: %v", c.ClassName, err)
	}

	log.Debug("Executing template for class: %s", c.ClassName)
	err = tmpl.Execute(outfile, c)
	if err != nil {
		log.Error("Error writing output file %v: %v", c.ClassName, err)
		return fmt.Errorf("error writing output file %v: %v", c.ClassName, err)
	}

	log.Info("Successfully wrote class %s to output", c.ClassName)
	return nil
}

func ParseClass(file string) (*Class, error) {
	log := logger.Default.WithPrefix(file)
	log.Info("Starting to parse file")

	res := Class{
		Fields:       []Field{},
		Methods:      []Method{},
		Constructors: []Constructor{},
	}

	log.Debug("Opening file: %s", file)
	filehandle, err := os.Open(file)
	if err != nil {
		log.Error("Error opening file: %v", err)
		return nil, fmt.Errorf("error opening file: %w", err)
	}

	log.Debug("Creating goquery document from file")
	doc, err := goquery.NewDocumentFromReader(filehandle)
	if err != nil {
		log.Error("Error parsing file: %v", err)
		return nil, fmt.Errorf("error parsing file: %w", err)
	}

	log.Debug("Looking for class name")
	class := doc.Find("div.floatright > h1")
	if class.Length() == 0 {
		log.Error("No class found in document")
		return nil, fmt.Errorf("no class found")
	}
	className := strings.TrimSpace(class.Text())
	// Clean up class name to be a valid Lua identifier
	// Merge [Client] and [Server] classes into single types
	// Replace "Player [Client]" and "Player [Server]" with "Player"
	// Replace "Alliance [Client]" and "Alliance [Server]" with "Alliance"

	// Remove inheritance part (after ":") - keep only the main class name
	if strings.Contains(className, " : ") {
		parts := strings.Split(className, " : ")
		if len(parts) == 2 {
			className = parts[0] // Keep only the main class name, ignore inheritance
		}
	}

	// Remove [Client] and [Server] patterns to merge classes
	className = strings.ReplaceAll(className, "[Client]", "")
	className = strings.ReplaceAll(className, "[Server]", "")

	// Remove any remaining brackets and clean up
	className = strings.ReplaceAll(className, "[", "")
	className = strings.ReplaceAll(className, "]", "")
	className = strings.ReplaceAll(className, "-", "_")
	className = strings.ReplaceAll(className, ",", "")

	// Replace spaces with underscores
	className = strings.ReplaceAll(className, " ", "_")

	// Clean up multiple underscores
	for strings.Contains(className, "__") {
		className = strings.ReplaceAll(className, "__", "_")
	}

	// Remove leading/trailing underscores
	className = strings.Trim(className, "_")

	res.ClassName = className
	log.Info("Found class: %s (cleaned from: %s)", res.ClassName, strings.TrimSpace(class.Text()))

	log.Debug("Parsing constructors")
	res.Constructors, err = getConstructors(doc, log)
	if err != nil {
		log.Error("Error getting constructors: %v", err)
		return nil, fmt.Errorf("error getting constructors: %w", err)
	}
	log.Debug("Found %d constructors", len(res.Constructors))

	log.Debug("Parsing fields")
	res.Fields, err = getFields(doc, log)
	if err != nil {
		log.Error("Error getting fields: %v", err)
		return nil, fmt.Errorf("error getting fields: %w", err)
	}
	log.Debug("Found %d fields", len(res.Fields))

	log.Debug("Parsing methods")
	res.Methods, err = getMethods(doc, log, res.ClassName)
	if err != nil {
		log.Error("Error getting methods: %v", err)
		return nil, fmt.Errorf("error getting methods: %w", err)
	}
	log.Debug("Found %d methods", len(res.Methods))

	log.Info("Parsing complete for class: %s", res.ClassName)
	logger.Dump("Final parsed class", res)
	return &res, nil
}

// TODO: Implement parsing comments for return values
// Something like "---returns (something) -> comment"
// Where "-> comment" is only shown if there's a comment
// And "---returns" only if there's a return type
// This is NOT a luals annotation because we can not use annotations on @overload
// But just a regular plain Lua comment
// TODO: Implement parsing comments for classes and constructors
func getConstructors(doc *goquery.Document, log *logger.Logger) ([]Constructor, error) {
	log.Debug("Starting constructor parsing")
	res := []Constructor{}

	codeblocks := doc.Find("div.floatright > div.codecontainer")
	log.Trace("Found %d code blocks", codeblocks.Length())
	if codeblocks.Length() == 0 {
		log.Error("No codeblocks found")
		return res, fmt.Errorf("no codeblocks found")
	}

	// The first code block should be the constructor
	// So far I have not found any classes with overloaded constructors...
	// So I can not handle that case yet
	constructorBlock := codeblocks.Eq(0)
	log.Debug("Processing first code block as constructor")

	constructor := constructorBlock.Find("div.function")
	paramTypes := constructor.Find("span.type")
	paramNames := constructor.Find("span.parameter")

	log.Trace("Found %d parameter types and %d parameter names", paramTypes.Length(), paramNames.Length())

	resConstructor := Constructor{}
	paramTypes.Each(func(i int, s *goquery.Selection) {
		pname := strings.TrimSpace(paramNames.Eq(i).Text())
		ptype := strings.TrimSpace(paramTypes.Eq(i).Text())
		ptype = MapType(ptype)

		log.Trace("Parameter %d: name='%s', type='%s'", i, pname, ptype)

		resConstructor.Params = append(resConstructor.Params, Param{
			Name:    pname,
			Type:    ptype,
			Comment: "",
		})
	})

	constructorDetails := constructorBlock.Children().Eq(1)
	constructorParameterDetails := constructorDetails.Find("span.parameter")
	log.Trace("Found %d constructor parameter details", constructorParameterDetails.Length())

	constructorParameterDetails.Each(func(i int, s *goquery.Selection) {
		param := strings.TrimSpace(s.Text())
		parameterParent := s.Parent()
		parameterDescription := parameterParent.Text()
		parameterDescription = strings.ReplaceAll(parameterDescription, fmt.Sprintf("\n%s\n", param), "")
		parameterDescription = strings.TrimSpace(parameterDescription)

		if len(resConstructor.Params) > i {
			log.Trace("Parameter %d comment: '%s'", i, parameterDescription)
			resConstructor.Params[i].Comment = parameterDescription
		}
	})

	constructorBlock.Find("div:not(.function):not(.indented) > p:not(:has(*))").Each(func(i int, s *goquery.Selection) {
		comment := strings.TrimSpace(s.Text())
		log.Trace("Constructor comment line %d: '%s'", i, comment)
		resConstructor.Comment += comment + "\n"
		resConstructor.Comment = strings.TrimSpace(resConstructor.Comment)
	})

	log.Trace("Final constructor: %+v", resConstructor)
	log.Debug("Constructor parsing complete")
	return append(res, resConstructor), nil
}

func getFields(doc *goquery.Document, log *logger.Logger) ([]Field, error) {
	log.Debug("Starting field parsing")
	res := []Field{}

	properties := doc.Find("div.floatright > div.codecontainer#Properties")
	log.Trace("Found properties container: %d elements", properties.Length())

	properties.ChildrenFiltered("div").Each(func(i int, s *goquery.Selection) {
		property := Field{}
		property.Name = strings.TrimSpace(s.Find("span.property").Text())
		property.Type = strings.TrimSpace(s.Find("span.type").Text())
		property.Type = MapType(property.Type)
		comment := s.Find("td[align='right']").Text()
		if comment != "" {
			property.Comment = strings.TrimSpace(comment)
		}

		log.Trace("Field %d: name='%s', type='%s', comment='%s'", i, property.Name, property.Type, property.Comment)
		res = append(res, property)
	})

	log.Debug("Found %d fields", len(res))
	return res, nil
}

func getMethods(doc *goquery.Document, log *logger.Logger, className string) ([]Method, error) {
	log.Debug("Starting method parsing")
	res := []Method{}

	codeblocks := doc.Find("div.floatright > div.codecontainer")
	log.Trace("Found %d code blocks for method parsing", codeblocks.Length())

	codeblocks.Each(func(blockIndex int, codeblock *goquery.Selection) {
		functionDiv := codeblock.Find("div.function")
		if functionDiv.Length() == 0 {
			return
		}

		method := Method{}
		method.Name = strings.TrimSpace(functionDiv.AttrOr("id", ""))
		method.Comment = strings.TrimSpace(functionDiv.Find("span.comment").Text())

		log.Trace("Processing method %d: name='%s', comment='%s'", blockIndex, method.Name, method.Comment)

		// Parse parameters
		types := functionDiv.Find("span.type")
		parameters := functionDiv.Find("span.parameter")
		log.Trace("Method %s has %d types and %d parameters", method.Name, types.Length(), parameters.Length())

		types.Each(func(i int, s *goquery.Selection) {
			param := Param{}
			param.Name = strings.TrimSpace(parameters.Eq(i).Text())

			// Skip parameters with empty names
			if param.Name == "" {
				log.Trace("Method %s parameter %d has empty name, skipping", method.Name, i)
				return
			}

			if IsReservedKeyword(param.Name) {
				log.Trace("Parameter name '%s' is reserved keyword, prefixing with __", param.Name)
				param.Name = fmt.Sprintf("__%s", param.Name)
			}

			// Get the type text and handle cases where it might be split across lines
			typeText := strings.TrimSpace(types.Eq(i).Text())
			// Replace newlines and multiple spaces with single spaces
			typeText = strings.ReplaceAll(typeText, "\n", " ")
			typeText = strings.ReplaceAll(typeText, "\r", " ")
			// Replace multiple spaces with single space
			for strings.Contains(typeText, "  ") {
				typeText = strings.ReplaceAll(typeText, "  ", " ")
			}
			typeText = strings.TrimSpace(typeText)

			param.Type = MapType(typeText)

			log.Trace("Method %s parameter %d: name='%s', type='%s'", method.Name, i, param.Name, param.Type)
			method.Params = append(method.Params, param)
		})

		// Parse return values
		// First, try to get return type from function signature
		functionText := functionDiv.Text()
		if strings.Contains(functionText, "function") {
			// Extract return types from function signature
			// Look for patterns like "function var", "function int...", "function Matrix, int..."
			lines := strings.Split(functionText, "\n")
			for _, line := range lines {
				line = strings.TrimSpace(line)
				if strings.HasPrefix(line, "function") {
					// Extract everything after "function" until the function name
					functionPart := strings.TrimPrefix(line, "function")
					// Find the function name (usually ends with "(" or " ")
					funcNameIndex := strings.Index(functionPart, "(")
					if funcNameIndex == -1 {
						funcNameIndex = strings.Index(functionPart, " ")
					}
					if funcNameIndex != -1 {
						returnTypesPart := strings.TrimSpace(functionPart[:funcNameIndex])
						// Remove the function name from the return types part
						// The function name is the last word in the return types part
						words := strings.Fields(returnTypesPart)
						if len(words) > 1 {
							// Remove the last word (function name) and join the rest
							returnTypesPart = strings.Join(words[:len(words)-1], " ")
						}

						// Handle complex return types like "table<int, string>" and multiple returns like "Matrix, int..."
						// We need to be careful about commas inside angle brackets
						var returnTypes []string
						var currentType strings.Builder
						bracketDepth := 0

						for i := 0; i < len(returnTypesPart); i++ {
							char := returnTypesPart[i]
							if char == '<' {
								bracketDepth++
								currentType.WriteByte(char)
							} else if char == '>' {
								bracketDepth--
								currentType.WriteByte(char)
							} else if char == ',' && bracketDepth == 0 {
								// Only split on commas that are not inside angle brackets
								returnTypes = append(returnTypes, strings.TrimSpace(currentType.String()))
								currentType.Reset()
							} else {
								currentType.WriteByte(char)
							}
						}
						// Add the last type
						if currentType.Len() > 0 {
							returnTypes = append(returnTypes, strings.TrimSpace(currentType.String()))
						}

						for _, returnType := range returnTypes {
							if returnType == "" {
								continue
							}

							// Handle cases like "int..." (multiple return values)
							if strings.HasSuffix(returnType, "...") {
								returnType = strings.TrimSuffix(returnType, "...")
								returnType = MapType(returnType)
								method.Returns = append(method.Returns, Return{
									Type:    returnType,
									Comment: "multiple return values",
								})
							} else {
								returnType = MapType(returnType)
								method.Returns = append(method.Returns, Return{
									Type:    returnType,
									Comment: "",
								})
							}
						}
					}
					break
				}
			}
		}

		// Parse return documentation from the second div
		detailsDiv := codeblock.Find("div:not(.function)")
		if detailsDiv.Length() > 0 {
			// Look for "Returns" section
			returnsHeader := detailsDiv.Find("p:contains('Returns')")
			if returnsHeader.Length() > 0 {
				// Get the indented content after the Returns header
				indentedContent := detailsDiv.Find("div.indented p")
				indentedContent.Each(func(i int, s *goquery.Selection) {
					comment := strings.TrimSpace(s.Text())
					if comment != "" {
						// If we have return types but no comments yet, add this as a comment
						if len(method.Returns) > i {
							method.Returns[i].Comment = comment
						} else if len(method.Returns) == 0 {
							// If no return types were found in signature, create a return with just comment
							method.Returns = append(method.Returns, Return{
								Type:    "any",
								Comment: comment,
							})
						}
					}
				})
			}
		}

		// If this is a constructor (method name matches class name or is contained in the original class name), ensure it returns the correct type
		// Handle cases where method name is "Alliance" but class name is "Alliance_Client"
		originalClassName := strings.TrimSpace(doc.Find("div.floatright > h1").Text())
		if method.Name == className || method.Name == originalClassName || strings.Contains(originalClassName, method.Name) {
			// Override the return type to be the cleaned class name
			if len(method.Returns) > 0 {
				method.Returns[0].Type = className
				method.Returns[0].Comment = "A new instance of " + className
			} else {
				method.Returns = []Return{
					{
						Type:    className,
						Comment: "A new instance of " + className,
					},
				}
			}
		}

		// Determine availability based on the original class name
		availability := determineAvailability(originalClassName)
		if availability != "" {
			method.Comment = availability + " " + method.Comment
		}

		log.Trace("Method %s has %d parameters and %d return values", method.Name, len(method.Params), len(method.Returns))
		res = append(res, method)
	})

	log.Debug("Found %d methods", len(res))
	return res, nil
}

// determineAvailability returns a comment indicating whether a method is available on client, server, or both
func determineAvailability(originalClassName string) string {
	if strings.Contains(originalClassName, "[Client]") && strings.Contains(originalClassName, "[Server]") {
		return "[Client/Server]"
	} else if strings.Contains(originalClassName, "[Client]") {
		return "[Client]"
	} else if strings.Contains(originalClassName, "[Server]") {
		return "[Server]"
	}
	return ""
}
