package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"os/signal"
	"strings"
	"syscall"

	"time"

	"github.com/jackc/pgx/v5/pgxpool"

	workerbacktest "git.toki-labs.com/toki/alt/services/worker/internal/backtest"
	"git.toki-labs.com/toki/alt/services/worker/internal/config"
	"git.toki-labs.com/toki/alt/services/worker/internal/jobs"
	"git.toki-labs.com/toki/alt/services/worker/internal/livetrading"
	"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/aggregation"
	"git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer"
	"git.toki-labs.com/toki/alt/services/worker/internal/papertrading"
	"git.toki-labs.com/toki/alt/services/worker/internal/providers/kis"
	"git.toki-labs.com/toki/alt/services/worker/internal/scheduler"
	"git.toki-labs.com/toki/alt/services/worker/internal/socket"
	"git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	cfg := config.Load()
	runner := jobs.NewRunner()
	jobs.RegisterBuiltins(runner)

	// Store-backed socket surfaces are optional at process startup. If the pool
	// cannot be constructed the worker still serves hello, while market and
	// backtest handlers report unavailable instead of panicking.
	var deps socket.Deps
	var refreshStatusStore *scheduler.RefreshStatusStore
	pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
	if err != nil {
		slog.Error("failed to create database pool; store-backed socket surfaces disabled", "error", err)
	} else {
		defer pool.Close()
		store := postgres.NewStore(pool)
		deps = storeBackedDeps(runner, store)

		// Wire refresh status store so the scheduler tick and socket handler
		// share the same in-memory store and the operator-facing query
		// surface reports actual runtime state.
		refreshStatusStore = scheduler.NewRefreshStatusStore()
		deps.RefreshStatus = refreshStatusStore

		if err := startScheduler(ctx, cfg, runner, refreshStatusStore); err != nil {
			slog.Error("failed to start scheduler", "error", err)
			os.Exit(1)
		}
	}

	server := socket.NewServer(cfg, deps)
	if err := server.Start(ctx); err != nil {
		slog.Error("failed to start worker socket server", "error", err)
		os.Exit(1)
	}

	slog.Info("worker ready",
		"redis_key_prefix", cfg.RedisKeyPrefix,
		"worker_queue", cfg.WorkerQueue,
		"handlers", runner.Len(),
		"backtest_read_surface", deps.Analysis != nil,
		"backtest_start_surface", deps.Starter != nil,
		"market_read_surface", deps.Instruments != nil && deps.Bars != nil,
		"market_import_surface", deps.DailyBarImporter != nil,
		"live_audit_surface", deps.Live != nil,
		"addr", fmt.Sprintf("%s:%d%s", cfg.Host, cfg.Port, cfg.SocketPath),
	)

	<-ctx.Done()
	if err := server.Stop(); err != nil {
		slog.Error("failed to stop worker socket server", "error", err)
		os.Exit(1)
	}
}

// storeBackedDeps wires every store-backed socket dependency around a single
// store, including the backtest execution rail. It is factored out of main so a
// unit test can assert the backtest start/read surfaces are wired without a live
// database. Backtest execution reads imported daily bars from storage and runs
// the bundled strategy resolver; the starter records the pending run and hands
// execution to the same runner the import jobs use, keeping a single async job
// rail rather than a second execution engine.
func storeBackedDeps(runner *jobs.Runner, store *postgres.Store) socket.Deps {
	kisProvider := kis.NewLiveProvider(kis.NewClient(kis.ConfigFromEnv()))
	// One importer instance backs both the async job handler and the socket
	// import command so the operator-facing command and scheduled jobs share the
	// same provider/store wiring.
	kisImporter := importer.New(kisProvider, store)
	jobs.RegisterDailyBarImportHandler(runner, kis.Capability(), kisImporter)

	barSource := workerbacktest.NewStorageBarSource(store, store)
	strategyPort := workerbacktest.NewBuiltInStrategyPort()
	engine := workerbacktest.NewEngine(barSource, strategyPort, store)
	jobs.RegisterRunBacktestHandler(runner, store, engine, time.Now)
	starter := jobs.NewBacktestStarter(runner, store)

	// Paper trading reuses the same imported-bar source and bundled strategy
	// resolver as backtest, but runs synchronously into a process-local
	// in-memory state keyed by account id (no durable store for readiness).
	paperEngine := papertrading.NewEngine(barSource, strategyPort)
	paperService := papertrading.NewService(paperEngine, time.Now)

	// Live trading service: nil broker means live order operations are unavailable
	// until a broker adapter is wired. The audit store is always injected so the
	// audit query surface is available as soon as the database pool is up.
	liveSvc := livetrading.NewService(nil)
	liveSvc.SetAuditStore(store)

	return socket.Deps{
		Starter:           starter,
		Analysis:          store,
		Results:           store,
		Instruments:       store,
		Bars:              store,
		DailyBarImporter:  kisImporter,
		MonthlyAggregator: aggregation.NewAggregator(store, store),
		Paper:             paperService,
		Live:              liveSvc,
	}
}

