package handlers

// ext_db_schema.go
//
// 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"

	"armature/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
}

// ListExtColumns returns the set of column names for a physical table.
// Uses PRAGMA table_info on SQLite and information_schema on Postgres.
func ListExtColumns(ctx context.Context, db *sql.DB, isPostgres bool, physicalTable string) (map[string]bool, error) {
	cols := make(map[string]bool)

	var rows *sql.Rows
	var err error
	if isPostgres {
		rows, err = db.QueryContext(ctx,
			`SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public'`,
			physicalTable)
	} else {
		rows, err = db.QueryContext(ctx, fmt.Sprintf("PRAGMA table_info(%s)", physicalTable))
	}
	if err != nil {
		return nil, fmt.Errorf("list columns for %s: %w", physicalTable, err)
	}
	defer rows.Close()

	for rows.Next() {
		if isPostgres {
			var name string
			if err := rows.Scan(&name); err != nil {
				continue
			}
			cols[name] = true
		} else {
			// PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
			var cid int
			var name, colType string
			var notnull int
			var dfltValue sql.NullString
			var pk int
			if err := rows.Scan(&cid, &name, &colType, &notnull, &dfltValue, &pk); err != nil {
				continue
			}
			cols[name] = true
		}
	}
	return cols, rows.Err()
}

// MigrateExtTables applies additive-only schema changes for a package update.
// For each table in the new manifest:
//   - New table: CREATE TABLE (full creation, same as CreateExtTables)
//   - Existing table: ALTER TABLE ADD COLUMN for each new column
//
// Indexes are applied idempotently with CREATE INDEX IF NOT EXISTS.
// New tables are registered in the ext_data_tables catalog.
// Returns a log of changes applied (for API response).
//
// No destructive changes: columns present in DB but absent from manifest are NOT dropped.
func MigrateExtTables(
	ctx context.Context,
	db *sql.DB,
	isPostgres bool,
	stores store.Stores,
	packageID string,
	newTables map[string]TableDef,
) ([]string, error) {
	if db == nil {
		return nil, nil
	}

	// Get existing registered tables for this package
	existingTableNames := make(map[string]bool)
	if stores.ExtData != nil {
		names, err := stores.ExtData.ListTables(ctx, packageID)
		if err != nil {
			return nil, fmt.Errorf("list existing tables for %s: %w", packageID, err)
		}
		for _, n := range names {
			existingTableNames[n] = true
		}
	}

	var changes []string

	for logicalName, td := range newTables {
		physical := extPhysicalTable(packageID, logicalName)

		if !existingTableNames[logicalName] {
			// New table — full creation
			if err := CreateExtTables(ctx, db, isPostgres, stores, packageID,
				map[string]TableDef{logicalName: td}); err != nil {
				return changes, fmt.Errorf("create new table %s: %w", physical, err)
			}
			changes = append(changes, fmt.Sprintf("created table %s", physical))
			continue
		}

		// Existing table — diff columns and add missing ones
		existingCols, err := ListExtColumns(ctx, db, isPostgres, physical)
		if err != nil {
			return changes, err
		}

		for col, typ := range td.Columns {
			if existingCols[col] {
				continue
			}
			sqlType := mapColType(typ, isPostgres)
			alterDDL := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", physical, col, sqlType)
			if _, err := db.ExecContext(ctx, alterDDL); err != nil {
				return changes, fmt.Errorf("add column %s.%s: %w", physical, col, err)
			}
			changes = append(changes, fmt.Sprintf("added column '%s' to %s", col, physical))
			log.Printf("[ext-db] added column %s (%s) to %s", col, sqlType, physical)
		}

		// Re-apply indexes idempotently
		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)
			}
		}
	}

	return changes, 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)
}
