// Patial Tech.
// Author, Ankit Patial

package main

import (
	"fmt"
	"go/format"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/text/cases"
	"golang.org/x/text/language"
)

// version can be set at build time using:
// go build -ldflags "-X main.version=v1.2.3" ./cmd
var version = "dev"

func generate(scheamPath, outDir string) error {
	// read schame.sql
	f, err := os.ReadFile(scheamPath)
	if err != nil {
		return err
	}

	// Parse scheam.sql
	tbls, err := parse(f)
	if err != nil {
		return err
	}

	// Output dir, create if not exists.
	if _, err := os.Stat(outDir); os.IsNotExist(err) {
		if err := os.MkdirAll(outDir, 0755); err != nil {
			return err
		}
	}

	// schema.go will hold all tables info
	var sb strings.Builder
	sb.WriteString(
		fmt.Sprintf("// Code generated by code.patial.tech/go/pgm/cmd %s DO NOT EDIT.\n\n", GetVersionString()),
	)
	sb.WriteString(fmt.Sprintf("package %s \n", filepath.Base(outDir)))
	sb.WriteString(`
	import "code.patial.tech/go/pgm"

	var (
`)
	caser := cases.Title(language.English)
	var (
		modalDir   string
		fieldCount int
	)
	for _, t := range tbls {
		// skip schema_migrations
		if t.Table == "schema_migrations" {
			continue
		}

		name := pluralToSingular(t.Table)

		name = strings.ReplaceAll(name, "_", " ")
		name = caser.String(name)
		name = strings.ReplaceAll(name, " ", "")
		fieldCount = len(t.Columns)
		sb.WriteString(fmt.Sprintf(
			" %s = pgm.Table{Name: %q, FieldCount: %d", name, t.Table, fieldCount,
		))
		if len(t.PrimaryKey) > 0 {
			sb.WriteString(", PK: []string{}")
			sb.WriteString("}\n")
		} else {
			sb.WriteString("}\n")
		}
		modalDir = strings.ToLower(name)
		os.Mkdir(filepath.Join(outDir, modalDir), 0755)

		if err = writeColFile(t.Table, t.Columns, filepath.Join(outDir, modalDir), caser); err != nil {
			return err
		}
	}

	sb.WriteString(")")

	sb.WriteString(`
	func DerivedTable(tblName string, fromQry pgm.Query) pgm.Table {
		t := pgm.Table{
			Name:         tblName,
			DerivedTable: fromQry,
		}
		return t
}`)

	// Format code before saving
	code, err := formatGoCode(sb.String())
	if err != nil {
		return err
	}

	// Save file to disk
	os.WriteFile(filepath.Join(outDir, "schema.go"), code, 0644)
	return nil
}

func writeColFile(tblName string, cols []*Column, outDir string, caser cases.Caser) error {
	var sb strings.Builder
	sb.WriteString(
		fmt.Sprintf("// Code generated by code.patial.tech/go/pgm/cmd %s DO NOT EDIT.\n\n", GetVersionString()),
	)
	sb.WriteString(fmt.Sprintf("package %s\n\n", filepath.Base(outDir)))
	sb.WriteString(fmt.Sprintf("import %q\n\n", "code.patial.tech/go/pgm"))
	sb.WriteString("const (")
	sb.WriteString("\n // All fields in table " + tblName)
	sb.WriteString(fmt.Sprintf("\n All pgm.Field = %q", tblName+".*"))
	var name string
	for _, c := range cols {
		name = strings.ReplaceAll(c.Name, "_", " ")
		name = caser.String(name)
		name = strings.ReplaceAll(name, " ", "")

		if strings.HasSuffix(name, "Id") {
			name = name[0:len(name)-2] + "ID"
		}

		sb.WriteString(fmt.Sprintf("\n // %s field has db type %q", name, c.Type))
		sb.WriteString(fmt.Sprintf("\n %s pgm.Field = %q", name, tblName+"."+c.Name))
	}

	sb.WriteString("\n)")

	// Format code before saving
	code, err := formatGoCode(sb.String())
	if err != nil {
		return err
	}
	// Save file to disk.
	return os.WriteFile(filepath.Join(outDir, tblName+".go"), code, 0644)
}

// pluralToSingular converts plural table names to singular forms
func pluralToSingular(plural string) string {
	// Handle special irregular plurals
	irregulars := map[string]string{
		"people":    "person",
		"children":  "child",
		"men":       "man",
		"women":     "woman",
		"feet":      "foot",
		"teeth":     "tooth",
		"mice":      "mouse",
		"geese":     "goose",
		"oxen":      "ox",
		"data":      "datum",
		"criteria":  "criterion",
		"phenomena": "phenomenon",
	}

	lower := strings.ToLower(plural)
	if singular, exists := irregulars[lower]; exists {
		return singular
	}

	// Handle words ending in -ies (cities -> city, companies -> company)
	if strings.HasSuffix(lower, "ies") && len(plural) > 3 {
		return plural[:len(plural)-3] + "y"
	}

	// Handle words ending in -ves (lives -> life, wives -> wife)
	if strings.HasSuffix(lower, "ves") && len(plural) > 3 {
		return plural[:len(plural)-3] + "fe"
	}

	// Handle words ending in -ses (classes -> class, addresses -> address)
	if strings.HasSuffix(lower, "ses") && len(plural) > 3 {
		return plural[:len(plural)-2]
	}

	// Handle words ending in -xes (boxes -> box, taxes -> tax)
	if strings.HasSuffix(lower, "xes") && len(plural) > 3 {
		return plural[:len(plural)-2]
	}

	// Handle words ending in -ches (watches -> watch, benches -> bench)
	if strings.HasSuffix(lower, "ches") && len(plural) > 4 {
		return plural[:len(plural)-2]
	}

	// Handle words ending in -shes (dishes -> dish, bushes -> bush)
	if strings.HasSuffix(lower, "shes") && len(plural) > 4 {
		return plural[:len(plural)-2]
	}

	// Handle words ending in -oes (heroes -> hero, potatoes -> potato)
	if strings.HasSuffix(lower, "oes") && len(plural) > 3 {
		return plural[:len(plural)-2]
	}

	// Handle general -es endings
	if strings.HasSuffix(lower, "es") && len(plural) > 2 {
		withoutEs := plural[:len(plural)-2]
		if len(withoutEs) > 0 {
			lastChar := strings.ToLower(string(withoutEs[len(withoutEs)-1]))
			// Only remove -es (not just -s) if the word ends in s, x, ch, sh, o (those need the -es)
			if lastChar == "s" || lastChar == "x" || lastChar == "o" ||
				strings.HasSuffix(strings.ToLower(withoutEs), "ch") ||
				strings.HasSuffix(strings.ToLower(withoutEs), "sh") {
				return withoutEs
			}
		}
		// For words like "references", "preferences" - just remove the -s
		return plural[:len(plural)-1]
	}

	// Handle simple -s endings (users -> user, books -> book)
	if strings.HasSuffix(lower, "s") && len(plural) > 1 {
		return plural[:len(plural)-1]
	}

	// If no rules match, return as-is (might already be singular)
	return plural
}

// formatGoCode formats the input Go code using go/format.
func formatGoCode(str string) ([]byte, error) {
	formatted, err := format.Source([]byte(str))
	if err != nil {
		return nil, fmt.Errorf("formatting code: %w", err)
	}

	return formatted, nil
}
