// Copyright 2026 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package golang

import (
	"go/format"
	"sort"
	"strconv"
	"strings"

	highbase "github.com/pb33f/libopenapi/datamodel/high/base"
)

var formatSource = format.Source

func (g *Generator) renderFile(irs []*SchemaIR) (*GeneratedFile, error) {
	if err := validatePackageName(g.packageName); err != nil {
		return nil, err
	}
	g.imports = make(map[string]struct{})
	g.decls = nil
	g.seenDecls = make(map[string]struct{})
	g.metadataSchemas = make(map[string]*highbase.Schema)
	g.metadataOrder = nil
	types := make([]*GeneratedType, 0, len(irs))
	for _, ir := range irs {
		if ir == nil {
			continue
		}
		g.renderDecl(ir)
		types = append(types, &GeneratedType{Name: ir.Name, Kind: ir.Kind})
	}
	metadataSource, err := g.renderSchemaMetadataSource()
	if err != nil {
		return nil, err
	}
	var b strings.Builder
	if g.generatedComment {
		b.WriteString("// Code generated by libopenapi generator/golang. DO NOT EDIT.\n")
	}
	if g.headerComment != "" {
		writeLineCommentBlock(&b, g.headerComment)
	}
	if g.generatedComment || g.headerComment != "" {
		b.WriteByte('\n')
	}
	if g.packageComment != "" {
		b.WriteString("// Package ")
		b.WriteString(g.packageName)
		b.WriteByte(' ')
		b.WriteString(strings.TrimSpace(g.packageComment))
		if !strings.HasSuffix(strings.TrimSpace(g.packageComment), ".") {
			b.WriteByte('.')
		}
		b.WriteByte('\n')
	}
	b.WriteString("package ")
	b.WriteString(g.packageName)
	b.WriteString("\n\n")
	g.writeImports(&b)
	for _, decl := range g.decls {
		b.WriteString(decl)
		b.WriteByte('\n')
	}
	src, err := formatSource([]byte(b.String()))
	if err != nil {
		return nil, err
	}
	return &GeneratedFile{
		PackageName:    g.packageName,
		Source:         src,
		SchemaMetadata: metadataSource,
		Types:          types,
		Diagnostics:    append([]Diagnostic(nil), g.diagnostics...),
	}, nil
}

func (g *Generator) renderSchemaMetadataSource() (*GeneratedSourceFile, error) {
	decl := g.renderSchemaMetadataSidecarDecl()
	if decl == "" {
		return nil, nil
	}
	var b strings.Builder
	if g.generatedComment {
		b.WriteString("// Code generated by libopenapi generator/golang. DO NOT EDIT.\n")
	}
	if g.headerComment != "" {
		writeLineCommentBlock(&b, g.headerComment)
	}
	if g.generatedComment || g.headerComment != "" {
		b.WriteByte('\n')
	}
	b.WriteString("package ")
	b.WriteString(g.packageName)
	b.WriteString("\n\n")
	b.WriteString(decl)
	b.WriteByte('\n')
	src, err := formatSource([]byte(b.String()))
	if err != nil {
		return nil, err
	}
	return &GeneratedSourceFile{Name: SchemaMetadataFileName, Source: src}, nil
}

func (g *Generator) writeImports(b *strings.Builder) {
	if len(g.imports) == 0 {
		return
	}
	imports := make([]string, 0, len(g.imports))
	for path := range g.imports {
		imports = append(imports, path)
	}
	sort.Strings(imports)
	if len(imports) == 1 {
		b.WriteString("import ")
		b.WriteString(strconv.Quote(imports[0]))
		b.WriteString("\n\n")
		return
	}
	b.WriteString("import (\n")
	for _, path := range imports {
		b.WriteByte('\t')
		b.WriteString(strconv.Quote(path))
		b.WriteByte('\n')
	}
	b.WriteString(")\n\n")
}

func (g *Generator) renderDecl(ir *SchemaIR) {
	if ir == nil || ir.Name == "" || ir.Kind == KindRef {
		return
	}
	switch ir.Kind {
	case KindUnion:
		g.renderUnionDecl(ir)
	case KindObject, KindAllOf:
		if shouldRenderObjectAlias(ir) {
			g.renderAliasDecl(ir)
			return
		}
		g.renderObjectDecl(ir)
	case KindEnum:
		g.renderEnumDecl(ir)
	default:
		g.renderAliasDecl(ir)
	}
}

