package handlers

import (
	"bufio"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"strings"

	"switchboard-core/database"
)

// coreTableOrder lists tables for backup in FK-safe import order.
// Ephemeral tables (node_registry, ws_tickets, rate_limit_counters,
// user_presence, oidc_auth_state, refresh_tokens) are excluded.
var coreTableOrder = []string{
	"schema_migrations",
	"users",
	"teams",
	"team_members",
	"groups",
	"group_members",
	"platform_policies",
	"global_settings",
	"packages",
	"package_user_settings",
	"package_team_settings",
	"extension_permissions",
	"ext_data_tables",
	"ext_connections",
	"ext_dependencies",
	"resource_grants",
	"notifications",
	"notification_preferences",
	"audit_log",
	"workflows",
	"workflow_stages",
	"workflow_versions",
	"workflow_instances",
	"workflow_assignments",
	"workflow_signoffs",
	"triggers",
	"scheduled_tasks",
	"trigger_logs",
}

// dumpTable writes all rows of a table as newline-delimited JSON to w.
// Columns are discovered dynamically via rows.ColumnTypes().
func dumpTable(ctx context.Context, db *sql.DB, table string, w io.Writer) (int, error) {
	rows, err := db.QueryContext(ctx, fmt.Sprintf("SELECT * FROM %s", table))
	if err != nil {
		return 0, fmt.Errorf("query %s: %w", table, err)
	}
	defer rows.Close()

	cols, err := rows.Columns()
	if err != nil {
		return 0, fmt.Errorf("columns %s: %w", table, err)
	}

	count := 0
	enc := json.NewEncoder(w)
	enc.SetEscapeHTML(false)

	for rows.Next() {
		// Create scan destinations — all as interface{}
		vals := make([]interface{}, len(cols))
		ptrs := make([]interface{}, len(cols))
		for i := range vals {
			ptrs[i] = &vals[i]
		}
		if err := rows.Scan(ptrs...); err != nil {
			return count, fmt.Errorf("scan %s row %d: %w", table, count, err)
		}

		row := make(map[string]interface{}, len(cols))
		for i, col := range cols {
			row[col] = normalizeValue(vals[i])
		}
		if err := enc.Encode(row); err != nil {
			return count, fmt.Errorf("encode %s row %d: %w", table, count, err)
		}
		count++
	}
	return count, rows.Err()
}

// normalizeValue converts database-specific types to JSON-friendly values.
func normalizeValue(v interface{}) interface{} {
	if v == nil {
		return nil
	}
	switch val := v.(type) {
	case []byte:
		// Try to parse as JSON first (for JSONB columns)
		var j interface{}
		if err := json.Unmarshal(val, &j); err == nil {
			return j
		}
		return string(val)
	default:
		return val
	}
}

// restoreTable reads JSONL from r and inserts rows into the table.
// Column names come from the first JSON object. Uses dialect-adapted SQL.
func restoreTable(ctx context.Context, db *sql.DB, table string, r io.Reader) (int, error) {
	scanner := bufio.NewScanner(r)
	// Allow large lines (e.g. JSONB audit_log entries)
	scanner.Buffer(make([]byte, 0, 64*1024), 10*1024*1024)

	count := 0
	for scanner.Scan() {
		line := scanner.Bytes()
		if len(line) == 0 {
			continue
		}
		var row map[string]interface{}
		if err := json.Unmarshal(line, &row); err != nil {
			return count, fmt.Errorf("unmarshal %s line %d: %w", table, count+1, err)
		}

		cols := make([]string, 0, len(row))
		vals := make([]interface{}, 0, len(row))
		for k, v := range row {
			cols = append(cols, k)
			vals = append(vals, marshalIfComplex(v))
		}

		// Build INSERT with positional placeholders
		placeholders := make([]string, len(cols))
		for i := range placeholders {
			placeholders[i] = fmt.Sprintf("$%d", i+1)
		}
		query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
			table,
			strings.Join(cols, ", "),
			strings.Join(placeholders, ", "),
		)

		if _, err := database.ExecContext(ctx, query, vals...); err != nil {
			return count, fmt.Errorf("insert %s row %d: %w", table, count+1, err)
		}
		count++
	}
	return count, scanner.Err()
}

// marshalIfComplex converts maps/slices back to JSON strings for DB storage.
func marshalIfComplex(v interface{}) interface{} {
	if v == nil {
		return nil
	}
	switch v.(type) {
	case map[string]interface{}, []interface{}:
		b, _ := json.Marshal(v)
		return string(b)
	default:
		return v
	}
}

// listExtDataTables returns all (package_id, physical_table_name) pairs
// from the ext_data_tables catalog.
func listExtDataTables(ctx context.Context, db *sql.DB) ([]extDataTableEntry, error) {
	rows, err := db.QueryContext(ctx, "SELECT package_id, table_name FROM ext_data_tables ORDER BY package_id, table_name")
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var entries []extDataTableEntry
	for rows.Next() {
		var e extDataTableEntry
		if err := rows.Scan(&e.PackageID, &e.TableName); err != nil {
			return nil, err
		}
		e.PhysicalName = fmt.Sprintf("ext_%s_%s", e.PackageID, e.TableName)
		entries = append(entries, e)
	}
	return entries, rows.Err()
}

type extDataTableEntry struct {
	PackageID    string
	TableName    string
	PhysicalName string
}

// tableExists checks if a table exists in the database.
func tableExists(ctx context.Context, db *sql.DB, table string) bool {
	if database.IsPostgres() {
		var exists bool
		db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = $1)", table).Scan(&exists)
		return exists
	}
	// SQLite
	var name string
	err := db.QueryRowContext(ctx, "SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name)
	return err == nil
}

// wipeTables deletes all data from the given tables in reverse order (FK-safe).
func wipeTables(ctx context.Context, db *sql.DB, tables []string) error {
	if database.IsSQLite() {
		db.ExecContext(ctx, "PRAGMA foreign_keys = OFF")
		defer db.ExecContext(ctx, "PRAGMA foreign_keys = ON")
	}

	// Delete in reverse order to respect FK constraints
	for i := len(tables) - 1; i >= 0; i-- {
		table := tables[i]
		if !tableExists(ctx, db, table) {
			continue
		}
		if database.IsPostgres() {
			if _, err := db.ExecContext(ctx, fmt.Sprintf("TRUNCATE TABLE %s CASCADE", table)); err != nil {
				log.Printf("[backup] warning: truncate %s: %v", table, err)
			}
		} else {
			if _, err := db.ExecContext(ctx, fmt.Sprintf("DELETE FROM %s", table)); err != nil {
				log.Printf("[backup] warning: delete %s: %v", table, err)
			}
		}
	}
	return nil
}
