package bun

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"

	"git.warky.dev/wdevs/relspecgo/pkg/models"
	"git.warky.dev/wdevs/relspecgo/pkg/pgsql"
	"git.warky.dev/wdevs/relspecgo/pkg/readers"
)

// Reader implements the readers.Reader interface for Bun Go model files
type Reader struct {
	options *readers.ReaderOptions
}

// NewReader creates a new Bun reader with the given options
func NewReader(options *readers.ReaderOptions) *Reader {
	return &Reader{
		options: options,
	}
}

// ReadDatabase reads Bun Go model files and returns a Database model
func (r *Reader) ReadDatabase() (*models.Database, error) {
	if r.options.FilePath == "" {
		return nil, fmt.Errorf("file path is required for Bun reader")
	}

	// Check if path is a directory or file
	info, err := os.Stat(r.options.FilePath)
	if err != nil {
		return nil, fmt.Errorf("failed to stat path: %w", err)
	}

	var files []string
	if info.IsDir() {
		// Read all .go files in directory
		entries, err := os.ReadDir(r.options.FilePath)
		if err != nil {
			return nil, fmt.Errorf("failed to read directory: %w", err)
		}
		for _, entry := range entries {
			if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") && !strings.HasSuffix(entry.Name(), "_test.go") {
				files = append(files, filepath.Join(r.options.FilePath, entry.Name()))
			}
		}
	} else {
		files = append(files, r.options.FilePath)
	}

	if len(files) == 0 {
		return nil, fmt.Errorf("no Go files found")
	}

	// Parse all files and collect tables
	db := models.InitDatabase("database")
	schemaMap := make(map[string]*models.Schema)

	for _, file := range files {
		tables, err := r.parseFile(file)
		if err != nil {
			return nil, fmt.Errorf("failed to parse file %s: %w", file, err)
		}

		for _, table := range tables {
			// Get or create schema
			schema, ok := schemaMap[table.Schema]
			if !ok {
				schema = models.InitSchema(table.Schema)
				schemaMap[table.Schema] = schema
			}
			schema.Tables = append(schema.Tables, table)
		}
	}

	// Convert schema map to slice
	for _, schema := range schemaMap {
		db.Schemas = append(db.Schemas, schema)
	}

	return db, nil
}

// ReadSchema reads Bun Go model files and returns a Schema model
func (r *Reader) ReadSchema() (*models.Schema, error) {
	db, err := r.ReadDatabase()
	if err != nil {
		return nil, err
	}

	if len(db.Schemas) == 0 {
		return nil, fmt.Errorf("no schemas found")
	}

	return db.Schemas[0], nil
}

// ReadTable reads a Bun Go model file and returns a Table model
func (r *Reader) ReadTable() (*models.Table, error) {
	schema, err := r.ReadSchema()
	if err != nil {
		return nil, err
	}

	if len(schema.Tables) == 0 {
		return nil, fmt.Errorf("no tables found")
	}

	return schema.Tables[0], nil
}

// parseFile parses a single Go file and extracts table models
func (r *Reader) parseFile(filename string) ([]*models.Table, error) {
	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
	if err != nil {
		return nil, fmt.Errorf("failed to parse Go file: %w", err)
	}

	var tables []*models.Table
	structMap := make(map[string]*models.Table)

	// First pass: collect struct definitions
	for _, decl := range node.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			structType, ok := typeSpec.Type.(*ast.StructType)
			if !ok {
				continue
			}

			// Check if this struct has bun tags (indicates it's a model)
			if r.hasModelFields(structType) {
				table := r.parseStruct(typeSpec.Name.Name, structType)
				if table != nil {
					structMap[typeSpec.Name.Name] = table
					tables = append(tables, table)
				}
			}
		}
	}

	// Second pass: find TableName() methods (for redundancy/verification)
	for _, decl := range node.Decls {
		funcDecl, ok := decl.(*ast.FuncDecl)
		if !ok || funcDecl.Name.Name != "TableName" {
			continue
		}

		// Get receiver type
		if funcDecl.Recv == nil || len(funcDecl.Recv.List) == 0 {
			continue
		}

		receiverType := r.getReceiverType(funcDecl.Recv.List[0].Type)
		if receiverType == "" {
			continue
		}

		// Find the table for this struct
		table, ok := structMap[receiverType]
		if !ok {
			continue
		}

		// Parse the return value (this is redundant with the bun tag, but provides verification)
		tableName, schemaName := r.parseTableNameMethod(funcDecl)
		if tableName != "" {
			table.Name = tableName
			if schemaName != "" {
				table.Schema = schemaName
			}

			// Update columns and indexes
			for _, col := range table.Columns {
				col.Table = tableName
				col.Schema = table.Schema
			}
			for _, idx := range table.Indexes {
				idx.Table = tableName
				idx.Schema = table.Schema
			}
		}
	}

	// Third pass: parse relationship fields for constraints
	for _, decl := range node.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			structType, ok := typeSpec.Type.(*ast.StructType)
			if !ok {
				continue
			}

			table, ok := structMap[typeSpec.Name.Name]
			if !ok {
				continue
			}

			// Parse relationship fields
			r.parseRelationshipConstraints(table, structType, structMap)
		}
	}

	return tables, nil
}