func (g *Generator) rememberDecl(name string) bool {
	if name == "" {
		return false
	}
	if _, ok := g.seenDecls[name]; ok {
		return false
	}
	g.seenDecls[name] = struct{}{}
	return true
}

func (g *Generator) renderObjectDecl(ir *SchemaIR) {
	if !g.rememberDecl(ir.Name) {
		return
	}
	g.renderChildren(ir)
	var b strings.Builder
	writeIRComments(&b, ir)
	b.WriteString("type ")
	b.WriteString(ir.Name)
	b.WriteString(" struct {\n")
	fields := newNameRegistry()
	additionalFieldName := "AdditionalProperties"
	if ir.AllOf != nil {
		for _, embed := range ir.AllOf {
			if embed != nil && embed.Kind == KindRef {
				b.WriteByte('\t')
				b.WriteString(g.goType(embed, true, false))
				b.WriteByte('\n')
			}
		}
	}
	if ir.Properties != nil {
		for propName, prop := range ir.Properties.FromOldest() {
			required := isRequired(ir, propName)
			fieldName, collision := fields.resolve(propName, g.fieldName(propName))
			if collision {
				g.addDiagnostic(DiagnosticFieldNameCollision, ir.Name+"."+propName, "field name collision resolved as "+fieldName)
			}
			fieldType := g.goType(prop, required, true)
			writeFieldComments(&b, fieldName, prop)
			b.WriteByte('\t')
			b.WriteString(fieldName)
			b.WriteByte(' ')
			b.WriteString(fieldType)
			if tag := tagLiteral(propName, required, g.jsonTags, g.yamlTags, g.omitEmpty, g.openAPITagLiteral(prop, fieldType)); tag != "" {
				b.WriteByte(' ')
				b.WriteString(tag)
			}
			b.WriteByte('\n')
		}
	}
	var additionalValueType string
	if ir.AdditionalProperties != nil {
		additionalValueType = g.goType(ir.AdditionalProperties, true, false)
		var collision bool
		additionalFieldName, collision = fields.resolve("$additionalProperties", "AdditionalProperties")
		if collision {
			g.addDiagnostic(DiagnosticFieldNameCollision, ir.Name+".additionalProperties", "additionalProperties field name collision resolved as "+additionalFieldName)
		}
		b.WriteByte('\t')
		b.WriteString(additionalFieldName)
		b.WriteString(" map[string]")
		b.WriteString(additionalValueType)
		b.WriteString(" `json:\"-\"`\n")
	}
	b.WriteString("}\n")
	if ir.AdditionalProperties != nil && g.additionalPropertiesMethods {
		g.addImport("encoding/json")
		writeAdditionalPropertiesMethods(&b, ir, additionalFieldName, additionalValueType)
	}
	g.decls = append(g.decls, b.String())
	g.recordSchemaMetadata(ir.Name, ir.SourceSchema)
}

func (g *Generator) renderChildren(ir *SchemaIR) {
	if ir.Properties != nil {
		for _, prop := range ir.Properties.FromOldest() {
			g.renderNested(prop)
		}
	}
	if ir.PatternProperties != nil {
		for _, prop := range ir.PatternProperties.FromOldest() {
			g.renderNested(prop)
		}
	}
	if ir.Items != nil {
		g.renderNested(ir.Items)
	}
	for _, item := range ir.PrefixItems {
		g.renderNested(item)
	}
	if ir.AdditionalProperties != nil {
		g.renderNested(ir.AdditionalProperties)
	}
	for _, child := range ir.AllOf {
		g.renderNested(child)
	}
}

func (g *Generator) renderNested(ir *SchemaIR) {
	if ir == nil {
		return
	}
	switch ir.Kind {
	case KindObject, KindAllOf, KindUnion, KindEnum:
		if ir.Name != "" {
			g.renderDecl(ir)
		}
	case KindArray:
		g.renderNested(ir.Items)
	case KindMap:
		g.renderNested(ir.AdditionalProperties)
	}
}

