package extgen

import (
	"bufio"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"regexp"
	"strings"
)

var phpClassRegex = regexp.MustCompile(`//\s*export_php:class\s+(\w+)`)
var phpMethodRegex = regexp.MustCompile(`//\s*export_php:method\s+(\w+)::([^{}\n]+)(?:\s*{\s*})?`)
var methodSignatureRegex = regexp.MustCompile(`(\w+)\s*\(([^)]*)\)\s*:\s*(\??[\w|]+)`)
var methodParamTypeNameRegex = regexp.MustCompile(`(\??[\w|]+)\s+\$?(\w+)`)

type exportDirective struct {
	line      int
	className string
}

type classParser struct{}

func (cp *classParser) Parse(filename string) ([]phpClass, error) {
	return cp.parse(filename)
}

func (cp *classParser) parse(filename string) (classes []phpClass, err error) {
	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
	if err != nil {
		return nil, fmt.Errorf("parsing file: %w", err)
	}

	validator := Validator{}

	exportDirectives := cp.collectExportDirectives(node, fset)
	methods, err := cp.parseMethods(filename)
	if err != nil {
		return nil, fmt.Errorf("parsing methods: %w", err)
	}

	// match structs to directives
	matchedDirectives := make(map[int]bool)

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

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

			var structType *ast.StructType
			if structType, ok = typeSpec.Type.(*ast.StructType); !ok {
				continue
			}

			var phpCl string
			var directiveLine int
			if phpCl, directiveLine = cp.extractPHPClassCommentWithLine(genDecl.Doc, fset); phpCl == "" {
				continue
			}

			matchedDirectives[directiveLine] = true

			class := phpClass{
				Name:     phpCl,
				GoStruct: typeSpec.Name.Name,
			}

			class.Properties = cp.parseStructFields(structType.Fields.List)

			// associate methods with this class
			for _, method := range methods {
				if method.ClassName == phpCl {
					class.Methods = append(class.Methods, method)
				}
			}

			if err := validator.validateClass(class); err != nil {
				fmt.Printf("Warning: Invalid class '%s': %v\n", class.Name, err)
				continue
			}

			classes = append(classes, class)
		}
	}

	for _, directive := range exportDirectives {
		if !matchedDirectives[directive.line] {
			return nil, fmt.Errorf("//export_php class directive at line %d is not followed by a struct declaration", directive.line)
		}
	}

	return classes, nil
}

func (cp *classParser) collectExportDirectives(node *ast.File, fset *token.FileSet) []exportDirective {
	var directives []exportDirective

	for _, commentGroup := range node.Comments {
		for _, comment := range commentGroup.List {
			if matches := phpClassRegex.FindStringSubmatch(comment.Text); matches != nil {
				pos := fset.Position(comment.Pos())
				directives = append(directives, exportDirective{
					line:      pos.Line,
					className: matches[1],
				})
			}
		}
	}

	return directives
}

func (cp *classParser) extractPHPClassCommentWithLine(commentGroup *ast.CommentGroup, fset *token.FileSet) (string, int) {
	if commentGroup == nil {
		return "", 0
	}

	for _, comment := range commentGroup.List {
		if matches := phpClassRegex.FindStringSubmatch(comment.Text); matches != nil {
			pos := fset.Position(comment.Pos())
			return matches[1], pos.Line
		}
	}

	return "", 0
}

func (cp *classParser) parseStructFields(fields []*ast.Field) []phpClassProperty {
	var properties []phpClassProperty

	for _, field := range fields {
		for _, name := range field.Names {
			prop := cp.parseStructField(name.Name, field)
			properties = append(properties, prop)
		}
	}

	return properties
}

func (cp *classParser) parseStructField(fieldName string, field *ast.Field) phpClassProperty {
	prop := phpClassProperty{Name: fieldName}

	// check if field is a pointer (nullable)
	if starExpr, isPointer := field.Type.(*ast.StarExpr); isPointer {
		prop.IsNullable = true
		prop.GoType = cp.typeToString(starExpr.X)
	} else {
		prop.IsNullable = false
		prop.GoType = cp.typeToString(field.Type)
	}

	prop.PhpType = cp.goTypeToPHPType(prop.GoType)

	return prop
}

func (cp *classParser) typeToString(expr ast.Expr) string {
	switch t := expr.(type) {
	case *ast.Ident:
		return t.Name
	case *ast.StarExpr:
		return "*" + cp.typeToString(t.X)
	case *ast.ArrayType:
		return "[]" + cp.typeToString(t.Elt)
	case *ast.MapType:
		return "map[" + cp.typeToString(t.Key) + "]" + cp.typeToString(t.Value)
	default:
		return "any"
	}
}

var goToPhpTypeMap = map[string]phpType{
	"string": phpString,
	"int":    phpInt, "int64": phpInt, "int32": phpInt, "int16": phpInt, "int8": phpInt,
	"uint": phpInt, "uint64": phpInt, "uint32": phpInt, "uint16": phpInt, "uint8": phpInt,
	"float64": phpFloat, "float32": phpFloat,
	"bool": phpBool,
	"any":  phpMixed,
}

func (cp *classParser) goTypeToPHPType(goType string) phpType {
	goType = strings.TrimPrefix(goType, "*")

	if phpType, exists := goToPhpTypeMap[goType]; exists {
		return phpType
	}

	if strings.HasPrefix(goType, "[]") || strings.HasPrefix(goType, "map[") {
		return phpArray
	}

	return phpMixed
}

