package whatshooked

import (
	"context"
	"encoding/json"
	"time"

	"git.warky.dev/wdevs/whatshooked/pkg/api"
	"git.warky.dev/wdevs/whatshooked/pkg/cache"
	"git.warky.dev/wdevs/whatshooked/pkg/config"
	"git.warky.dev/wdevs/whatshooked/pkg/eventlogger"
	"git.warky.dev/wdevs/whatshooked/pkg/events"
	"git.warky.dev/wdevs/whatshooked/pkg/handlers"
	"git.warky.dev/wdevs/whatshooked/pkg/hooks"
	"git.warky.dev/wdevs/whatshooked/pkg/logging"
	"git.warky.dev/wdevs/whatshooked/pkg/models"
	"git.warky.dev/wdevs/whatshooked/pkg/storage"
	"git.warky.dev/wdevs/whatshooked/pkg/utils"
	"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
	resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
	"github.com/google/uuid"
	"github.com/uptrace/bun"
	"go.mau.fi/whatsmeow/types"
)

// WhatsHooked is the main library instance
type WhatsHooked struct {
	config       *config.Config
	configPath   string
	eventBus     *events.EventBus
	whatsappMgr  *whatsapp.Manager
	hookMgr      *hooks.Manager
	eventLogger  *eventlogger.Logger
	messageCache *cache.MessageCache
	handlers     *handlers.Handlers
	apiServer    *api.Server // ResolveSpec unified server
	dbReady      bool        // Flag to indicate if database is ready
}

// NewFromFile creates a WhatsHooked instance from a config file
func NewFromFile(configPath string) (*WhatsHooked, error) {
	cfg, err := config.Load(configPath)
	if err != nil {
		return nil, err
	}

	// Initialize logging from config
	logging.Init(cfg.LogLevel)

	return newWithConfig(cfg, configPath)
}

// New creates a WhatsHooked instance with programmatic config
func New(opts ...Option) (*WhatsHooked, error) {
	cfg := &config.Config{
		Server: config.ServerConfig{
			Host: "0.0.0.0",
			Port: 8825,
		},
		Media: config.MediaConfig{
			DataPath: "./data/media",
			Mode:     "link",
		},
		LogLevel: "info",
	}

	// Apply options
	for _, opt := range opts {
		opt(cfg)
	}

	// Initialize logging from config
	logging.Init(cfg.LogLevel)

	return newWithConfig(cfg, "")
}

// newWithConfig is the internal constructor
func newWithConfig(cfg *config.Config, configPath string) (*WhatsHooked, error) {
	wh := &WhatsHooked{
		config:     cfg,
		configPath: configPath,
		eventBus:   events.NewEventBus(),
	}

	// Initialize WhatsApp manager
	wh.whatsappMgr = whatsapp.NewManager(
		wh.eventBus,
		cfg.Media,
		cfg,
		configPath,
		func(updatedCfg *config.Config) error {
			if configPath != "" {
				if err := config.Save(configPath, updatedCfg); err != nil {
					return err
				}
			}
			// Sync account configs to database
			if db := storage.GetDB(); db != nil {
				accountRepo := storage.NewWhatsAppAccountRepository(db)
				for _, waCfg := range updatedCfg.WhatsApp {
					cfgJSON := ""
					if waCfg.BusinessAPI != nil {
						if b, err := json.Marshal(waCfg.BusinessAPI); err == nil {
							cfgJSON = string(b)
						}
					}
					if err := accountRepo.UpdateConfig(context.Background(), waCfg.ID, waCfg.PhoneNumber, cfgJSON, !waCfg.Disabled); err != nil {
						logging.Warn("Failed to sync account config to database", "account_id", waCfg.ID, "error", err)
					}
				}
			}
			return nil
		},
	)

	// Initialize message cache
	cacheConfig := cache.Config{
		Enabled:   cfg.MessageCache.Enabled,
		Storage:   cfg.MessageCache.Storage,
		DataPath:  cfg.MessageCache.DataPath,
		DBType:    cfg.Database.Type,
		MaxAge:    time.Duration(cfg.MessageCache.MaxAgeDays) * 24 * time.Hour,
		MaxEvents: cfg.MessageCache.MaxEvents,
	}

	messageCache, err := cache.NewMessageCache(cacheConfig)
	if err != nil {
		logging.Error("Failed to initialize message cache", "error", err)
		// Continue without cache rather than failing
		messageCache = &cache.MessageCache{}
	}
	wh.messageCache = messageCache

	// Initialize hook manager
	wh.hookMgr = hooks.NewManager(wh.eventBus, wh.messageCache)
	// Don't load hooks here - will be loaded from database after it's initialized
	wh.hookMgr.Start()

	// Initialize event logger if enabled
	if cfg.EventLogger.Enabled && len(cfg.EventLogger.Targets) > 0 {
		logger, err := eventlogger.NewLogger(cfg.EventLogger, cfg.Database, wh.whatsappMgr, cfg.Server.DefaultCountryCode)
		if err == nil {
			wh.eventLogger = logger
			wh.eventBus.SubscribeAll(func(event events.Event) {
				wh.eventLogger.Log(event)
			})
			logging.Info("Event logger initialized", "targets", cfg.EventLogger.Targets)
		} else {
			logging.Error("Failed to initialize event logger", "error", err)
		}
	}

	// Create handlers
	wh.handlers = handlers.New(wh.whatsappMgr, wh.hookMgr, cfg, configPath)

	return wh, nil
}