func (g *Generator) renderAliasDecl(ir *SchemaIR) {
	if !g.rememberDecl(ir.Name) {
		return
	}
	g.renderChildren(ir)
	var b strings.Builder
	writeIRComments(&b, ir)
	b.WriteString("type ")
	b.WriteString(ir.Name)
	b.WriteByte(' ')
	b.WriteString(g.goType(ir, true, false))
	b.WriteByte('\n')
	g.decls = append(g.decls, b.String())
	g.recordSchemaMetadata(ir.Name, ir.SourceSchema)
}

func shouldRenderObjectAlias(ir *SchemaIR) bool {
	return ir != nil &&
		ir.Kind == KindObject &&
		(ir.Properties == nil || ir.Properties.Len() == 0) &&
		(ir.PatternProperties == nil || ir.PatternProperties.Len() == 0) &&
		len(ir.AllOf) == 0 &&
		ir.AdditionalProperties == nil &&
		(ir.AdditionalAllowed == nil || *ir.AdditionalAllowed)
}

func (g *Generator) renderEnumDecl(ir *SchemaIR) {
	if !g.rememberDecl(ir.Name) {
		return
	}
	var b strings.Builder
	writeIRComments(&b, ir)
	b.WriteString("type ")
	b.WriteString(ir.Name)
	b.WriteByte(' ')
	shape := enumShapeFor(ir.Enum)
	baseType := shape.goType
	b.WriteString(baseType)
	b.WriteByte('\n')
	if g.enumConstants && shape.constants {
		b.WriteString("\nconst (\n")
		used := make(map[string]struct{})
		for _, node := range ir.Enum {
			literal := enumLiteral(node, baseType)
			if literal == "" {
				continue
			}
			name := uniqueName(ir.Name+g.enumValueName(node.Value), used)
			b.WriteByte('\t')
			b.WriteString(name)
			b.WriteByte(' ')
			b.WriteString(ir.Name)
			b.WriteString(" = ")
			b.WriteString(literal)
			b.WriteByte('\n')
		}
		b.WriteString(")\n")
	}
	g.decls = append(g.decls, b.String())
	g.recordSchemaMetadata(ir.Name, ir.SourceSchema)
}

func writeAdditionalPropertiesMethods(b *strings.Builder, ir *SchemaIR, fieldName, valueType string) {
	b.WriteString("\nfunc (m *")
	b.WriteString(ir.Name)
	b.WriteString(") UnmarshalJSON(data []byte) error {\n")
	b.WriteString("\ttype Alias ")
	b.WriteString(ir.Name)
	b.WriteString("\n\tvar known Alias\n\tif err := json.Unmarshal(data, &known); err != nil {\n\t\treturn err\n\t}\n\t*m = ")
	b.WriteString(ir.Name)
	b.WriteString("(known)\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n")
	if ir.Properties != nil {
		for propName := range ir.Properties.FromOldest() {
			b.WriteString("\tdelete(raw, ")
			b.WriteString(strconv.Quote(propName))
			b.WriteString(")\n")
		}
	}
	b.WriteString("\tif len(raw) == 0 {\n\t\treturn nil\n\t}\n\tm.")
	b.WriteString(fieldName)
	b.WriteString(" = make(map[string]")
	b.WriteString(valueType)
	b.WriteString(", len(raw))\n\tfor key, value := range raw {\n\t\tvar decoded ")
	b.WriteString(valueType)
	b.WriteString("\n\t\tif err := json.Unmarshal(value, &decoded); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.")
	b.WriteString(fieldName)
	b.WriteString("[key] = decoded\n\t}\n\treturn nil\n}\n\n")
	b.WriteString("func (m ")
	b.WriteString(ir.Name)
	b.WriteString(") MarshalJSON() ([]byte, error) {\n\ttype Alias ")
	b.WriteString(ir.Name)
	b.WriteString("\n\tencoded, err := json.Marshal(Alias(m))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar object map[string]json.RawMessage\n\tif err := json.Unmarshal(encoded, &object); err != nil {\n\t\treturn nil, err\n\t}\n\tfor key, value := range m.")
	b.WriteString(fieldName)
	b.WriteString(" {\n\t\tencodedValue, err := json.Marshal(value)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tobject[key] = encodedValue\n\t}\n\treturn json.Marshal(object)\n}\n")
}