// getReceiverType extracts the type name from a receiver
func (r *Reader) getReceiverType(expr ast.Expr) string {
	switch t := expr.(type) {
	case *ast.Ident:
		return t.Name
	case *ast.StarExpr:
		if ident, ok := t.X.(*ast.Ident); ok {
			return ident.Name
		}
	}
	return ""
}

// parseTableNameMethod parses a TableName() method and extracts the table and schema name
func (r *Reader) parseTableNameMethod(funcDecl *ast.FuncDecl) (tableName, schemaName string) {
	if funcDecl.Body == nil {
		return "", ""
	}

	// Look for return statement
	for _, stmt := range funcDecl.Body.List {
		retStmt, ok := stmt.(*ast.ReturnStmt)
		if !ok {
			continue
		}

		if len(retStmt.Results) == 0 {
			continue
		}

		// Get the return value (should be a string literal)
		if basicLit, ok := retStmt.Results[0].(*ast.BasicLit); ok {
			if basicLit.Kind == token.STRING {
				// Remove quotes
				fullName := strings.Trim(basicLit.Value, "\"")

				// Split schema.table
				if strings.Contains(fullName, ".") {
					parts := strings.SplitN(fullName, ".", 2)
					return parts[1], parts[0]
				}

				return fullName, "public"
			}
		}
	}

	return "", ""
}

// hasModelFields checks if the struct has fields with bun tags
func (r *Reader) hasModelFields(structType *ast.StructType) bool {
	for _, field := range structType.Fields.List {
		if field.Tag != nil {
			tag := field.Tag.Value
			if strings.Contains(tag, "bun:") {
				return true
			}
		}
	}
	return false
}

// parseStruct converts an AST struct to a Table model
func (r *Reader) parseStruct(structName string, structType *ast.StructType) *models.Table {
	// Extract table name from the first field's bun tag if present
	tableName := ""
	schemaName := "public"

	// Look for table name in struct tags
	for _, field := range structType.Fields.List {
		if field.Tag != nil {
			tag := field.Tag.Value
			if strings.Contains(tag, "bun:\"table:") {
				tableName, schemaName = r.extractTableNameFromTag(tag)
				break
			}
		}
	}

	// If no table name found, derive from struct name
	if tableName == "" {
		tableName = r.deriveTableName(structName)
	}

	table := models.InitTable(tableName, schemaName)
	sequence := uint(1)

	// Parse fields
	for _, field := range structType.Fields.List {
		if field.Tag == nil {
			continue
		}

		tag := field.Tag.Value
		if !strings.Contains(tag, "bun:") {
			continue
		}

		// Skip BaseModel and relationship fields
		if r.isBaseModel(field) || r.isRelationship(tag) {
			continue
		}

		// Get field name
		fieldName := ""
		if len(field.Names) > 0 {
			fieldName = field.Names[0].Name
		}

		// Parse column from tag
		column := r.parseColumn(fieldName, field.Type, tag, sequence)
		if column != nil {
			column.Table = tableName
			column.Schema = schemaName
			table.Columns[column.Name] = column

			// Parse indexes from bun tags
			r.parseIndexesFromTag(table, column, tag)

			sequence++
		}
	}

	return table
}

