package migrations

import (
	"database/sql"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"gorm.io/gen"
	"gorm.io/gen/field"
	"gorm.io/gorm"
)

// ForeignKeyMeta menyimpan metadata Foreign Key dari PostgreSQL.
type ForeignKeyMeta struct {
	ConstraintName string
	TableName      string
	ColumnName     string
	RefTable       string
	RefColumn      string
	Ordinal        int
}

// GenerateModel membuat model GORM dari seluruh tabel public.
func GenerateModel(table *string, db *gorm.DB) {
	currentDir, err := os.Getwd()
	if err != nil {
		log.Fatalf(
			"Gagal mendapatkan working directory: %v",
			err,
		)
	}

	// Folder sementara untuk generated query.
	queryOutPath := filepath.Join(
		currentDir,
		"internal",
		"domain",
		"query_utils",
	)

	// Folder model.
	modelOutPath := filepath.Join(
		currentDir,
		"internal",
		"domain",
	)

	if err := os.MkdirAll(queryOutPath, os.ModePerm); err != nil {
		log.Fatalf(
			"Gagal membuat folder query_utils: %v",
			err,
		)
	}

	if err := os.MkdirAll(modelOutPath, os.ModePerm); err != nil {
		log.Fatalf(
			"Gagal membuat folder domain: %v",
			err,
		)
	}

	// ---------------------------------------------------------
	// SQL DB
	// ---------------------------------------------------------

	sqlDB, err := db.DB()
	if err != nil {
		log.Fatalf(
			"Gagal mengambil instance sql.DB: %v",
			err,
		)
	}

	// ---------------------------------------------------------
	// Foreign Keys
	// ---------------------------------------------------------

	fkMetas := fetchForeignKeys(sqlDB)

	log.Printf(
		"Berhasil mendeteksi %d kolom Foreign Key fisik di database.",
		len(fkMetas),
	)

	// ---------------------------------------------------------
	// GORM Gen
	// ---------------------------------------------------------

	g := gen.NewGenerator(gen.Config{
		OutPath:      queryOutPath,
		ModelPkgPath: modelOutPath,

		Mode: gen.WithoutContext,

		FieldWithIndexTag: true,
		FieldWithTypeTag:  true,
		FieldNullable:     true,
		FieldSignable:     true,
	})

	g.UseDB(db)

	// ---------------------------------------------------------
	// Get tables
	// ---------------------------------------------------------
	qInfo := ""
	if table != nil {
		qInfo = fmt.Sprintf(`
		SELECT
			table_name
		FROM information_schema.tables
		WHERE
			table_schema = 'public'
			AND table_type = 'BASE TABLE'
			AND table_name = '%s'
		ORDER BY table_name
	`, *table)
	} else {
		qInfo = `
		SELECT
			table_name
		FROM information_schema.tables
		WHERE
			table_schema = 'public'
			AND table_type = 'BASE TABLE'
		ORDER BY table_name
	`
	}

	rows, err := sqlDB.Query(qInfo)
	if err != nil {
		log.Fatalf(
			"Gagal mengambil daftar tabel: %v",
			err,
		)
	}

	defer rows.Close()

	var tableNames []string

	for rows.Next() {
		var tName string

		if err := rows.Scan(&tName); err != nil {
			log.Printf(
				"Gagal membaca nama tabel: %v",
				err,
			)
			continue
		}

		// Jangan generate tabel internal GORM.
		if strings.HasPrefix(tName, "gorm_") {
			continue
		}

		tableNames = append(
			tableNames,
			tName,
		)
	}

	if err := rows.Err(); err != nil {
		log.Fatalf(
			"Gagal membaca daftar tabel: %v",
			err,
		)
	}

	log.Printf(
		"Berhasil menemukan %d tabel.",
		len(tableNames),
	)

	// ---------------------------------------------------------
	// Group FK
	//
	// Satu constraint = satu relationship.
	//
	// Ini penting untuk composite FK.
	// ---------------------------------------------------------

	fkGroups := groupForeignKeys(fkMetas)

	// ---------------------------------------------------------
	// Relationship options per table
	// ---------------------------------------------------------

	tableRelationOpts := make(
		map[string][]gen.ModelOpt,
	)

	// Digunakan untuk mencegah relationship yang benar-benar
	// identik dibuat lebih dari satu kali.
	//
	// key:
	//
	// source table
	// relation type
	// property
	// target table
	// foreign keys
	// references
	//
	seenRelations := make(
		map[string]bool,
	)

	// ---------------------------------------------------------
	// Build relationships
	// ---------------------------------------------------------

	for _, group := range fkGroups {
		if len(group) == 0 {
			continue
		}

		// Pastikan urutan composite FK konsisten.
		sort.Slice(
			group,
			func(i, j int) bool {
				return group[i].Ordinal < group[j].Ordinal
			},
		)

		first := group[0]

		// Pastikan source dan target table memang ada.
		if !containsString(
			tableNames,
			first.TableName,
		) {
			log.Printf(
				"WARNING: source table %s tidak ditemukan.",
				first.TableName,
			)
			continue
		}

		if !containsString(
			tableNames,
			first.RefTable,
		) {
			log.Printf(
				"WARNING: referenced table %s tidak ditemukan.",
				first.RefTable,
			)
			continue
		}

		// -----------------------------------------------------
		// Collect FK columns
		// -----------------------------------------------------

		foreignKeys := make(
			[]string,
			0,
			len(group),
		)

		references := make(
			[]string,
			0,
			len(group),
		)

		for _, fk := range group {
			foreignKeys = append(
				foreignKeys,
				fk.ColumnName,
			)

			references = append(
				references,
				fk.RefColumn,
			)
		}

		// -----------------------------------------------------
		// BELONGS TO
		// -----------------------------------------------------

		belongsProperty := belongsToPropertyName(first)

		belongsRelationKey := buildRelationKey(
			first.TableName,
			"belongs_to",
			belongsProperty,
			first.RefTable,
			foreignKeys,
			references,
		)

		if !seenRelations[belongsRelationKey] {
			seenRelations[belongsRelationKey] = true

			log.Printf(
				"BELONGS TO: %s (%s) -> %s (%s) [%s]",
				first.TableName,
				strings.Join(foreignKeys, ", "),
				first.RefTable,
				strings.Join(references, ", "),
				belongsProperty,
			)

			relation := gen.FieldRelate(
				field.BelongsTo,
				belongsProperty,
				g.GenerateModel(first.RefTable),
				&field.RelateConfig{
					GORMTag: field.GormTag{
						"foreignKey": foreignKeys,
						"references": references,
					},
				},
			)

			tableRelationOpts[first.TableName] =
				append(
					tableRelationOpts[first.TableName],
					relation,
				)
		}

		// -----------------------------------------------------
		// HAS MANY
		// -----------------------------------------------------

		hasManyProperty := hasManyPropertyName(first)

		hasManyRelationKey := buildRelationKey(
			first.RefTable,
			"has_many",
			hasManyProperty,
			first.TableName,
			foreignKeys,
			references,
		)

		if !seenRelations[hasManyRelationKey] {
			seenRelations[hasManyRelationKey] = true

			log.Printf(
				"HAS MANY: %s -> %s (%s) [%s]",
				first.RefTable,
				first.TableName,
				strings.Join(foreignKeys, ", "),
				hasManyProperty,
			)

			relation := gen.FieldRelate(
				field.HasMany,
				hasManyProperty,
				g.GenerateModel(first.TableName),
				&field.RelateConfig{
					GORMTag: field.GormTag{
						"foreignKey": foreignKeys,
						"references": references,
					},
				},
			)

			tableRelationOpts[first.RefTable] =
				append(
					tableRelationOpts[first.RefTable],
					relation,
				)
		}
	}

	// ---------------------------------------------------------
	// Generate final models
	// ---------------------------------------------------------

	var finalModels []any

	// Simpan generated model berdasarkan table.
	//
	// Kita tidak menggunakan gen.Model karena type tersebut
	// memang tidak diekspos oleh gorm/gen.
	//
	// GenerateModel tetap dipanggil di sini untuk menghasilkan
	// final model dengan relation options.
	//
	// Tidak ada kebutuhan untuk menyimpan return type-nya.
	for _, tName := range tableNames {
		opts := tableRelationOpts[tName]

		compiledModel := g.GenerateModel(
			tName,
			opts...,
		)

		finalModels = append(
			finalModels,
			compiledModel,
		)
	}

	// ---------------------------------------------------------
	// ApplyBasic
	// ---------------------------------------------------------

	if table != nil && *table != "" {
		targetTable := strings.TrimSpace(*table)

		if !containsString(
			tableNames,
			targetTable,
		) {
			log.Fatalf(
				"Tabel %s tidak ditemukan di database!",
				targetTable,
			)
		}

		// Generate model khusus table yang diminta.
		specModel := g.GenerateModel(
			targetTable,
			tableRelationOpts[targetTable]...,
		)

		g.ApplyBasic(specModel)

		log.Printf(
			"Hanya generate model: %s",
			targetTable,
		)
	} else {
		g.ApplyBasic(
			finalModels...,
		)

		log.Printf(
			"Generate seluruh %d model.",
			len(finalModels),
		)
	}

	// ---------------------------------------------------------
	// Execute
	// ---------------------------------------------------------

	g.Execute()

	// ---------------------------------------------------------
	// Remove query utility
	// ---------------------------------------------------------

	if err := os.RemoveAll(queryOutPath); err != nil {
		log.Printf(
			"Peringatan: gagal menghapus query_utils: %v",
			err,
		)
	}

	// ---------------------------------------------------------
	// Rename .gen.go -> .go
	// ---------------------------------------------------------

	if err := renameGeneratedFiles(modelOutPath); err != nil {
		log.Printf(
			"Peringatan: gagal mengubah nama file model: %v",
			err,
		)
	}

	log.Println(
		"Selesai! Model berhasil dibuat.",
	)
}

