package dbml

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

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

// Writer implements the writers.Writer interface for DBML format
type Writer struct {
	options *writers.WriterOptions
}

// NewWriter creates a new DBML writer with the given options
func NewWriter(options *writers.WriterOptions) *Writer {
	return &Writer{
		options: options,
	}
}

// WriteDatabase writes a Database model to DBML format
func (w *Writer) WriteDatabase(db *models.Database) error {
	content := w.databaseToDBML(db)

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

	fmt.Print(content)
	return nil
}

// WriteSchema writes a Schema model to DBML format
func (w *Writer) WriteSchema(schema *models.Schema) error {
	content := w.schemaToDBML(schema)

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

	fmt.Print(content)
	return nil
}

// WriteTable writes a Table model to DBML format
func (w *Writer) WriteTable(table *models.Table) error {
	content := w.tableToDBML(table)

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

	fmt.Print(content)
	return nil
}

// databaseToDBML converts a Database to DBML format string
func (w *Writer) databaseToDBML(d *models.Database) string {
	var sb strings.Builder

	if d.Description != "" {
		fmt.Fprintf(&sb, "// %s\n", d.Description)
	}
	if d.Comment != "" {
		fmt.Fprintf(&sb, "// %s\n", d.Comment)
	}
	if d.Description != "" || d.Comment != "" {
		sb.WriteString("\n")
	}

	if dirLines := directiveLines(d.Metadata, "", ""); len(dirLines) > 0 {
		for _, line := range dirLines {
			sb.WriteString(line)
			sb.WriteString("\n")
		}
		sb.WriteString("\n")
	}

	for _, schema := range d.Schemas {
		sb.WriteString(w.schemaToDBML(schema))
	}

	sb.WriteString("\n// Relationships\n")
	for _, schema := range d.Schemas {
		for _, table := range schema.Tables {
			for _, constraint := range sortConstraints(table.Constraints) {
				if constraint.Type == models.ForeignKeyConstraint {
					sb.WriteString(w.constraintToDBML(constraint, table))
				}
			}
		}
	}

	return sb.String()
}

// schemaToDBML converts a Schema to DBML format string
func (w *Writer) schemaToDBML(schema *models.Schema) string {
	var sb strings.Builder

	if schema.Description != "" {
		fmt.Fprintf(&sb, "// Schema: %s - %s\n", schema.Name, schema.Description)
	}

	for _, table := range schema.Tables {
		sb.WriteString(w.tableToDBML(table))
		sb.WriteString("\n")
	}

	return sb.String()
}

// tableToDBML converts a Table to DBML format string
func (w *Writer) tableToDBML(t *models.Table) string {
	var sb strings.Builder

	tableName := fmt.Sprintf("%s.%s", t.Schema, t.Name)
	fmt.Fprintf(&sb, "Table %s {\n", tableName)

	for _, column := range sortColumns(t.Columns) {
		fmt.Fprintf(&sb, "  %s %s", column.Name, column.Type)

		var attrs []string
		if column.IsPrimaryKey {
			attrs = append(attrs, "pk")
		}
		if column.NotNull && !column.IsPrimaryKey {
			attrs = append(attrs, "not null")
		}
		if column.AutoIncrement {
			attrs = append(attrs, "increment")
		}
		if column.Default != nil {
			// Check if default value contains backticks (DBML expressions like `now()`)
			defaultStr := fmt.Sprintf("%v", column.Default)
			if strings.HasPrefix(defaultStr, "`") && strings.HasSuffix(defaultStr, "`") {
				// Already an expression with backticks, use as-is
				attrs = append(attrs, fmt.Sprintf("default: %s", defaultStr))
			} else {
				// Regular value, wrap in single quotes
				attrs = append(attrs, fmt.Sprintf("default: '%v'", column.Default))
			}
		}

		if len(attrs) > 0 {
			fmt.Fprintf(&sb, " [%s]", strings.Join(attrs, ", "))
		}

		if column.Comment != "" {
			fmt.Fprintf(&sb, " // %s", column.Comment)
		}
		sb.WriteString("\n")

		for _, line := range directiveLines(column.Metadata, "  ", column.Name) {
			sb.WriteString(line)
			sb.WriteString("\n")
		}
	}

	if len(t.Indexes) > 0 {
		sb.WriteString("\n  indexes {\n")
		for _, index := range sortIndexes(t.Indexes) {
			var indexAttrs []string
			if index.Unique {
				indexAttrs = append(indexAttrs, "unique")
			}
			if index.Name != "" {
				indexAttrs = append(indexAttrs, fmt.Sprintf("name: '%s'", index.Name))
			}
			if index.Type != "" {
				indexAttrs = append(indexAttrs, fmt.Sprintf("type: %s", index.Type))
			}

			fmt.Fprintf(&sb, "    (%s)", strings.Join(index.Columns, ", "))
			if len(indexAttrs) > 0 {
				fmt.Fprintf(&sb, " [%s]", strings.Join(indexAttrs, ", "))
			}
			sb.WriteString("\n")

			for _, line := range directiveLines(index.Metadata, "    ", "") {
				sb.WriteString(line)
				sb.WriteString("\n")
			}
		}
		sb.WriteString("  }\n")
	}

	for _, line := range directiveLines(t.Metadata, "  ", "") {
		sb.WriteString(line)
		sb.WriteString("\n")
	}

	note := strings.TrimSpace(t.Description + " " + t.Comment)
	if note != "" {
		fmt.Fprintf(&sb, "\n  Note: '%s'\n", note)
	}

	sb.WriteString("}\n")
	return sb.String()
}