func startScheduler(ctx context.Context, cfg config.Config, runner *jobs.Runner, statusStore *scheduler.RefreshStatusStore) error {
	if !cfg.SchedulerEnabled {
		return nil
	}

	f, err := os.Open(cfg.ScheduleConfigPath)
	if err != nil {
		return fmt.Errorf("failed to open schedule config: %w", err)
	}
	defer f.Close()

	schedCfg, err := scheduler.LoadConfig(f)
	if err != nil {
		return fmt.Errorf("failed to load schedule config: %w", err)
	}

	// Store the config so the refresh status store can compute next_run values
	// for the socket handler surface.
	if statusStore != nil {
		statusStore.SetScheduleConfig(schedCfg)
	}

	capabilities := scheduler.CapabilityMap(kis.Capability())
	validation := scheduler.ValidateConfig(schedCfg, scheduler.ValidationOptions{
		Now:          time.Now(),
		Capabilities: capabilities,
	})
	if validation.HasRejected() {
		var reasons []string
		for _, item := range validation.Items {
			if item.Status == "rejected" {
				reasons = append(reasons, fmt.Sprintf("%s: %s", item.Schedule, item.Reason))
			}
		}
		return fmt.Errorf("invalid schedule config: %s", strings.Join(reasons, "; "))
	}

	slog.Info("starting scheduler loop",
		"scheduler_enabled", true,
		"schedule_count", len(schedCfg.Schedules),
	)

	go func() {
		ticker := time.NewTicker(1 * time.Minute)
		defer ticker.Stop()

		opts := scheduler.TickOptions{
			Capabilities: capabilities,
			Executor:     runner,
		}

		runSchedulerLoop(ctx, schedCfg, opts, ticker.C, statusStore)
	}()

	return nil
}

func runSchedulerLoop(ctx context.Context, schedCfg scheduler.Config, opts scheduler.TickOptions, ticks <-chan time.Time, statusStore *scheduler.RefreshStatusStore) {
	for {
		select {
		case <-ctx.Done():
			slog.Info("stopping scheduler loop")
			return
		case <-ticks:
			opts.Now = time.Now()
			res, err := scheduler.RunTick(ctx, schedCfg, opts)
			if err != nil {
				slog.Error("scheduler tick execution error", "error", err)
				continue
			}
			for _, item := range res.Items {
				if item.Status == "failed" {
					slog.Error("scheduler job failed", "schedule", item.Schedule, "symbol", item.Symbol, "error", item.Error)
				} else if item.Status == "skipped" {
					slog.Info("scheduler job skipped", "schedule", item.Schedule, "symbol", item.Symbol, "reason", item.Error)
				} else {
					slog.Info("scheduler job succeeded", "schedule", item.Schedule, "symbol", item.Symbol, "readiness", item.Readiness, "freshness", item.Freshness)
				}
			}
			// Record tick results in the refresh status store so the socket
			// handler surface reports actual runtime state.
			if statusStore != nil {
				now := opts.Now
				for _, sched := range schedCfg.Schedules {
					statusStore.RecordTick(sched.Name, res.Items, now)
				}
			}
		}
	}
}