// -------------------------------------------------------------
// Group Foreign Keys
// -------------------------------------------------------------
//
// Satu constraint_name dianggap sebagai satu relationship.
//
// Contoh:
//
// FK:
//   company_id -> company.id
//   user_id    -> user.id
//
// Jika berbeda constraint:
//
//   fk_order_company
//   fk_order_user
//
// maka akan menjadi dua relationship.
//
// Composite FK:
//
//   FK (company_id, order_id)
//      -> orders (company_id, id)
//
// menjadi satu relationship.
//
// -------------------------------------------------------------

func groupForeignKeys(
	metas []ForeignKeyMeta,
) [][]ForeignKeyMeta {

	groups := make(
		map[string][]ForeignKeyMeta,
	)

	for _, meta := range metas {

		key := fmt.Sprintf(
			"%s|%s",
			meta.TableName,
			meta.ConstraintName,
		)

		groups[key] = append(
			groups[key],
			meta,
		)
	}

	result := make(
		[][]ForeignKeyMeta,
		0,
		len(groups),
	)

	for _, group := range groups {

		sort.Slice(
			group,
			func(i, j int) bool {
				return group[i].Ordinal < group[j].Ordinal
			},
		)

		result = append(
			result,
			group,
		)
	}

	// Untuk deterministic output.
	sort.Slice(
		result,
		func(i, j int) bool {

			if result[i][0].TableName != result[j][0].TableName {
				return result[i][0].TableName <
					result[j][0].TableName
			}

			return result[i][0].ConstraintName <
				result[j][0].ConstraintName
		},
	)

	return result
}