// isBaseModel checks if a field is bun.BaseModel
func (r *Reader) isBaseModel(field *ast.Field) bool {
	if len(field.Names) > 0 {
		return false // BaseModel is embedded, so it has no name
	}

	// Check if the type is bun.BaseModel
	selExpr, ok := field.Type.(*ast.SelectorExpr)
	if !ok {
		return false
	}

	ident, ok := selExpr.X.(*ast.Ident)
	if !ok {
		return false
	}

	return ident.Name == "bun" && selExpr.Sel.Name == "BaseModel"
}

// isRelationship checks if a field is a relationship based on bun tag
func (r *Reader) isRelationship(tag string) bool {
	return strings.Contains(tag, "bun:\"rel:") || strings.Contains(tag, ",rel:")
}

// getRelationType extracts the relationship type from a bun tag
func (r *Reader) getRelationType(bunTag string) string {
	if strings.Contains(bunTag, "rel:has-many") {
		return "has-many"
	}
	if strings.Contains(bunTag, "rel:belongs-to") {
		return "belongs-to"
	}
	if strings.Contains(bunTag, "rel:has-one") {
		return "has-one"
	}
	if strings.Contains(bunTag, "rel:many-to-many") {
		return "many-to-many"
	}
	return ""
}

// parseRelationshipConstraints parses relationship fields to extract foreign key constraints
func (r *Reader) parseRelationshipConstraints(table *models.Table, structType *ast.StructType, structMap map[string]*models.Table) {
	for _, field := range structType.Fields.List {
		if field.Tag == nil {
			continue
		}

		tag := field.Tag.Value
		if !r.isRelationship(tag) {
			continue
		}

		bunTag := r.extractBunTag(tag)

		// Get the referenced type name from the field type
		referencedType := r.getRelationshipType(field.Type)
		if referencedType == "" {
			continue
		}

		// Find the referenced table
		referencedTable, ok := structMap[referencedType]
		if !ok {
			continue
		}

		// Parse the join information: join:user_id=id
		// This means: thisTable.user_id = referencedTable.id
		joinInfo := r.parseJoinInfo(bunTag)
		if joinInfo == nil {
			continue
		}

		// Determine which table gets the FK based on relationship type
		relType := r.getRelationType(bunTag)

		var fkTable *models.Table
		var fkColumn, refTable, refColumn string

		switch strings.ToLower(relType) {
		case "belongs-to":
			// For belongs-to: FK is on the current table
			// join:user_id=id means table.user_id references referencedTable.id
			fkTable = table
			fkColumn = joinInfo.ForeignKey
			refTable = referencedTable.Name
			refColumn = joinInfo.ReferencedKey
		case "has-many":
			// For has-many: FK is on the referenced table
			// join:id=user_id means referencedTable.user_id references table.id
			fkTable = referencedTable
			fkColumn = joinInfo.ReferencedKey
			refTable = table.Name
			refColumn = joinInfo.ForeignKey
		default:
			continue
		}

		constraint := &models.Constraint{
			Name:              fmt.Sprintf("fk_%s_%s", fkTable.Name, refTable),
			Type:              models.ForeignKeyConstraint,
			Table:             fkTable.Name,
			Schema:            fkTable.Schema,
			Columns:           []string{fkColumn},
			ReferencedTable:   refTable,
			ReferencedSchema:  fkTable.Schema,
			ReferencedColumns: []string{refColumn},
			OnDelete:          "NO ACTION", // Bun doesn't specify this in tags
			OnUpdate:          "NO ACTION",
		}

		fkTable.Constraints[constraint.Name] = constraint
	}
}

// JoinInfo holds parsed join information
type JoinInfo struct {
	ForeignKey    string // Column in the related table
	ReferencedKey string // Column in the current table
}

// parseJoinInfo parses join information from bun tag
// Example: join:user_id=id means foreign_key=referenced_key
func (r *Reader) parseJoinInfo(bunTag string) *JoinInfo {
	// Look for join: in the tag
	if !strings.Contains(bunTag, "join:") {
		return nil
	}

	// Extract join clause
	parts := strings.Split(bunTag, ",")
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if strings.HasPrefix(part, "join:") {
			joinStr := strings.TrimPrefix(part, "join:")
			// Parse user_id=id
			joinParts := strings.SplitN(joinStr, "=", 2)
			if len(joinParts) == 2 {
				return &JoinInfo{
					ForeignKey:    joinParts[0],
					ReferencedKey: joinParts[1],
				}
			}
		}
	}

	return nil
}