// ConnectAll connects to all configured WhatsApp accounts
func (wh *WhatsHooked) ConnectAll(ctx context.Context) error {
	// If database is ready, load accounts from database
	if wh.dbReady {
		return wh.connectFromDatabase(ctx)
	}

	// Otherwise, fall back to config file (legacy)
	return wh.connectFromConfig(ctx)
}

// connectFromDatabase loads and connects WhatsApp accounts from the database
func (wh *WhatsHooked) connectFromDatabase(ctx context.Context) error {
	db := storage.GetDB()
	if db == nil {
		logging.Warn("Database not available, skipping account connections")
		return nil
	}

	// Load active WhatsApp accounts from database
	accountRepo := storage.NewWhatsAppAccountRepository(db)
	accounts, err := accountRepo.List(ctx, map[string]interface{}{"active": true})
	if err != nil {
		logging.Error("Failed to load WhatsApp accounts from database", "error", err)
		return err
	}

	logging.Info("Loading WhatsApp accounts from database", "count", len(accounts))

	for _, account := range accounts {
		// Skip if account_id is not set
		accountID := account.ID.String()
		if accountID == "" {
			accountID = account.ID.String() // Fall back to UUID if account_id not set
		}

		// Convert database model to config format
		waCfg := config.WhatsAppConfig{
			ID:          accountID,
			PhoneNumber: account.PhoneNumber.String(),
			Type:        account.AccountType.String(),
			SessionPath: account.SessionPath.String(),
			Disabled:    !account.Active,
		}

		// For business-api accounts, parse the Config JSON column to populate BusinessAPI
		if waCfg.Type == "business-api" && account.Config.String() != "" {
			var businessAPICfg config.BusinessAPIConfig
			if err := json.Unmarshal([]byte(account.Config.String()), &businessAPICfg); err == nil {
				waCfg.BusinessAPI = &businessAPICfg
			} else {
				logging.Warn("Failed to parse business API config from database", "account_id", accountID, "error", err)
			}
		}

		if err := wh.whatsappMgr.Connect(ctx, waCfg); err != nil {
			logging.Error("Failed to connect to WhatsApp", "account_id", waCfg.ID, "error", err)
			// Continue connecting to other accounts even if one fails
		} else {
			if err := accountRepo.UpdateStatus(ctx, waCfg.ID, "connecting"); err != nil {
				logging.Warn("Failed to set account status to connecting", "account_id", waCfg.ID, "error", err)
			}
		}
	}
	return nil
}

