package replica

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

// TransferStats holds statistics about the transfer
type TransferStats struct {
	TotalRows    int64
	TotalTables  int
	TransferTime int64 // in milliseconds
	Errors       []string
	Progress     TransferProgress
}

// TransferProgress tracks the current position in the transfer
type TransferProgress struct {
	DatabasesProcessed int
	CurrentDatabase    string
	TablesProcessed    map[string]int64 // tableName -> rows transferred
	LastCheckpoint     time.Time
}

// InitialTransfer handles the initial data transfer from primary to secondary
type InitialTransfer struct {
	primaryDB      *sql.DB
	secondaryDB    *sql.DB
	batchSize      int
	workerCount    int
	excludedDBs    map[string]bool
	mu             sync.Mutex
	stats          TransferStats
	checkpointFile string
	progress       TransferProgress
	pauseChan      chan struct{}
	resumeChan     chan struct{}
	isPaused       bool
}

// NewInitialTransfer creates a new initial transfer handler
func NewInitialTransfer(primaryDSN, secondaryDSN string, batchSize, workerCount int) (*InitialTransfer, error) {
	primaryDB, err := sql.Open("mysql", primaryDSN)
	if err != nil {
		return nil, fmt.Errorf("failed to connect to primary: %v", err)
	}
	primaryDB.SetMaxOpenConns(batchSize)
	primaryDB.SetMaxIdleConns(2)

	secondaryDB, err := sql.Open("mysql", secondaryDSN)
	if err != nil {
		primaryDB.Close()
		return nil, fmt.Errorf("failed to connect to secondary: %v", err)
	}
	secondaryDB.SetMaxOpenConns(batchSize)
	secondaryDB.SetMaxIdleConns(2)

	if err := primaryDB.Ping(); err != nil {
		primaryDB.Close()
		secondaryDB.Close()
		return nil, fmt.Errorf("failed to ping primary: %v", err)
	}

	if err := secondaryDB.Ping(); err != nil {
		primaryDB.Close()
		secondaryDB.Close()
		return nil, fmt.Errorf("failed to ping secondary: %v", err)
	}

	return &InitialTransfer{
		primaryDB:   primaryDB,
		secondaryDB: secondaryDB,
		batchSize:   batchSize,
		workerCount: workerCount,
		excludedDBs: map[string]bool{
			"information_schema": true,
			"performance_schema": true,
			"mysql":              true,
			"sys":                true,
		},
		checkpointFile: "transfer_progress.json",
		progress: TransferProgress{
			TablesProcessed: make(map[string]int64),
		},
		pauseChan:  make(chan struct{}),
		resumeChan: make(chan struct{}),
	}, nil
}

// Transfer executes the initial data transfer
func (t *InitialTransfer) Transfer(excludeSchemas []string) error {
	startTime := time.Now()

	// Load previous progress if exists
	t.loadProgress()

	// Add user-specified exclusions
	for _, db := range excludeSchemas {
		t.excludedDBs[db] = true
	}

	// Get list of databases to transfer
	databases, err := t.getDatabases()
	if err != nil {
		return fmt.Errorf("failed to get databases: %v", err)
	}

	Info("[TRANSFER] Starting initial data transfer...")
	Infof("[TRANSFER] Found %d databases to transfer", len(databases))

	// Get current binlog position before transfer
	binlogPos, err := t.getBinlogPosition()
	if err != nil {
		Warnf("[WARN] Failed to get binlog position: %v", err)
	}

	for i, dbName := range databases {
		// Check for pause signal
		t.checkPause()

		if t.excludedDBs[dbName] {
			Infof("[TRANSFER] Skipping excluded database: %s", dbName)
			continue
		}

		// Skip already processed databases
		if i < t.progress.DatabasesProcessed {
			Infof("[TRANSFER] Skipping already processed database: %s", dbName)
			continue
		}

		t.progress.CurrentDatabase = dbName
		t.saveProgress()

		if err := t.transferDatabase(dbName); err != nil {
			return fmt.Errorf("failed to transfer database %s: %v", dbName, err)
		}

		t.progress.DatabasesProcessed++
		t.saveProgress()
	}

	t.stats.TransferTime = time.Since(startTime).Milliseconds()

	Infof("[TRANSFER] Transfer completed: %d tables, %d rows in %dms",
		t.stats.TotalTables, t.stats.TotalRows, t.stats.TransferTime)

	if binlogPos != "" {
		Infof("[TRANSFER] Binlog position before transfer: %s", binlogPos)
	}

	// Clear progress on successful completion
	t.clearProgress()

	return nil
}