// -------------------------------------------------------------
// Build unique relation key
// -------------------------------------------------------------

func buildRelationKey(
	sourceTable string,
	relationType string,
	propertyName string,
	targetTable string,
	foreignKeys []string,
	references []string,
) string {

	return strings.Join(
		[]string{
			sourceTable,
			relationType,
			propertyName,
			targetTable,
			strings.Join(foreignKeys, ","),
			strings.Join(references, ","),
		},
		"|",
	)
}

// -------------------------------------------------------------
// BelongsTo property name
// -------------------------------------------------------------
//
// user_id
//     -> User_Relation
//
// customer_id
//     -> Customer_Relation
//
// company_id
//     -> Company_Relation
//
// -------------------------------------------------------------

func belongsToPropertyName(fk ForeignKeyMeta) string {
	name := strings.TrimSuffix(
		fk.ColumnName,
		"_id",
	)

	if name == "" || name == fk.ColumnName {
		name = fk.RefTable
	}

	return snakeToPascalCase(name) + "_Relation"
}

// -------------------------------------------------------------
// HasMany property name
// -------------------------------------------------------------
//
// orders -> Orders_Relation
//
// -------------------------------------------------------------

func hasManyPropertyName(fk ForeignKeyMeta) string {
	tableName := snakeToPascalCase(fk.TableName)

	columnName := strings.TrimSuffix(
		fk.ColumnName,
		"_id",
	)

	columnName = snakeToPascalCase(columnName)

	return tableName +
		"_By" +
		columnName +
		"_Relation"
}

