package replica

import (
	"context"
	"database/sql"
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

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

// BinlogSyncService handles MySQL binlog streaming with resilience features
type BinlogSyncService struct {
	syncer        *replication.BinlogSyncer
	streamer      *replication.BinlogStreamer
	position      mysql.Position
	handlers      *EventHandlers
	positionMgr   *PositionManager
	stopChan      chan struct{}
	instanceName  string
	secondaryName string
	primaryDB     *sql.DB
	lastEventPos  uint32       // for deduplication
	lastEventGTID string       // for GTID-based deduplication (future)
	healthTicker  *time.Ticker // for periodic health checks
}

// NewBinlogSyncService creates a new binlog sync service
func NewBinlogSyncService(cfg BinlogConfig, primaryDB, secondaryDB *sql.DB, secondaryName string) *BinlogSyncService {
	syncerCfg := replication.BinlogSyncerConfig{
		ServerID: cfg.ServerID,
		Flavor:   "mariadb",
		Host:     cfg.Host,
		Port:     cfg.Port,
		User:     cfg.User,
		Password: cfg.Password,
	}

	positionFile := fmt.Sprintf("binlog_position_%s_%s.json", cfg.Name, secondaryName)

	return &BinlogSyncService{
		syncer:        replication.NewBinlogSyncer(syncerCfg),
		handlers:      NewEventHandlers(secondaryDB, secondaryName),
		positionMgr:   NewPositionManager(secondaryDB, positionFile),
		stopChan:      make(chan struct{}),
		instanceName:  cfg.Name,
		secondaryName: secondaryName,
		primaryDB:     primaryDB,
	}
}

// Start begins binlog streaming
func (s *BinlogSyncService) Start() error {
	if err := s.positionMgr.InitTable(); err != nil {
		Warnf("[%s][WARN] Failed to init position table: %v", s.secondaryName, err)
	}

	pos, err := s.positionMgr.Load()
	if err != nil {
		return fmt.Errorf("failed to load position: %v", err)
	}

	if pos.Name == "" {
		s.streamer, err = s.syncer.StartSync(mysql.Position{Name: "", Pos: 4})
	} else {
		s.streamer, err = s.syncer.StartSync(pos)
	}

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

	Infof("[%s] Started streaming from %s:%d", s.instanceName, pos.Name, pos.Pos)
	return s.processEvents()
}

// StartWithStop begins binlog streaming with graceful shutdown
func (s *BinlogSyncService) StartWithStop() error {
	if err := s.positionMgr.InitTable(); err != nil {
		Warnf("[%s][WARN] Failed to init position table: %v", s.secondaryName, err)
	}

	pos, err := s.positionMgr.Load()
	if err != nil {
		return fmt.Errorf("failed to load position: %v", err)
	}

	if pos.Name == "" {
		s.streamer, err = s.syncer.StartSync(mysql.Position{Name: "", Pos: 4})
	} else {
		s.streamer, err = s.syncer.StartSync(pos)
	}

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

	Infof("[%s] Started streaming from %s:%d", s.instanceName, pos.Name, pos.Pos)
	return s.processEventsWithStop()
}

// processEventsWithStop processes events with graceful shutdown and health checks
func (s *BinlogSyncService) processEventsWithStop() error {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	// Start health check ticker (every 30 seconds)
	s.healthTicker = time.NewTicker(30 * time.Second)
	defer s.healthTicker.Stop()

	// Start health check goroutine
	healthChan := make(chan error, 1)
	go s.runHealthChecks(healthChan)

	for {
		select {
		case <-sigChan:
			Infof("[%s] Shutting down...", s.secondaryName)
			s.positionMgr.Save(s.position)
			s.syncer.Close()
			return nil
		case <-s.stopChan:
			Infof("[%s] Stop signal received", s.secondaryName)
			s.positionMgr.Save(s.position)
			s.syncer.Close()
			return nil
		case <-s.healthTicker.C:
			// Trigger health check
			go s.runHealthChecks(healthChan)
		case healthErr := <-healthChan:
			if healthErr != nil {
				Errorf("[%s][HEALTH CHECK] Failed: %v", s.secondaryName, healthErr)
			}
		default:
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			ev, err := s.streamer.GetEvent(ctx)
			cancel()

			if err == context.DeadlineExceeded {
				continue
			}
			if err != nil {
				return fmt.Errorf("failed to get event: %v", err)
			}

			s.processEvent(ev)
		}
	}
}

// runHealthChecks performs periodic health checks
func (s *BinlogSyncService) runHealthChecks(resultChan chan<- error) {
	// Check secondary DB connection
	if err := s.handlers.PingSecondary(); err != nil {
		resultChan <- fmt.Errorf("secondary DB ping failed: %v", err)
		return
	}

	// Check if syncer is still healthy
	if s.syncer == nil {
		resultChan <- fmt.Errorf("syncer is nil")
		return
	}

	resultChan <- nil
}

// processEvents processes events continuously
func (s *BinlogSyncService) processEvents() error {
	for {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		ev, err := s.streamer.GetEvent(ctx)
		cancel()

		if err == context.DeadlineExceeded {
			continue
		}
		if err != nil {
			return fmt.Errorf("failed to get event: %v", err)
		}

		s.processEvent(ev)
	}
}

// processEvent processes a single event with panic recovery
// Note: Deduplication disabled as it was causing valid events to be skipped
func (s *BinlogSyncService) processEvent(ev *replication.BinlogEvent) {
	defer func() {
		if r := recover(); r != nil {
			Errorf("[%s][PANIC RECOVERED] processEvent panic: %v", s.secondaryName, r)
		}
	}()

	s.position.Pos = ev.Header.LogPos

	switch e := ev.Event.(type) {
	case *replication.RotateEvent:
		s.position.Name = string(e.NextLogName)
		Infof("[%s] Rotated to %s", s.instanceName, s.position.Name)

	case *replication.TableMapEvent:
		s.handlers.HandleTableMap(e)

	case *replication.RowsEvent:
		s.handlers.HandleRows(ev.Header, e)

	case *replication.QueryEvent:
		s.handlers.HandleQuery(e)

	case *replication.XIDEvent:
		s.positionMgr.Save(s.position)
	}

	if ev.Header.LogPos%1000 == 0 {
		s.positionMgr.Save(s.position)
	}
}

// Stop signals the service to stop
func (s *BinlogSyncService) Stop() {
	close(s.stopChan)
}

// NeedResync checks if a resync is needed
func (s *BinlogSyncService) NeedResync() (bool, error) {
	// Check if position file/table exists
	pos, err := s.positionMgr.Load()
	if err != nil {
		return false, err
	}

	// If no position saved, we need initial transfer
	if pos.Name == "" {
		Infof("[%s] No saved position found, resync needed", s.secondaryName)
		return true, nil
	}

	// Check if secondary DB has any tables/data
	hasData, err := s.checkSecondaryHasData()
	if err != nil {
		return false, err
	}

	if !hasData {
		Infof("[%s] Secondary database is empty, resync needed", s.secondaryName)
		return true, nil
	}

	return false, nil
}

// checkSecondaryHasData checks if the secondary database has any data
func (s *BinlogSyncService) checkSecondaryHasData() (bool, error) {
	// Get the secondary DB from position manager
	db := s.positionMgr.db
	if db == nil {
		return true, nil // Can't check, assume OK
	}

	// Check if any table has data
	var count int
	err := db.QueryRow("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'replica'").Scan(&count)
	if err != nil {
		return false, err
	}

	return count > 0, nil
}

// RunInitialTransfer performs the initial data transfer
func (s *BinlogSyncService) RunInitialTransfer(batchSize int, excludeSchemas []string) error {
	Infof("[%s] Starting initial data transfer...", s.secondaryName)

	// Get secondary DB from handlers
	secondaryDB := s.handlers.secondaryDB
	if secondaryDB == nil {
		return fmt.Errorf("secondary DB not available")
	}

	// Create initial transfer with proper initialization
	transfer := &InitialTransfer{
		primaryDB:   s.primaryDB,
		secondaryDB: secondaryDB,
		batchSize:   batchSize,
		workerCount: 1,
		excludedDBs: map[string]bool{
			"information_schema": true,
			"performance_schema": true,
			"mysql":              true,
			"sys":                true,
		},
		checkpointFile: fmt.Sprintf("transfer_progress_%s_%s.json", s.instanceName, s.secondaryName),
		progress: TransferProgress{
			TablesProcessed: make(map[string]int64),
		},
	}

	if err := transfer.Transfer(excludeSchemas); err != nil {
		return fmt.Errorf("transfer failed: %v", err)
	}

	// Reset position after successful transfer
	if err := s.positionMgr.Reset(); err != nil {
		return fmt.Errorf("failed to reset position: %v", err)
	}

	Infof("[%s] Initial transfer completed successfully", s.secondaryName)
	return nil
}

// StartWithResync starts binlog streaming with automatic resync if needed
func (s *BinlogSyncService) StartWithResync(batchSize int, excludeSchemas []string) error {
	if err := s.positionMgr.InitTable(); err != nil {
		Warnf("[%s][WARN] Failed to init position table: %v", s.secondaryName, err)
	}

	// Check if resync is needed
	needResync, err := s.NeedResync()
	if err != nil {
		return fmt.Errorf("failed to check resync status: %v", err)
	}

	if needResync {
		if err := s.RunInitialTransfer(batchSize, excludeSchemas); err != nil {
			return fmt.Errorf("initial transfer failed: %v", err)
		}
	}

	// Start normal streaming
	return s.StartWithStop()
}

// GetSecondaryName returns the secondary name
func (s *BinlogSyncService) GetSecondaryName() string {
	return s.secondaryName
}

// MultiBinlogSyncService manages multiple binlog sync services
type MultiBinlogSyncService struct {
	services []*BinlogSyncService
	stopChan chan struct{}
	wg       sync.WaitGroup
}

// NewMultiBinlogSyncService creates a new multi-secondary binlog sync service
func NewMultiBinlogSyncService() *MultiBinlogSyncService {
	return &MultiBinlogSyncService{
		services: make([]*BinlogSyncService, 0),
		stopChan: make(chan struct{}),
	}
}

// AddService adds a service to the multi-service manager
func (m *MultiBinlogSyncService) AddService(service *BinlogSyncService) {
	m.services = append(m.services, service)
}

// StartAll starts all services
func (m *MultiBinlogSyncService) StartAll() error {
	for _, service := range m.services {
		m.wg.Add(1)
		go func(svc *BinlogSyncService) {
			defer m.wg.Done()
			Infof("[%s] Starting binlog sync service", svc.GetSecondaryName())
			if err := svc.StartWithResync(1000, []string{"information_schema", "performance_schema", "mysql", "sys"}); err != nil {
				Errorf("[%s] Service error: %v", svc.GetSecondaryName(), err)
			}
		}(service)
	}
	return nil
}

// StopAll stops all services
func (m *MultiBinlogSyncService) StopAll() {
	close(m.stopChan)
	for _, service := range m.services {
		service.Stop()
	}
	m.wg.Wait()
}

// Wait waits for all services to complete
func (m *MultiBinlogSyncService) Wait() {
	m.wg.Wait()
}

// Len returns the number of services
func (m *MultiBinlogSyncService) Len() int {
	return len(m.services)
}