// getRelationshipType extracts the type name from a relationship field
func (r *Reader) getRelationshipType(expr ast.Expr) string {
	switch t := expr.(type) {
	case *ast.ArrayType:
		// []*ModelPost -> ModelPost
		if starExpr, ok := t.Elt.(*ast.StarExpr); ok {
			if ident, ok := starExpr.X.(*ast.Ident); ok {
				return ident.Name
			}
		}
	case *ast.StarExpr:
		// *ModelPost -> ModelPost
		if ident, ok := t.X.(*ast.Ident); ok {
			return ident.Name
		}
	}
	return ""
}

// parseIndexesFromTag extracts index definitions from Bun tags
func (r *Reader) parseIndexesFromTag(table *models.Table, column *models.Column, tag string) {
	bunTag := r.extractBunTag(tag)

	// Parse tag into parts
	parts := strings.Split(bunTag, ",")
	for _, part := range parts {
		part = strings.TrimSpace(part)

		// Check for unique index
		if part == "unique" {
			// Auto-generate index name: idx_tablename_columnname
			indexName := fmt.Sprintf("idx_%s_%s", table.Name, column.Name)

			if _, exists := table.Indexes[indexName]; !exists {
				index := &models.Index{
					Name:    indexName,
					Table:   table.Name,
					Schema:  table.Schema,
					Columns: []string{column.Name},
					Unique:  true,
					Type:    "btree",
				}
				table.Indexes[indexName] = index
			}
		} else if strings.HasPrefix(part, "unique:") {
			// Named unique index: unique:idx_name
			indexName := strings.TrimPrefix(part, "unique:")

			// Check if index already exists (for composite indexes)
			if idx, exists := table.Indexes[indexName]; exists {
				// Add this column to the existing index
				idx.Columns = append(idx.Columns, column.Name)
			} else {
				// Create new index
				index := &models.Index{
					Name:    indexName,
					Table:   table.Name,
					Schema:  table.Schema,
					Columns: []string{column.Name},
					Unique:  true,
					Type:    "btree",
				}
				table.Indexes[indexName] = index
			}
		}
	}
}

// extractTableNameFromTag extracts table and schema from bun tag
func (r *Reader) extractTableNameFromTag(tag string) (tableName, schemaName string) {
	// Extract bun tag value
	re := regexp.MustCompile(`bun:"table:([^"]+)"`)
	matches := re.FindStringSubmatch(tag)
	if len(matches) < 2 {
		return "", "public"
	}

	tablePart := matches[1]
	parts := strings.Split(tablePart, ",")
	fullName := parts[0]

	// Split schema.table
	if strings.Contains(fullName, ".") {
		schemaParts := strings.SplitN(fullName, ".", 2)
		return schemaParts[1], schemaParts[0]
	}

	return fullName, "public"
}

// deriveTableName derives a table name from struct name
func (r *Reader) deriveTableName(structName string) string {
	// Remove "Model" prefix if present
	name := strings.TrimPrefix(structName, "Model")

	// Convert PascalCase to snake_case
	var result strings.Builder
	for i, r := range name {
		if i > 0 && r >= 'A' && r <= 'Z' {
			result.WriteRune('_')
		}
		result.WriteRune(r)
	}

	return strings.ToLower(result.String())
}