// connectFromConfig loads and connects WhatsApp accounts from config file (legacy)
func (wh *WhatsHooked) connectFromConfig(ctx context.Context) error {
	var accountRepo *storage.WhatsAppAccountRepository
	if db := storage.GetDB(); db != nil {
		accountRepo = storage.NewWhatsAppAccountRepository(db)
	}

	for _, waCfg := range wh.config.WhatsApp {
		// Skip disabled accounts
		if waCfg.Disabled {
			logging.Info("Skipping disabled account", "account_id", waCfg.ID)
			if accountRepo != nil {
				if err := accountRepo.UpdateStatus(ctx, waCfg.ID, "disconnected"); err != nil {
					logging.Warn("Failed to set disabled account status", "account_id", waCfg.ID, "error", err)
				}
			}
			continue
		}

		if err := wh.whatsappMgr.Connect(ctx, waCfg); err != nil {
			logging.Error("Failed to connect to WhatsApp", "account_id", waCfg.ID, "error", err)
			// Continue connecting to other accounts even if one fails
		} else if accountRepo != nil {
			if err := accountRepo.UpdateStatus(ctx, waCfg.ID, "connecting"); err != nil {
				logging.Warn("Failed to set account status to connecting", "account_id", waCfg.ID, "error", err)
			}
		}
	}
	return nil
}

// loadHooksFromDatabase loads webhooks from the database
func (wh *WhatsHooked) loadHooksFromDatabase(ctx context.Context) error {
	db := storage.GetDB()
	if db == nil {
		logging.Warn("Database not available, skipping hook loading")
		return nil
	}

	// Load active hooks from database
	hookRepo := storage.NewHookRepository(db)
	dbHooks, err := hookRepo.List(ctx, map[string]interface{}{"active": true})
	if err != nil {
		logging.Error("Failed to load hooks from database", "error", err)
		return err
	}

	logging.Info("Loading hooks from database", "count", len(dbHooks))

	// Convert database models to config format
	configHooks := make([]config.Hook, 0, len(dbHooks))
	for _, dbHook := range dbHooks {
		hook := config.Hook{
			ID:            dbHook.ID.String(),
			Name:          dbHook.Name.String(),
			URL:           dbHook.URL.String(),
			Method:        dbHook.Method.String(),
			Description:   dbHook.Description.String(),
			Active:        dbHook.Active,
			AllowInsecure: dbHook.AllowInsecure,
		}

		// Parse headers JSON if present
		if headersStr := dbHook.Headers.String(); headersStr != "" {
			hook.Headers = make(map[string]string)
			if err := json.Unmarshal([]byte(headersStr), &hook.Headers); err != nil {
				logging.Warn("Failed to parse hook headers", "hook_id", hook.ID, "error", err)
			}
		}

		// Parse events JSON if present
		if eventsStr := dbHook.Events.String(); eventsStr != "" {
			if err := json.Unmarshal([]byte(eventsStr), &hook.Events); err != nil {
				logging.Warn("Failed to parse hook events", "hook_id", hook.ID, "error", err)
			}
		}

		configHooks = append(configHooks, hook)
	}

	// Load hooks into the hook manager
	wh.hookMgr.LoadHooks(configHooks)
	return nil
}