// checkPause checks if transfer should be paused
func (t *InitialTransfer) checkPause() {
	if t.isPaused {
		Info("[TRANSFER] Transfer paused, waiting for resume...")
		<-t.resumeChan
		Info("[TRANSFER] Transfer resumed")
	}
}

// Pause pauses the transfer
func (t *InitialTransfer) Pause() {
	if !t.isPaused {
		t.isPaused = true
		t.pauseChan <- struct{}{}
		Info("[TRANSFER] Transfer pause requested")
	}
}

// Resume resumes the transfer
func (t *InitialTransfer) Resume() {
	if t.isPaused {
		t.isPaused = false
		t.resumeChan <- struct{}{}
		Info("[TRANSFER] Transfer resume requested")
	}
}

// saveProgress saves the current progress to a checkpoint file
func (t *InitialTransfer) saveProgress() {
	t.progress.LastCheckpoint = time.Now()

	data, err := json.MarshalIndent(t.progress, "", "  ")
	if err != nil {
		Warnf("[WARN] Failed to marshal progress: %v", err)
		return
	}

	err = os.WriteFile(t.checkpointFile, data, 0644)
	if err != nil {
		Warnf("[WARN] Failed to save progress: %v", err)
	}
}

// loadProgress loads previous progress from checkpoint file
func (t *InitialTransfer) loadProgress() {
	data, err := os.ReadFile(t.checkpointFile)
	if err != nil {
		Info("[INFO] No previous progress found, starting fresh")
		return
	}

	var progress TransferProgress
	if err := json.Unmarshal(data, &progress); err != nil {
		Warnf("[WARN] Failed to load progress: %v", err)
		return
	}

	t.progress = progress
	Infof("[INFO] Resuming transfer: %d databases, %d tables in progress",
		progress.DatabasesProcessed, len(progress.TablesProcessed))
}

// clearProgress removes the checkpoint file
func (t *InitialTransfer) clearProgress() {
	os.Remove(t.checkpointFile)
}

// GetProgress returns the current transfer progress
func (t *InitialTransfer) GetProgress() TransferProgress {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.progress
}

// getDatabases returns list of databases to transfer
func (t *InitialTransfer) getDatabases() ([]string, error) {
	rows, err := t.primaryDB.Query("SHOW DATABASES")
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var databases []string
	for rows.Next() {
		var dbName string
		if err := rows.Scan(&dbName); err != nil {
			return nil, err
		}
		databases = append(databases, dbName)
	}

	return databases, rows.Err()
}

// transferDatabase transfers all tables in a database
func (t *InitialTransfer) transferDatabase(dbName string) error {
	Infof("[TRANSFER] Transferring database: %s", dbName)

	// Get list of tables
	tables, err := t.getTables(dbName)
	if err != nil {
		return fmt.Errorf("failed to get tables: %v", err)
	}

	Infof("[TRANSFER] Found %d tables in %s", len(tables), dbName)

	for _, table := range tables {
		// Check for pause signal
		t.checkPause()

		// Skip already processed tables
		tableKey := dbName + "." + table
		if t.progress.TablesProcessed[tableKey] > 0 {
			Infof("[TRANSFER] Skipping already processed table: %s", tableKey)
			continue
		}

		if err := t.transferTable(dbName, table); err != nil {
			Errorf("[ERROR] Failed to transfer table %s.%s: %v", dbName, table, err)
			t.mu.Lock()
			t.stats.Errors = append(t.stats.Errors, fmt.Sprintf("%s.%s: %v", dbName, table, err))
			t.mu.Unlock()
		}
	}

	return nil
}

