package handlers

// ext_db_schema.go — v0.29.2
//
// DDL generation and lifecycle management for extension-owned database tables.
// Tables are namespaced as ext_{pkg_slug}_{logical_name} and tracked in the
// ext_data_tables catalog.  CreateExtTables is called on install;
// DropExtTables is called on uninstall.

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"regexp"
	"strings"

	"switchboard-core/store"
)

// validSchemaIdentifier matches safe SQL identifiers for table and column names.
var validSchemaIdentifier = regexp.MustCompile(`^[a-z][a-z0-9_]{0,62}$`)

// TableDef describes a single extension-owned table as declared in a manifest.
type TableDef struct {
	Columns map[string]string // colName → manifest type ("text","int","real","bool","timestamp")
	Indexes [][]string        // each inner slice is an ordered list of column names for one index
}

// ParseDBTables extracts the "db_tables" key from a package manifest map.
// Returns the map of logicalName→TableDef and ok=true when the key is present
// and produces at least one valid table definition.
func ParseDBTables(manifest map[string]any) (map[string]TableDef, bool) {
	raw, ok := manifest["db_tables"]
	if !ok {
		return nil, false
	}
	tablesRaw, ok := raw.(map[string]any)
	if !ok || len(tablesRaw) == 0 {
		return nil, false
	}

	tables := make(map[string]TableDef, len(tablesRaw))
	for name, defRaw := range tablesRaw {
		if !validSchemaIdentifier.MatchString(name) {
			log.Printf("[ext-db] skipping invalid table name %q", name)
			continue
		}
		defMap, ok := defRaw.(map[string]any)
		if !ok {
			continue
		}

		td := TableDef{Columns: map[string]string{}}

		// Parse columns map.
		if colsRaw, ok := defMap["columns"].(map[string]any); ok {
			for col, typ := range colsRaw {
				if !validSchemaIdentifier.MatchString(col) {
					log.Printf("[ext-db] skipping invalid column name %q in table %q", col, name)
					continue
				}
				if t, ok := typ.(string); ok {
					td.Columns[col] = t
				}
			}
		}

		// Parse indexes array.
		if idxsRaw, ok := defMap["indexes"].([]any); ok {
			for _, idxRaw := range idxsRaw {
				if idxArr, ok := idxRaw.([]any); ok {
					var cols []string
					for _, c := range idxArr {
						if s, ok := c.(string); ok && validSchemaIdentifier.MatchString(s) {
							cols = append(cols, s)
						}
					}
					if len(cols) > 0 {
						td.Indexes = append(td.Indexes, cols)
					}
				}
			}
		}

		tables[name] = td
	}

	if len(tables) == 0 {
		return nil, false
	}
	return tables, true
}

// mapColType maps a manifest type string to a SQL column type for the given dialect.
func mapColType(typStr string, isPostgres bool) string {
	switch strings.ToLower(typStr) {
	case "int", "integer":
		return "INTEGER"
	case "real", "float":
		return "REAL"
	case "bool", "boolean":
		if isPostgres {
			return "BOOLEAN"
		}
		return "INTEGER"
	case "timestamp":
		if isPostgres {
			return "TIMESTAMPTZ"
		}
		return "TEXT"
	default: // "text" and anything unrecognized
		return "TEXT"
	}
}

// extPhysicalTable builds the namespaced physical table name for a package's logical table.
// e.g. packageID="my-ext", logicalName="logs" → "ext_my_ext_logs"
func extPhysicalTable(packageID, logicalName string) string {
	slug := strings.ReplaceAll(packageID, "-", "_")
	return "ext_" + slug + "_" + logicalName
}

// createdAtColDef returns the dialect-correct DDL fragment for the created_at column.
func createdAtColDef(isPostgres bool) string {
	if isPostgres {
		return "created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
	}
	return "created_at TEXT NOT NULL DEFAULT (datetime('now'))"
}

// CreateExtTables generates dialect-correct DDL and executes it for all tables
// in the provided map.  Each successfully created table is registered in the
// ext_data_tables catalog via stores.ExtData.
//
// Errors from individual table creation are returned immediately (fail-fast).
// Index creation failures are logged but non-fatal.
// Catalog registration failures are logged but non-fatal.
func CreateExtTables(
	ctx context.Context,
	db *sql.DB,
	isPostgres bool,
	stores store.Stores,
	packageID string,
	tables map[string]TableDef,
) error {
	if db == nil {
		return nil
	}
	for logicalName, td := range tables {
		physical := extPhysicalTable(packageID, logicalName)

		// Build column list: id first, user columns, created_at last.
		cols := []string{"id TEXT PRIMARY KEY"}
		for col, typ := range td.Columns {
			cols = append(cols, col+" "+mapColType(typ, isPostgres))
		}
		cols = append(cols, createdAtColDef(isPostgres))

		ddl := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n\t%s\n)",
			physical, strings.Join(cols, ",\n\t"))

		if _, err := db.ExecContext(ctx, ddl); err != nil {
			return fmt.Errorf("create ext table %s: %w", physical, err)
		}

		// Create declared indexes.
		for i, idxCols := range td.Indexes {
			idxName := fmt.Sprintf("idx_%s_%d", physical, i)
			idxDDL := fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s (%s)",
				idxName, physical, strings.Join(idxCols, ", "))
			if _, err := db.ExecContext(ctx, idxDDL); err != nil {
				log.Printf("[ext-db] index create failed (%s): %v", idxName, err)
			}
		}

		// Record logical name in catalog.
		if stores.ExtData != nil {
			if err := stores.ExtData.RegisterTable(ctx, packageID, logicalName); err != nil {
				log.Printf("[ext-db] catalog register failed (%s/%s): %v", packageID, logicalName, err)
			}
		}

		log.Printf("[ext-db] created table %s for package %s", physical, packageID)
	}
	return nil
}

// DropExtTables drops all physical tables registered for a package and removes
// their entries from the ext_data_tables catalog.
//
// Table drop errors are logged but non-fatal; the catalog is cleared regardless.
func DropExtTables(
	ctx context.Context,
	db *sql.DB,
	stores store.Stores,
	packageID string,
) error {
	if db == nil || stores.ExtData == nil {
		return nil
	}
	tables, err := stores.ExtData.ListTables(ctx, packageID)
	if err != nil {
		return fmt.Errorf("list ext tables for %s: %w", packageID, err)
	}
	for _, logicalName := range tables {
		physical := extPhysicalTable(packageID, logicalName)
		if _, err := db.ExecContext(ctx, "DROP TABLE IF EXISTS "+physical); err != nil {
			log.Printf("[ext-db] drop table %s failed: %v", physical, err)
		} else {
			log.Printf("[ext-db] dropped table %s for package %s", physical, packageID)
		}
	}
	return stores.ExtData.DeletePackageTables(ctx, packageID)
}