// syncConfigToDatabase upserts hooks and WhatsApp accounts from config.json into the
// database so that config-file entries are never silently ignored at startup. Existing
// rows (matched by ID) are updated to reflect any config changes; new rows are inserted.
// The admin user is used as the owner for all config-seeded rows.
func (wh *WhatsHooked) syncConfigToDatabase(ctx context.Context) error {
	db := storage.GetDB()
	if db == nil {
		return nil
	}

	// Resolve the admin user ID to assign as owner of config-seeded rows.
	userRepo := storage.NewUserRepository(db)
	adminUser, err := userRepo.GetByUsername(ctx, "admin")
	if err != nil || adminUser == nil {
		logging.Warn("Admin user not found, skipping config sync", "error", err)
		return nil
	}
	adminID := adminUser.ID.String()

	now := time.Now()

	// --- Sync hooks ---
	if len(wh.config.Hooks) > 0 {
		for _, h := range wh.config.Hooks {
			if h.ID == "" {
				logging.Warn("Skipping config hook with no ID", "name", h.Name)
				continue
			}

			method := h.Method
			if method == "" {
				method = "POST"
			}

			// Serialize headers and events as JSON blobs.
			headersJSON := ""
			if len(h.Headers) > 0 {
				if b, err := json.Marshal(h.Headers); err == nil {
					headersJSON = string(b)
				}
			}
			eventsJSON := ""
			if len(h.Events) > 0 {
				if b, err := json.Marshal(h.Events); err == nil {
					eventsJSON = string(b)
				}
			}

			row := models.ModelPublicHook{
				ID:            resolvespec_common.NewSqlString(h.ID),
				Name:          resolvespec_common.NewSqlString(h.Name),
				URL:           resolvespec_common.NewSqlString(h.URL),
				Method:        resolvespec_common.NewSqlString(method),
				Description:   resolvespec_common.NewSqlString(h.Description),
				Headers:       resolvespec_common.NewSqlString(headersJSON),
				Events:        resolvespec_common.NewSqlString(eventsJSON),
				Active:        h.Active,
				AllowInsecure: h.AllowInsecure,
				UserID:        resolvespec_common.NewSqlString(adminID),
				CreatedAt:     resolvespec_common.NewSqlTimeStamp(now),
				UpdatedAt:     resolvespec_common.NewSqlTimeStamp(now),
			}

			_, err := db.NewInsert().
				Model(&row).
				On("CONFLICT (id) DO UPDATE").
				Set("name = EXCLUDED.name").
				Set("url = EXCLUDED.url").
				Set("method = EXCLUDED.method").
				Set("description = EXCLUDED.description").
				Set("headers = EXCLUDED.headers").
				Set("events = EXCLUDED.events").
				Set("active = EXCLUDED.active").
				Set("allow_insecure = EXCLUDED.allow_insecure").
				Set("updated_at = EXCLUDED.updated_at").
				Exec(ctx)
			if err != nil {
				logging.Error("Failed to sync hook from config", "hook_id", h.ID, "error", err)
			} else {
				logging.Info("Synced hook from config to database", "hook_id", h.ID, "name", h.Name)
			}
		}
	}

	// --- Sync WhatsApp accounts ---
	if len(wh.config.WhatsApp) > 0 {
		for _, wa := range wh.config.WhatsApp {
			if wa.ID == "" {
				logging.Warn("Skipping config WhatsApp account with no ID", "phone", wa.PhoneNumber)
				continue
			}

			accountType := wa.Type
			if accountType == "" {
				accountType = "whatsmeow"
			}

			cfgJSON := ""
			if wa.BusinessAPI != nil {
				if b, err := json.Marshal(wa.BusinessAPI); err == nil {
					cfgJSON = string(b)
				}
			}

			row := models.ModelPublicWhatsappAccount{
				ID:          resolvespec_common.NewSqlString(wa.ID),
				AccountType: resolvespec_common.NewSqlString(accountType),
				PhoneNumber: resolvespec_common.NewSqlString(wa.PhoneNumber),
				SessionPath: resolvespec_common.NewSqlString(wa.SessionPath),
				Config:      resolvespec_common.NewSqlString(cfgJSON),
				Active:      !wa.Disabled,
				Status:      resolvespec_common.NewSqlString("disconnected"),
				UserID:      resolvespec_common.NewSqlString(adminID),
				CreatedAt:   resolvespec_common.NewSqlTimeStamp(now),
				UpdatedAt:   resolvespec_common.NewSqlTimeStamp(now),
			}

			result, err := db.NewInsert().
				Model(&row).
				// Config should only prime missing accounts, never mutate existing DB rows.
				On("CONFLICT (id) DO NOTHING").
				Exec(ctx)
			if err != nil {
				logging.Error("Failed to sync WhatsApp account from config", "account_id", wa.ID, "error", err)
			} else {
				if result != nil {
					if rows, rowsErr := result.RowsAffected(); rowsErr == nil && rows == 0 {
						logging.Debug("Skipped existing WhatsApp account during config prime", "account_id", wa.ID)
						continue
					}
				}
				logging.Info("Primed WhatsApp account from config into database", "account_id", wa.ID, "phone", wa.PhoneNumber)
			}
		}
	}

	return nil
}

// Handlers returns the HTTP handlers instance
func (wh *WhatsHooked) Handlers() *handlers.Handlers {
	return wh.handlers
}