func (g *Generator) goType(ir *SchemaIR, required bool, field bool) string {
	if ir == nil {
		return "any"
	}
	var typ string
	switch ir.Kind {
	case KindRef:
		typ = ir.Name
		if typ == "" {
			typ = g.refTypeName(ir.Ref)
		}
		if g.componentKinds[typ] == KindUnion {
			typ += "Union"
		}
	case KindObject, KindAllOf:
		if ir.Properties != nil && ir.Properties.Len() > 0 || len(ir.AllOf) > 0 {
			typ = ir.Name
		} else if ir.AdditionalAllowed != nil && !*ir.AdditionalAllowed && ir.Name != "" {
			typ = ir.Name
		} else if ir.AdditionalProperties != nil {
			typ = "map[string]" + g.goType(ir.AdditionalProperties, true, false)
		} else {
			typ = "map[string]any"
		}
	case KindArray:
		if len(ir.PrefixItems) > 0 {
			typ = "[]any"
		} else {
			typ = "[]" + g.goType(ir.Items, true, false)
		}
	case KindMap:
		typ = "map[string]" + g.goType(ir.AdditionalProperties, true, false)
	case KindString:
		typ = g.formatType(ir.Format, "string")
	case KindInteger:
		switch ir.Format {
		case "int32":
			typ = "int32"
		case "int64":
			typ = "int64"
		default:
			typ = "int"
		}
	case KindNumber:
		if ir.Format == "float" {
			typ = "float32"
		} else {
			typ = "float64"
		}
	case KindBoolean:
		typ = "bool"
	case KindEnum:
		if ir.Name != "" {
			typ = ir.Name
		} else {
			typ = "string"
		}
	case KindUnion:
		typ = ir.Name + "Union"
	default:
		typ = "any"
	}
	if field && shouldPointer(typ, ir, required, g.optionalFieldsAsPointers, g.nullableAsPointer) {
		return "*" + typ
	}
	return typ
}

func (g *Generator) formatType(format, fallback string) string {
	if mapping, ok := g.formatMappings[format]; ok {
		g.addImport(mapping.importPath)
		return mapping.goType
	}
	return fallback
}

func shouldPointer(typ string, ir *SchemaIR, required, optionalPointers, nullablePointer bool) bool {
	if typ == "any" || strings.HasPrefix(typ, "[]") || strings.HasPrefix(typ, "map[") {
		return false
	}
	if ir != nil && ir.Nullable && nullablePointer {
		return true
	}
	return !required && optionalPointers
}

func writeComment(b *strings.Builder, name, text string) {
	if text == "" {
		return
	}
	writeLineComment(b, name+" "+strings.TrimSpace(strings.Split(text, "\n")[0]))
}

func writeIRComments(b *strings.Builder, ir *SchemaIR) {
	if ir == nil {
		return
	}
	description := ir.Description
	if description == "" {
		description = ir.Title
	}
	writeComment(b, ir.Name, description)
	for _, comment := range ir.Comments {
		writeLineComment(b, ir.Name+" "+comment)
	}
}

func writeFieldComments(b *strings.Builder, fieldName string, ir *SchemaIR) {
	if ir == nil {
		return
	}
	description := ir.Description
	if description == "" {
		description = ir.Title
	}
	writeComment(b, fieldName, description)
	for _, comment := range ir.Comments {
		writeLineComment(b, fieldName+" "+comment)
	}
}

func writeLineCommentBlock(b *strings.Builder, text string) {
	for _, line := range strings.Split(strings.TrimSpace(text), "\n") {
		writeLineComment(b, strings.TrimSpace(line))
	}
}

func writeLineComment(b *strings.Builder, line string) {
	if line == "" {
		return
	}
	b.WriteString("// ")
	b.WriteString(line)
	if !strings.HasSuffix(line, ".") {
		b.WriteByte('.')
	}
	b.WriteByte('\n')
}
