package app

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"sync"
	"time"

	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/config"
	httpserver "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/http"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/platform"
	redisclient "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/redis"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/service/parser"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/service/parser/registry"
	mongostore "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/store/mongo"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/telemetry"
)

const (
	serviceName           = "code-parser-service"
	shutdownJobDrainTimeout = 30 * time.Second
)

type App struct {
	cfg         config.Config
	logger      *slog.Logger
	mongoStore  *mongostore.Store
	redis       *redisclient.Client
	publisher   *redisclient.Publisher
	consumer    *redisclient.Consumer
	svc         *parser.Service
	server      *http.Server
}

func New() (*App, error) {
	platform.Name = serviceName

	cfg, err := config.Load(serviceName, 6002)
	if err != nil {
		return nil, err
	}

	logger := telemetry.NewLogger(telemetry.LoggerConfig{
		ServiceName: serviceName,
		Level:       cfg.LogLevel,
		LogFile:     cfg.LogFile,
	})
	slog.SetDefault(logger)

	if err := cfg.Git.EnsureWorkspace(); err != nil {
		return nil, fmt.Errorf("initialize git workspace: %w", err)
	}

	redis, err := redisclient.New(cfg.Redis)
	if err != nil {
		return nil, fmt.Errorf("initialize redis: %w", err)
	}

	mongoStore, err := mongostore.New(cfg.Mongo)
	if err != nil {
		_ = redis.Close()
		return nil, fmt.Errorf("initialize mongodb: %w", err)
	}

	indexCtx, indexCancel := context.WithTimeout(context.Background(), 10*time.Second)
	if err := mongoStore.EnsureIndexes(indexCtx); err != nil {
		indexCancel()
		_ = mongoStore.Close(context.Background())
		_ = redis.Close()
		return nil, fmt.Errorf("ensure mongodb indexes: %w", err)
	}
	indexCancel()

	publisher := redisclient.NewPublisher(redis, cfg.Redis, logger)
	built := registry.Build(registry.Options{EnableFallback: cfg.Parser.EnableFallback})
	parserOpts := []parser.ServiceOption{
		parser.WithFinalizeStore(mongoStore),
		parser.WithCommitOrderStore(mongoStore),
	}
	if built.Fallback != nil {
		parserOpts = append(parserOpts, parser.WithFallbackAnalyzer(built.Fallback))
	}
	parserService := parser.NewService(parser.ServiceConfig{
		WorkspacePath:             cfg.Git.WorkspacePath,
		Workers:                   cfg.Parser.Workers,
		ParserVersion:             cfg.Parser.Version,
		CodeParseFailureThreshold: cfg.Parser.CodeParseFailureThreshold,
		Logger:                    logger,
	}, built.Analyzers, publisher, mongoStore, parserOpts...)

	consumer := redisclient.NewConsumer(redis, cfg, parserService, logger)

	ready := func() bool {
		ctx := context.Background()
		return redis.Ready(ctx) && mongoStore.Ready(ctx) && cfg.Git.Ready(ctx)
	}

	httpServer := httpserver.New(serviceName, ready, httpserver.DebugAPI{
		Jobs:      parserService,
		Artifacts: mongoStore,
	})

	return &App{
		cfg:        cfg,
		logger:     logger,
		mongoStore: mongoStore,
		redis:      redis,
		publisher:  publisher,
		consumer:   consumer,
		svc:        parserService,
		server: &http.Server{
			Addr:              cfg.Address(),
			Handler:           telemetry.HTTPMiddleware(logger, httpServer.Handler()),
			ReadHeaderTimeout: 5 * time.Second,
		},
	}, nil
}

func (a *App) Run(ctx context.Context) error {
	bg := context.Background()
	redisReady := a.redis.Ready(bg)
	mongoReady := a.mongoStore.Ready(bg)
	workspaceReady := a.cfg.Git.Ready(bg)

	a.logger.Info("starting service",
		"address", a.cfg.Address(),
		"environment", a.cfg.Environment,
		"status", a.svc.Status(),
		"languages", a.svc.SupportedLanguages(),
		"redis_url", a.cfg.Redis.URL,
		"redis_stream_prefix", a.cfg.Redis.StreamPrefix,
		"redis_ready", redisReady,
		"mongo_uri", a.cfg.Mongo.URI,
		"mongo_database", a.cfg.Mongo.Database,
		"mongo_ready", mongoReady,
		"git_workspace_path", a.cfg.Git.WorkspacePath,
		"git_workspace_ready", workspaceReady,
		"parser_workers", a.cfg.Parser.Workers,
		"parser_version", a.cfg.Parser.Version,
	)

	var consumerWG sync.WaitGroup
	consumerWG.Add(1)
	go func() {
		defer consumerWG.Done()
		defer telemetry.RecoverPanic(a.logger, "redis-consumer")
		if err := a.consumer.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
			telemetry.RecordError(a.logger, err, "redis consumer stopped")
		}
	}()

	errCh := make(chan error, 1)
	go func() {
		a.logger.Info("http server listening", "address", a.cfg.Address())
		if err := a.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			errCh <- err
		}
	}()

	select {
	case <-ctx.Done():
		consumerWG.Wait()

		drainCtx, drainCancel := context.WithTimeout(context.Background(), shutdownJobDrainTimeout)
		a.svc.WaitForJobs(drainCtx)
		drainCancel()
		if remaining := a.svc.ActiveJobCount(); remaining > 0 {
			a.logger.Warn("shutdown proceeding with in-flight parse jobs", "active_jobs", remaining)
		}

		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		if err := a.server.Shutdown(shutdownCtx); err != nil {
			return fmt.Errorf("shutdown server: %w", err)
		}
		if err := a.mongoStore.Close(shutdownCtx); err != nil {
			return fmt.Errorf("close mongodb: %w", err)
		}
		if err := a.redis.Close(); err != nil {
			return fmt.Errorf("close redis: %w", err)
		}
		a.logger.Info("service stopped")
		return nil
	case err := <-errCh:
		return err
	}
}