// getTables returns list of tables in a database
func (t *InitialTransfer) getTables(dbName string) ([]string, error) {
	query := fmt.Sprintf("SHOW TABLES FROM `%s`", dbName)
	rows, err := t.primaryDB.Query(query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var tables []string
	for rows.Next() {
		var tableName string
		if err := rows.Scan(&tableName); err != nil {
			return nil, err
		}
		tables = append(tables, tableName)
	}

	return tables, rows.Err()
}

// transferTable transfers a single table with chunked reads
func (t *InitialTransfer) transferTable(dbName, tableName string) error {
	// Get table structure
	schema, err := t.getTableSchema(dbName, tableName)
	if err != nil {
		return fmt.Errorf("failed to get table schema: %v", err)
	}

	// Check if table has data
	hasData, err := t.tableHasData(dbName, tableName)
	if err != nil {
		return fmt.Errorf("failed to check table data: %v", err)
	}

	if !hasData {
		Infof("[TRANSFER] Table %s.%s is empty, skipping data transfer", dbName, tableName)
		return nil
	}

	// Get row count
	count, err := t.getRowCount(dbName, tableName)
	if err != nil {
		return fmt.Errorf("failed to get row count: %v", err)
	}

	Infof("[TRANSFER] Transferring %s.%s (%d rows)", dbName, tableName, count)

	// Get primary key or first unique column for chunking
	pkColumn, err := t.getPrimaryKey(dbName, tableName)
	if err != nil {
		Warnf("[WARN] No primary key found for %s.%s, using full scan", dbName, tableName)
		return t.transferTableFullScan(dbName, tableName, schema, count)
	}

	return t.transferTableChunked(dbName, tableName, schema, pkColumn, count)
}

// tableHasData checks if a table has any rows
func (t *InitialTransfer) tableHasData(dbName, tableName string) (bool, error) {
	query := fmt.Sprintf("SELECT 1 FROM `%s`.`%s` LIMIT 1", dbName, tableName)
	var exists int
	err := t.primaryDB.QueryRow(query).Scan(&exists)
	if err == sql.ErrNoRows {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// getRowCount returns the number of rows in a table
func (t *InitialTransfer) getRowCount(dbName, tableName string) (int64, error) {
	query := fmt.Sprintf("SELECT COUNT(*) FROM `%s`.`%s`", dbName, tableName)
	var count int64
	err := t.primaryDB.QueryRow(query).Scan(&count)
	return count, err
}

// getTableSchema returns the column definitions for a table
func (t *InitialTransfer) getTableSchema(dbName, tableName string) ([]ColumnInfo, error) {
	query := fmt.Sprintf(
		"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA "+
			"FROM INFORMATION_SCHEMA.COLUMNS "+
			"WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' "+
			"ORDER BY ORDINAL_POSITION",
		dbName, tableName,
	)

	rows, err := t.primaryDB.Query(query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var columns []ColumnInfo
	for rows.Next() {
		var col ColumnInfo
		if err := rows.Scan(&col.Name, &col.Type, &col.Nullable, &col.Key, &col.Extra); err != nil {
			return nil, err
		}
		columns = append(columns, col)
	}

	return columns, rows.Err()
}

// ColumnInfo holds information about a table column
type ColumnInfo struct {
	Name     string
	Type     string
	Nullable string
	Key      string
	Extra    string
}

// getPrimaryKey returns the primary key column for a table
func (t *InitialTransfer) getPrimaryKey(dbName, tableName string) (string, error) {
	query := fmt.Sprintf(
		"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE "+
			"WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' AND CONSTRAINT_NAME = 'PRIMARY' "+
			"ORDER BY ORDINAL_POSITION LIMIT 1",
		dbName, tableName,
	)

	var pkColumn string
	err := t.primaryDB.QueryRow(query).Scan(&pkColumn)
	if err == sql.ErrNoRows {
		return "", fmt.Errorf("no primary key")
	}
	return pkColumn, err
}

// transferTableChunked transfers a table using primary key chunks
func (t *InitialTransfer) transferTableChunked(dbName, tableName string, columns []ColumnInfo, pkColumn string, rowCount int64) error {
	tableKey := dbName + "." + tableName

	// Get starting offset from progress
	startOffset := t.progress.TablesProcessed[tableKey]

	// Get min and max primary key values
	minMax, err := t.getMinMaxPK(dbName, tableName, pkColumn)
	if err != nil {
		return fmt.Errorf("failed to get min/max: %v", err)
	}

	// Adjust start offset to be within range
	if startOffset < minMax.Min {
		startOffset = minMax.Min
	}

	// Transfer chunks
	offset := startOffset
	for offset < minMax.Max {
		// Check for pause signal
		t.checkPause()

		query := fmt.Sprintf(
			"SELECT * FROM `%s`.`%s` WHERE `%s` >= %d AND `%s` < %d ORDER BY `%s`",
			dbName, tableName, pkColumn, offset, pkColumn, offset+int64(t.batchSize), pkColumn,
		)

		rows, err := t.primaryDB.Query(query)
		if err != nil {
			return fmt.Errorf("failed to query chunk: %v", err)
		}

		rowsInserted, err := t.insertRows(t.secondaryDB, dbName, tableName, columns, rows)
		rows.Close()

		if err != nil {
			return fmt.Errorf("failed to insert chunk: %v", err)
		}

		// Update progress
		t.mu.Lock()
		t.progress.TablesProcessed[tableKey] = offset + int64(t.batchSize)
		t.mu.Unlock()

		// Save checkpoint every 1000 rows
		if rowsInserted%1000 == 0 {
			t.saveProgress()
		}

		offset += int64(t.batchSize)
	}

	return nil
}

// MinMax holds min and max values
type MinMax struct {
	Min int64
	Max int64
}

// getMinMaxPK returns the min and max primary key values
func (t *InitialTransfer) getMinMaxPK(dbName, tableName, pkColumn string) (*MinMax, error) {
	query := fmt.Sprintf(
		"SELECT COALESCE(MIN(`%s`), 0), COALESCE(MAX(`%s`), 0) FROM `%s`.`%s`",
		pkColumn, pkColumn, dbName, tableName,
	)

	var minVal, maxVal int64
	err := t.primaryDB.QueryRow(query).Scan(&minVal, &maxVal)
	if err != nil {
		return nil, err
	}

	return &MinMax{Min: minVal, Max: maxVal + 1}, nil
}

// transferTableFullScan transfers a table without primary key (slower, uses LIMIT/OFFSET)
func (t *InitialTransfer) transferTableFullScan(dbName, tableName string, columns []ColumnInfo, rowCount int64) error {
	tableKey := dbName + "." + tableName

	// Get starting offset from progress
	startOffset := t.progress.TablesProcessed[tableKey]

	var offset int64 = startOffset

	for offset < rowCount {
		// Check for pause signal
		t.checkPause()

		query := fmt.Sprintf(
			"SELECT * FROM `%s`.`%s` LIMIT %d OFFSET %d",
			dbName, tableName, t.batchSize, offset,
		)

		rows, err := t.primaryDB.Query(query)
		if err != nil {
			return fmt.Errorf("failed to query chunk: %v", err)
		}

		rowsInserted, err := t.insertRows(t.secondaryDB, dbName, tableName, columns, rows)
		rows.Close()

		if err != nil {
			return fmt.Errorf("failed to insert chunk: %v", err)
		}

		// Update progress
		t.mu.Lock()
		t.progress.TablesProcessed[tableKey] = offset
		t.mu.Unlock()

		// Save checkpoint every 1000 rows
		if rowsInserted%1000 == 0 {
			t.saveProgress()
		}

		offset += int64(t.batchSize)
	}

	return nil
}

// insertRows inserts rows from a query result into the secondary database
func (t *InitialTransfer) insertRows(db *sql.DB, dbName, tableName string, columns []ColumnInfo, rows *sql.Rows) (int64, error) {
	// Build INSERT statement
	placeholders := make([]string, len(columns))
	colNames := make([]string, len(columns))
	for i := range columns {
		placeholders[i] = "?"
		colNames[i] = fmt.Sprintf("`%s`", columns[i].Name)
	}

	insertSQL := fmt.Sprintf(
		"INSERT INTO `%s`.`%s` (%s) VALUES (%s)",
		dbName, tableName, strings.Join(colNames, ", "), strings.Join(placeholders, ", "),
	)

	// Prepare statement
	stmt, err := db.Prepare(insertSQL)
	if err != nil {
		return 0, fmt.Errorf("failed to prepare statement: %v", err)
	}
	defer stmt.Close()

	// Insert rows
	rowCount := int64(0)
	for rows.Next() {
		values := make([]interface{}, len(columns))
		valuePtrs := make([]interface{}, len(columns))

		for i := range values {
			valuePtrs[i] = &values[i]
		}

		if err := rows.Scan(valuePtrs...); err != nil {
			return rowCount, fmt.Errorf("failed to scan row: %v", err)
		}

		_, err := stmt.Exec(values...)
		if err != nil {
			return rowCount, fmt.Errorf("failed to insert row: %v", err)
		}
		rowCount++
	}

	t.mu.Lock()
	t.stats.TotalRows += rowCount
	t.stats.TotalTables++
	t.mu.Unlock()

	Infof("[TRANSFER] Inserted %d rows into %s.%s", rowCount, dbName, tableName)

	return rowCount, rows.Err()
}

// getBinlogPosition returns the current binlog position
func (t *InitialTransfer) getBinlogPosition() (string, error) {
	var file string
	var pos uint32
	var doDB, ignoreDB string // Ignore extra columns from SHOW MASTER STATUS

	err := t.primaryDB.QueryRow("SHOW MASTER STATUS").Scan(&file, &pos, &doDB, &ignoreDB)
	if err == sql.ErrNoRows {
		return "", nil
	}
	if err != nil {
		return "", err
	}

	return fmt.Sprintf("%s:%d", file, pos), nil
}

// Close closes the database connections
func (t *InitialTransfer) Close() error {
	if err := t.primaryDB.Close(); err != nil {
		return err
	}
	return t.secondaryDB.Close()
}

// GetStats returns the transfer statistics
func (t *InitialTransfer) GetStats() TransferStats {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.stats
}