// Manager returns the WhatsApp manager
func (wh *WhatsHooked) Manager() *whatsapp.Manager {
	return wh.whatsappMgr
}

// EventBus returns the event bus
func (wh *WhatsHooked) EventBus() *events.EventBus {
	return wh.eventBus
}

// HookManager returns the hook manager
func (wh *WhatsHooked) HookManager() *hooks.Manager {
	return wh.hookMgr
}

// Config returns the configuration
func (wh *WhatsHooked) Config() *config.Config {
	return wh.config
}

// Close shuts down all components gracefully
func (wh *WhatsHooked) Close() error {
	wh.whatsappMgr.DisconnectAll()
	if wh.eventLogger != nil {
		return wh.eventLogger.Close()
	}
	return nil
}

// StartServer starts the ResolveSpec HTTP server
func (wh *WhatsHooked) StartServer(ctx context.Context) error {
	return wh.StartAPIServer(ctx)
}

// StopServer stops the ResolveSpec HTTP server
func (wh *WhatsHooked) StopServer(ctx context.Context) error {
	return wh.StopAPIServer(ctx)
}

// StartAPIServer starts the unified ResolveSpec server
func (wh *WhatsHooked) StartAPIServer(ctx context.Context) error {
	// Subscribe to hook success events for two-way communication
	wh.eventBus.Subscribe(events.EventHookSuccess, wh.handleHookResponse)

	// Initialize database
	logging.Info("Initializing database")
	if err := storage.Initialize(&wh.config.Database); err != nil {
		return err
	}

	db := storage.GetDB()

	// Create tables
	logging.Info("Creating database tables")
	if err := storage.CreateTables(ctx); err != nil {
		return err
	}

	// Bind message cache to database storage once DB is initialized.
	if wh.messageCache != nil {
		if err := wh.messageCache.ConfigureDatabase(db); err != nil {
			logging.Error("Failed to configure message cache database storage", "error", err)
		}
	}

	// Seed initial data (creates admin user if not exists)
	logging.Info("Seeding initial data")
	if err := storage.SeedData(ctx); err != nil {
		return err
	}

	// Mark database as ready for account/hook loading
	wh.dbReady = true

	// Keep whatsapp_account.status synchronized with runtime events.
	wh.subscribeAccountStatusEvents(db)

	// Persist hook events to the event_log table
	eventLogRepo := storage.NewEventLogRepository(db)
	wh.eventBus.Subscribe(events.EventHookFailed, func(event events.Event) {
		wh.persistEventLog(eventLogRepo, event, false)
	})
	wh.eventBus.Subscribe(events.EventHookSuccess, func(event events.Event) {
		wh.persistEventLog(eventLogRepo, event, true)
	})

	// Sync config.json hooks and accounts into the database so they are never silently ignored
	logging.Info("Syncing config to database")
	if err := wh.syncConfigToDatabase(ctx); err != nil {
		logging.Error("Failed to sync config to database", "error", err)
		// Non-fatal: continue with whatever is already in the database
	}

	// Load hooks from database
	if err := wh.loadHooksFromDatabase(ctx); err != nil {
		logging.Error("Failed to load hooks from database", "error", err)
		// Continue anyway, hooks can be added later
	}

	// Create unified server
	logging.Info("Creating ResolveSpec server", "host", wh.config.Server.Host, "port", wh.config.Server.Port)
	apiServer, err := api.NewServer(wh.config, db, wh)
	if err != nil {
		return err
	}

	wh.apiServer = apiServer

	// Start the server in a goroutine (non-blocking)
	go func() {
		if err := wh.apiServer.Start(); err != nil {
			logging.Error("ResolveSpec server error", "error", err)
		}
	}()

	logging.Info("ResolveSpec server started successfully")
	return nil
}

