package graphql

import (
	"fmt"
	"os"
	"sort"
	"strings"

	"git.warky.dev/wdevs/relspecgo/pkg/models"
	"git.warky.dev/wdevs/relspecgo/pkg/writers"
)

type Writer struct {
	options *writers.WriterOptions
}

func NewWriter(options *writers.WriterOptions) *Writer {
	return &Writer{
		options: options,
	}
}

func (w *Writer) WriteDatabase(db *models.Database) error {
	content := w.databaseToGraphQL(db)

	if w.options.OutputPath != "" {
		return os.WriteFile(w.options.OutputPath, []byte(content), 0o644)
	}

	fmt.Print(content)
	return nil
}

func (w *Writer) WriteSchema(schema *models.Schema) error {
	db := models.InitDatabase(schema.Name)
	db.Schemas = []*models.Schema{schema}
	return w.WriteDatabase(db)
}

func (w *Writer) WriteTable(table *models.Table) error {
	schema := models.InitSchema(table.Schema)
	schema.Tables = []*models.Table{table}
	db := models.InitDatabase(schema.Name)
	db.Schemas = []*models.Schema{schema}
	return w.WriteDatabase(db)
}

func (w *Writer) databaseToGraphQL(db *models.Database) string {
	var sb strings.Builder

	// Header comment
	if w.shouldIncludeComments() {
		sb.WriteString("# Generated GraphQL Schema\n")
		if db.Name != "" {
			fmt.Fprintf(&sb, "# Database: %s\n", db.Name)
		}
		sb.WriteString("\n")
	}

	// Custom scalar declarations
	if w.shouldIncludeScalarDeclarations() {
		scalars := w.collectCustomScalars(db)
		if len(scalars) > 0 {
			for _, scalar := range scalars {
				fmt.Fprintf(&sb, "scalar %s\n", scalar)
			}
			sb.WriteString("\n")
		}
	}

	// Enum definitions
	for _, schema := range db.Schemas {
		for _, enum := range schema.Enums {
			sb.WriteString(w.enumToGraphQL(enum))
			sb.WriteString("\n")
		}
	}

	// Type definitions
	for _, schema := range db.Schemas {
		for _, table := range schema.Tables {
			// Skip join tables (tables with only PK+FK columns)
			if w.isJoinTable(table) {
				continue
			}

			sb.WriteString(w.tableToGraphQL(table, db, schema))
			sb.WriteString("\n")
		}
	}

	return sb.String()
}

func (w *Writer) shouldIncludeComments() bool {
	if w.options.Metadata != nil {
		if include, ok := w.options.Metadata["includeComments"].(bool); ok {
			return include
		}
	}
	return true // Default to true
}

func (w *Writer) shouldIncludeScalarDeclarations() bool {
	if w.options.Metadata != nil {
		if include, ok := w.options.Metadata["includeScalarDeclarations"].(bool); ok {
			return include
		}
	}
	return true // Default to true
}

func (w *Writer) collectCustomScalars(db *models.Database) []string {
	scalarsNeeded := make(map[string]bool)

	for _, schema := range db.Schemas {
		for _, table := range schema.Tables {
			for _, col := range table.Columns {
				if scalar := w.sqlTypeToCustomScalar(col.Type); scalar != "" {
					scalarsNeeded[scalar] = true
				}
			}
		}
	}

	// Convert to sorted slice
	scalars := make([]string, 0, len(scalarsNeeded))
	for scalar := range scalarsNeeded {
		scalars = append(scalars, scalar)
	}
	sort.Strings(scalars)

	return scalars
}

func (w *Writer) isJoinTable(table *models.Table) bool {
	// A join table typically has:
	// 1. Exactly 2 FK constraints
	// 2. Composite primary key on those FK columns
	// 3. No other columns

	fkCount := 0
	for _, constraint := range table.Constraints {
		if constraint.Type == models.ForeignKeyConstraint {
			fkCount++
		}
	}

	if fkCount != 2 {
		return false
	}

	// Check if all columns are either PKs or FKs
	for _, col := range table.Columns {
		isFKColumn := false
		for _, constraint := range table.Constraints {
			if constraint.Type == models.ForeignKeyConstraint {
				for _, fkCol := range constraint.Columns {
					if fkCol == col.Name {
						isFKColumn = true
						break
					}
				}
			}
		}

		if !isFKColumn && !col.IsPrimaryKey {
			// Found a column that's neither PK nor FK
			return false
		}
	}

	return true
}

func (w *Writer) enumToGraphQL(enum *models.Enum) string {
	var sb strings.Builder

	fmt.Fprintf(&sb, "enum %s {\n", enum.Name)
	for _, value := range enum.Values {
		fmt.Fprintf(&sb, "  %s\n", value)
	}
	sb.WriteString("}\n")

	return sb.String()
}

func (w *Writer) tableToGraphQL(table *models.Table, db *models.Database, schema *models.Schema) string {
	var sb strings.Builder

	// Type name
	typeName := table.Name

	// Description comment
	if w.shouldIncludeComments() && (table.Description != "" || table.Comment != "") {
		desc := table.Description
		if desc == "" {
			desc = table.Comment
		}
		fmt.Fprintf(&sb, "# %s\n", desc)
	}

	fmt.Fprintf(&sb, "type %s {\n", typeName)

	// Collect and categorize fields
	var idFields, scalarFields, relationFields []string

	for _, column := range table.Columns {
		// Skip FK columns (they become relation fields)
		if w.isForeignKeyColumn(column, table) {
			continue
		}

		gqlType := w.sqlTypeToGraphQL(column.Type, column, table, schema)
		if gqlType == "" {
			continue // Skip if type couldn't be mapped
		}

		// Determine nullability
		if column.NotNull {
			gqlType += "!"
		}

		field := fmt.Sprintf("  %s: %s", column.Name, gqlType)

		if column.IsPrimaryKey {
			idFields = append(idFields, field)
		} else {
			scalarFields = append(scalarFields, field)
		}
	}

	// Add relation fields
	relationFields = w.generateRelationFields(table, db, schema)

	// Write fields in order: ID (sorted), scalars (sorted), relations (sorted)
	sort.Strings(idFields)
	for _, field := range idFields {
		sb.WriteString(field + "\n")
	}

	sort.Strings(scalarFields)
	for _, field := range scalarFields {
		sb.WriteString(field + "\n")
	}

	if len(relationFields) > 0 {
		if len(scalarFields) > 0 || len(idFields) > 0 {
			sb.WriteString("\n") // Blank line before relations
		}
		sort.Strings(relationFields)
		for _, field := range relationFields {
			sb.WriteString(field + "\n")
		}
	}

	sb.WriteString("}\n")

	return sb.String()
}

func (w *Writer) isForeignKeyColumn(column *models.Column, table *models.Table) bool {
	for _, constraint := range table.Constraints {
		if constraint.Type == models.ForeignKeyConstraint {
			for _, fkCol := range constraint.Columns {
				if fkCol == column.Name {
					return true
				}
			}
		}
	}
	return false
}