// constraintToDBML converts a Constraint to DBML format string
func (w *Writer) constraintToDBML(c *models.Constraint, t *models.Table) string {
	if c.Type != models.ForeignKeyConstraint || c.ReferencedTable == "" {
		return ""
	}

	fromTable := fmt.Sprintf("%s.%s", c.Schema, c.Table)
	toTable := fmt.Sprintf("%s.%s", c.ReferencedSchema, c.ReferencedTable)

	relationship := ">" // Default to many-to-one
	for _, index := range t.Indexes {
		if index.Unique && strings.Join(index.Columns, ",") == strings.Join(c.Columns, ",") {
			relationship = "-" // one-to-one
			break
		}
	}
	for _, column := range c.Columns {
		if t.Columns[column].IsPrimaryKey {
			relationship = "-" // one-to-one
			break
		}
	}

	var fromRef, toRef string
	if len(c.Columns) == 1 {
		fromRef = fmt.Sprintf("%s.%s", fromTable, c.Columns[0])
	} else {
		fromRef = fmt.Sprintf("%s.(%s)", fromTable, strings.Join(c.Columns, ", "))
	}

	if len(c.ReferencedColumns) == 1 {
		toRef = fmt.Sprintf("%s.%s", toTable, c.ReferencedColumns[0])
	} else {
		toRef = fmt.Sprintf("%s.(%s)", toTable, strings.Join(c.ReferencedColumns, ", "))
	}

	var actions []string
	if c.OnDelete != "" {
		actions = append(actions, fmt.Sprintf("delete: %s", c.OnDelete))
	}
	if c.OnUpdate != "" {
		actions = append(actions, fmt.Sprintf("update: %s", c.OnUpdate))
	}

	refLine := fmt.Sprintf("Ref: %s %s %s", fromRef, relationship, toRef)
	if len(actions) > 0 {
		refLine += fmt.Sprintf(" [%s]", strings.Join(actions, ", "))
	}

	return refLine + "\n"
}

// sortColumns returns columns sorted by Sequence then Name for deterministic output.
func sortColumns(columns map[string]*models.Column) []*models.Column {
	result := make([]*models.Column, 0, len(columns))
	for _, col := range columns {
		result = append(result, col)
	}
	sort.Slice(result, func(i, j int) bool {
		if result[i].Sequence > 0 && result[j].Sequence > 0 {
			return result[i].Sequence < result[j].Sequence
		}
		return result[i].Name < result[j].Name
	})
	return result
}

// sortConstraints returns constraints sorted by Sequence then Name for deterministic output.
func sortConstraints(constraints map[string]*models.Constraint) []*models.Constraint {
	result := make([]*models.Constraint, 0, len(constraints))
	for _, c := range constraints {
		result = append(result, c)
	}
	sort.Slice(result, func(i, j int) bool {
		if result[i].Sequence > 0 && result[j].Sequence > 0 {
			return result[i].Sequence < result[j].Sequence
		}
		return result[i].Name < result[j].Name
	})
	return result
}

// sortIndexes returns indexes sorted by Sequence then Name for deterministic output.
func sortIndexes(indexes map[string]*models.Index) []*models.Index {
	result := make([]*models.Index, 0, len(indexes))
	for _, idx := range indexes {
		result = append(result, idx)
	}
	sort.Slice(result, func(i, j int) bool {
		if result[i].Sequence > 0 && result[j].Sequence > 0 {
			return result[i].Sequence < result[j].Sequence
		}
		return result[i].Name < result[j].Name
	})
	return result
}