// parseColumn parses a struct field into a Column model
func (r *Reader) parseColumn(fieldName string, fieldType ast.Expr, tag string, sequence uint) *models.Column {
	// Extract bun tag
	bunTag := r.extractBunTag(tag)
	if bunTag == "" {
		return nil
	}

	column := models.InitColumn("", "", "")
	column.Sequence = sequence

	// Parse bun tag
	parts := strings.Split(bunTag, ",")
	if len(parts) > 0 {
		column.Name = parts[0]
	}

	// Track if we found explicit nullability markers
	hasExplicitNullableMarker := false

	// Parse tag attributes
	for _, part := range parts[1:] {
		kv := strings.SplitN(part, ":", 2)
		key := kv[0]
		value := ""
		if len(kv) > 1 {
			value = kv[1]
		}

		switch key {
		case "type":
			// Parse type and extract length if present (e.g., varchar(255))
			column.Type, column.Length = r.parseTypeWithLength(value)
		case "pk":
			column.IsPrimaryKey = true
		case "notnull":
			column.NotNull = true
			hasExplicitNullableMarker = true
		case "nullzero":
			column.NotNull = false
			hasExplicitNullableMarker = true
		case "autoincrement":
			column.AutoIncrement = true
		case "default":
			// Default value from Bun tag (e.g., default:gen_random_uuid())
			column.Default = value
		}
	}

	// If no type specified in tag, derive from Go type
	if column.Type == "" {
		column.Type = r.goTypeToSQL(fieldType)
	}

	// Determine if nullable based on Go type and bun tags
	// In Bun:
	// - explicit "notnull" tag means NOT NULL
	// - explicit "nullzero" tag means nullable
	// - absence of explicit markers: infer from Go type
	if !hasExplicitNullableMarker {
		// Infer from Go type if no explicit marker found
		column.NotNull = !r.isNullableGoType(fieldType)
	}

	// Primary keys are always NOT NULL
	if column.IsPrimaryKey {
		column.NotNull = true
	}

	return column
}

// extractBunTag extracts the bun tag value from a struct tag
func (r *Reader) extractBunTag(tag string) string {
	// Remove backticks
	tag = strings.Trim(tag, "`")

	// Use reflect.StructTag to properly parse
	st := reflect.StructTag(tag)
	return st.Get("bun")
}

// parseTypeWithLength parses a type string and extracts length if present
// e.g., "varchar(255)" returns ("varchar", 255)
func (r *Reader) parseTypeWithLength(typeStr string) (baseType string, length int) {
	typeStr = strings.TrimSpace(typeStr)
	baseType = typeStr

	// Check for type with length: varchar(255), char(10), etc.
	re := regexp.MustCompile(`^([a-zA-Z\s]+)\((\d+)\)$`)
	matches := re.FindStringSubmatch(typeStr)
	if len(matches) == 3 {
		rawBaseType := strings.TrimSpace(matches[1])
		if pgsql.SupportsLength(rawBaseType) {
			if _, err := fmt.Sscanf(matches[2], "%d", &length); err == nil {
				baseType = pgsql.CanonicalizeBaseType(rawBaseType)
				return baseType, length
			}
		}
	}

	return baseType, length
}

// goTypeToSQL maps Go types to SQL types
func (r *Reader) goTypeToSQL(expr ast.Expr) string {
	switch t := expr.(type) {
	case *ast.Ident:
		switch t.Name {
		case "int", "int32":
			return "integer"
		case "int64":
			return "bigint"
		case "string":
			return "text"
		case "bool":
			return "boolean"
		case "float32":
			return "real"
		case "float64":
			return "double precision"
		}
	case *ast.SelectorExpr:
		// Handle types like time.Time, sql_types.SqlString, etc.
		if ident, ok := t.X.(*ast.Ident); ok {
			switch ident.Name {
			case "time":
				if t.Sel.Name == "Time" {
					return "timestamp"
				}
			case "sql_types":
				return r.sqlTypeToSQL(t.Sel.Name)
			}
		}
	case *ast.StarExpr:
		// Pointer type - nullable version
		return r.goTypeToSQL(t.X)
	}
	return "text"
}

// sqlTypeToSQL maps sql_types types to SQL types
func (r *Reader) sqlTypeToSQL(typeName string) string {
	switch typeName {
	case "SqlString":
		return "text"
	case "SqlInt":
		return "integer"
	case "SqlInt64":
		return "bigint"
	case "SqlFloat":
		return "double precision"
	case "SqlBool":
		return "boolean"
	case "SqlTime":
		return "timestamp"
	default:
		return "text"
	}
}

// isNullableGoType checks if a Go type represents a nullable field type
// (this is for types that CAN be nullable, not whether they ARE nullable)
func (r *Reader) isNullableGoType(expr ast.Expr) bool {
	switch t := expr.(type) {
	case *ast.StarExpr:
		// Pointer type can be nullable
		return true
	case *ast.SelectorExpr:
		// Check for sql_types nullable types
		if ident, ok := t.X.(*ast.Ident); ok {
			if ident.Name == "sql_types" {
				return strings.HasPrefix(t.Sel.Name, "Sql")
			}
		}
	}
	return false
}