func (wh *WhatsHooked) subscribeAccountStatusEvents(db *bun.DB) {
	accountRepo := storage.NewWhatsAppAccountRepository(db)
	updateStatus := func(event events.Event, status string) {
		accountID, _ := event.Data["account_id"].(string)
		if accountID == "" {
			return
		}
		if err := accountRepo.UpdateStatus(context.Background(), accountID, status); err != nil {
			logging.Warn("Failed to sync account status from event", "account_id", accountID, "status", status, "error", err)
		}
	}

	wh.eventBus.Subscribe(events.EventWhatsAppConnected, func(event events.Event) {
		updateStatus(event, "connected")
	})
	wh.eventBus.Subscribe(events.EventWhatsAppDisconnected, func(event events.Event) {
		updateStatus(event, "disconnected")
	})
	wh.eventBus.Subscribe(events.EventWhatsAppQRCode, func(event events.Event) {
		updateStatus(event, "pairing")
	})
	wh.eventBus.Subscribe(events.EventWhatsAppQRTimeout, func(event events.Event) {
		updateStatus(event, "connecting")
	})
	wh.eventBus.Subscribe(events.EventWhatsAppQRError, func(event events.Event) {
		updateStatus(event, "disconnected")
	})
	wh.eventBus.Subscribe(events.EventWhatsAppPairFailed, func(event events.Event) {
		updateStatus(event, "disconnected")
	})
	wh.eventBus.Subscribe(events.EventWhatsAppPairSuccess, func(event events.Event) {
		updateStatus(event, "connected")
	})
}

// StopAPIServer stops the ResolveSpec server
func (wh *WhatsHooked) StopAPIServer(ctx context.Context) error {
	if wh.apiServer != nil {
		logging.Info("Stopping ResolveSpec server")
		// The server manager handles graceful shutdown internally
		// We just need to close the database
		return storage.Close()
	}
	return nil
}

// persistEventLog writes a hook event to the public.event_log table.
func (wh *WhatsHooked) persistEventLog(repo *storage.EventLogRepository, event events.Event, success bool) {
	hookID, _ := event.Data["hook_id"].(string)

	errStr := ""
	if !success {
		errStr, _ = event.Data["error"].(string)
	}

	data, _ := json.Marshal(event.Data)

	entry := models.ModelPublicEventLog{
		ID:         resolvespec_common.NewSqlString(uuid.New().String()),
		EventType:  resolvespec_common.NewSqlString(string(event.Type)),
		EntityType: resolvespec_common.NewSqlString("hook"),
		EntityID:   resolvespec_common.NewSqlString(hookID),
		Data:       resolvespec_common.NewSqlString(string(data)),
		Error:      resolvespec_common.NewSqlString(errStr),
		Success:    success,
		CreatedAt:  resolvespec_common.NewSqlTimeStamp(event.Timestamp),
	}

	if err := repo.Create(context.Background(), &entry); err != nil {
		logging.Error("Failed to persist event log entry", "event_type", event.Type, "hook_id", hookID, "error", err)
	}
}

// handleHookResponse processes hook success events for two-way communication
func (wh *WhatsHooked) handleHookResponse(event events.Event) {
	// Use event context for sending message
	ctx := event.Context
	if ctx == nil {
		ctx = context.Background()
	}

	// Extract response from event data
	responseData, ok := event.Data["response"]
	if !ok || responseData == nil {
		return
	}

	// Try to cast to HookResponse
	resp, ok := responseData.(hooks.HookResponse)
	if !ok {
		return
	}

	if !resp.SendMessage {
		return
	}

	// Determine which account to use - default to first available if not specified
	targetAccountID := resp.AccountID
	if targetAccountID == "" && len(wh.config.WhatsApp) > 0 {
		targetAccountID = wh.config.WhatsApp[0].ID
	}

	// Format phone number to JID format
	formattedJID := utils.FormatPhoneToJID(resp.To, wh.config.Server.DefaultCountryCode)

	// Parse JID
	jid, err := types.ParseJID(formattedJID)
	if err != nil {
		logging.Error("Invalid JID in hook response", "jid", formattedJID, "error", err)
		return
	}

	// Send message with context
	if err := wh.whatsappMgr.SendTextMessage(ctx, targetAccountID, jid, resp.Text); err != nil {
		logging.Error("Failed to send message from hook response", "error", err)
	} else {
		logging.Info("Message sent from hook response", "account_id", targetAccountID, "to", resp.To)
	}
}