// -------------------------------------------------------------
// Fetch Foreign Keys
// -------------------------------------------------------------

func fetchForeignKeys(
	db *sql.DB,
) []ForeignKeyMeta {

	query := `
		SELECT
			tc.constraint_name,
			kcu.table_name,
			kcu.column_name,
			ccu.table_name AS referenced_table_name,
			ccu.column_name AS referenced_column_name,
			kcu.ordinal_position
		FROM information_schema.table_constraints AS tc

		JOIN information_schema.key_column_usage AS kcu
			ON tc.constraint_name = kcu.constraint_name
			AND tc.table_schema = kcu.table_schema

		JOIN information_schema.referential_constraints AS rc
			ON tc.constraint_name = rc.constraint_name
			AND tc.table_schema = rc.constraint_schema

		JOIN information_schema.key_column_usage AS ccu
			ON rc.unique_constraint_name = ccu.constraint_name
			AND rc.unique_constraint_schema = ccu.table_schema
			AND kcu.ordinal_position = ccu.ordinal_position

		WHERE
			tc.constraint_type = 'FOREIGN KEY'
			AND tc.table_schema = 'public'

		ORDER BY
			kcu.table_name,
			tc.constraint_name,
			kcu.ordinal_position
	`

	rows, err := db.Query(query)
	if err != nil {
		log.Printf(
			"Gagal membaca metadata Foreign Key: %v",
			err,
		)

		return nil
	}

	defer rows.Close()

	var metas []ForeignKeyMeta

	for rows.Next() {

		var m ForeignKeyMeta

		err := rows.Scan(
			&m.ConstraintName,
			&m.TableName,
			&m.ColumnName,
			&m.RefTable,
			&m.RefColumn,
			&m.Ordinal,
		)

		if err != nil {
			log.Printf(
				"Gagal membaca metadata Foreign Key: %v",
				err,
			)

			continue
		}

		metas = append(
			metas,
			m,
		)
	}

	if err := rows.Err(); err != nil {
		log.Printf(
			"Error membaca Foreign Key rows: %v",
			err,
		)
	}

	return metas
}

// -------------------------------------------------------------
// Rename generated files
// -------------------------------------------------------------

func renameGeneratedFiles(
	dir string,
) error {

	files, err := os.ReadDir(dir)
	if err != nil {
		return err
	}

	for _, file := range files {

		if file.IsDir() {
			continue
		}

		if !strings.HasSuffix(
			file.Name(),
			".gen.go",
		) {
			continue
		}

		oldPath := filepath.Join(
			dir,
			file.Name(),
		)

		newName := strings.TrimSuffix(
			file.Name(),
			".gen.go",
		) + ".go"

		newPath := filepath.Join(
			dir,
			newName,
		)

		if err := os.Rename(
			oldPath,
			newPath,
		); err != nil {
			return err
		}
	}

	return nil
}

// -------------------------------------------------------------
// Contains string
// -------------------------------------------------------------

func containsString(
	items []string,
	target string,
) bool {

	for _, item := range items {
		if item == target {
			return true
		}
	}

	return false
}

// -------------------------------------------------------------
// snake_case -> PascalCase
// -------------------------------------------------------------

func snakeToPascalCase(
	s string,
) string {

	parts := strings.Split(
		s,
		"_",
	)

	for i, part := range parts {

		if len(part) == 0 {
			continue
		}

		parts[i] =
			strings.ToUpper(part[:1]) +
				part[1:]
	}

	return strings.Join(
		parts,
		"",
	)
}