func (cp *classParser) parseMethods(filename string) (methods []phpClassMethod, err error) {
	file, err := os.Open(filename)
	if err != nil {
		return nil, err
	}

	defer func() {
		e := file.Close()
		if err != nil {
			err = e
		}
	}()

	scanner := bufio.NewScanner(file)
	var currentMethod *phpClassMethod

	lineNumber := 0
	for scanner.Scan() {
		lineNumber++
		line := strings.TrimSpace(scanner.Text())

		if matches := phpMethodRegex.FindStringSubmatch(line); matches != nil {
			className := strings.TrimSpace(matches[1])
			signature := strings.TrimSpace(matches[2])

			method, err := cp.parseMethodSignature(className, signature)
			if err != nil {
				fmt.Printf("Warning: Error parsing method signature %q: %v\n", signature, err)

				continue
			}

			validator := Validator{}
			phpFunc := phpFunction{
				Name:             method.Name,
				Signature:        method.Signature,
				Params:           method.Params,
				ReturnType:       method.ReturnType,
				IsReturnNullable: method.isReturnNullable,
			}

			if err := validator.validateTypes(phpFunc); err != nil {
				fmt.Printf("Warning: Method \"%s::%s\" uses unsupported types: %v\n", className, method.Name, err)

				continue
			}

			method.lineNumber = lineNumber
			currentMethod = method
		}

		if currentMethod != nil && strings.HasPrefix(line, "func ") {
			goFunc, err := cp.extractGoMethodFunction(scanner, line)
			if err != nil {
				return nil, fmt.Errorf("extracting Go method function: %w", err)
			}

			currentMethod.GoFunction = goFunc

			validator := Validator{}
			phpFunc := phpFunction{
				Name:             currentMethod.Name,
				Signature:        currentMethod.Signature,
				GoFunction:       currentMethod.GoFunction,
				Params:           currentMethod.Params,
				ReturnType:       currentMethod.ReturnType,
				IsReturnNullable: currentMethod.isReturnNullable,
			}

			if err := validator.validateGoFunctionSignatureWithOptions(phpFunc, true); err != nil {
				fmt.Printf("Warning: Go method signature mismatch for '%s::%s': %v\n", currentMethod.ClassName, currentMethod.Name, err)
				currentMethod = nil
				continue
			}

			methods = append(methods, *currentMethod)
			currentMethod = nil
		}
	}

	if currentMethod != nil {
		return nil, fmt.Errorf("//export_php:method directive at line %d is not followed by a function declaration", currentMethod.lineNumber)
	}

	return methods, scanner.Err()
}

func (cp *classParser) parseMethodSignature(className, signature string) (*phpClassMethod, error) {
	matches := methodSignatureRegex.FindStringSubmatch(signature)

	if len(matches) != 4 {
		return nil, fmt.Errorf("invalid method signature format")
	}

	methodName := matches[1]
	paramsStr := strings.TrimSpace(matches[2])
	returnTypeStr := strings.TrimSpace(matches[3])

	isReturnNullable := strings.HasPrefix(returnTypeStr, "?")
	returnType := strings.TrimPrefix(returnTypeStr, "?")

	var params []phpParameter
	if paramsStr != "" {
		paramParts := strings.SplitSeq(paramsStr, ",")
		for part := range paramParts {
			param, err := cp.parseMethodParameter(strings.TrimSpace(part))
			if err != nil {
				return nil, fmt.Errorf("parsing parameter '%s': %w", part, err)
			}

			params = append(params, param)
		}
	}

	return &phpClassMethod{
		Name:             methodName,
		PhpName:          methodName,
		ClassName:        className,
		Signature:        signature,
		Params:           params,
		ReturnType:       phpType(returnType),
		isReturnNullable: isReturnNullable,
	}, nil
}

func (cp *classParser) parseMethodParameter(paramStr string) (phpParameter, error) {
	parts := strings.Split(paramStr, "=")
	typePart := strings.TrimSpace(parts[0])

	param := phpParameter{HasDefault: len(parts) > 1}

	if param.HasDefault {
		param.DefaultValue = cp.sanitizeDefaultValue(strings.TrimSpace(parts[1]))
	}

	matches := methodParamTypeNameRegex.FindStringSubmatch(typePart)

	if len(matches) < 3 {
		return phpParameter{}, fmt.Errorf("invalid parameter format: %s", paramStr)
	}

	typeStr := strings.TrimSpace(matches[1])
	param.Name = strings.TrimSpace(matches[2])
	param.IsNullable = strings.HasPrefix(typeStr, "?")
	param.PhpType = phpType(strings.TrimPrefix(typeStr, "?"))

	return param, nil
}

func (cp *classParser) sanitizeDefaultValue(value string) string {
	if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
		return value
	}

	if strings.ToLower(value) == "null" {
		return "null"
	}

	return strings.Trim(value, `'"`)
}

func (cp *classParser) extractGoMethodFunction(scanner *bufio.Scanner, firstLine string) (string, error) {
	goFunc := firstLine + "\n"
	braceCount := 1

	for scanner.Scan() {
		line := scanner.Text()
		goFunc += line + "\n"

		for _, char := range line {
			switch char {
			case '{':
				braceCount++
			case '}':
				braceCount--
			}
		}

		if braceCount == 0 {
			break
		}
	}

	return goFunc, nil
}
