package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"github.com/prometheus/client_golang/prometheus/promhttp"

	ffmpegconv "git.vakhrushev.me/av/transcriber/internal/adapter/converter/ffmpeg"
	ffmpegmv "git.vakhrushev.me/av/transcriber/internal/adapter/metaviewer/ffmpeg"
	"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer/yandex"
	sqliterepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/sqlite"
	"git.vakhrushev.me/av/transcriber/internal/config"
	httpcontroller "git.vakhrushev.me/av/transcriber/internal/controller/http"
	"git.vakhrushev.me/av/transcriber/internal/controller/worker"
	"git.vakhrushev.me/av/transcriber/internal/metrics"
	"git.vakhrushev.me/av/transcriber/internal/service"
	"git.vakhrushev.me/av/transcriber/web"
)

// main держит одну обязанность: отказ подъёма пишется **одной** строкой и
// кончается ненулевым кодом выхода.
//
// Работа вынесена в run, чтобы уборка шла отложенными вызовами: `os.Exit`
// посреди подъёма оставил бы за собой открытые пулы базы и незакрытого клиента
// распознавания.
func main() {
	// Создаем структурированный логгер
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))
	slog.SetDefault(logger)

	if err := run(logger); err != nil {
		logger.Error("Transcriber service failed to start", "error", err)
		os.Exit(1)
	}
}

func run(logger *slog.Logger) error {
	// Parse command line flags
	configPath := flag.String("c", "config.toml", "Path to config file")
	flag.StringVar(configPath, "config", "config.toml", "Path to config file (alias for -c)")
	flag.Parse()

	cfg, err := config.LoadConfig(*configPath)
	if err != nil {
		return fmt.Errorf("unable to load configuration from %s: %w", *configPath, err)
	}
	logger.Info("Configuration loaded successfully", "config_path", *configPath)

	// Пустой перечень доверенных адресов роняет старт: он значит «не верить
	// никому», то есть сервис, поднявшийся никого не узнающим, — и узнать об
	// этом было бы неоткуда.
	if err := cfg.Auth.Validate(); err != nil {
		return err
	}

	// Перечень разобран один раз, при старте: разбирать строки на каждом запросе
	// значило бы платить за настройку, которая не меняется.
	trustedNetworks, err := cfg.Auth.TrustedNetworks()
	if err != nil {
		return err
	}
	// Перечень называется строкой журнала: сервис, никого не узнающий из-за
	// неверного перечня, иначе неотличим от сервиса, до которого заголовок не
	// доходит вовсе, — а это разные поломки в разных местах.
	logger.Info("Trusted proxies configured", "trusted_proxies", cfg.Auth.TrustedProxies)

	// Настройки отладочного входа: заполненная имитация без предохранителя, имя
	// заголовка, которого сервис не читает, и имитация без годного логина роняют
	// старт. Имена заголовков приходят проверке доводом — дом у них один,
	// константы транспорта, — а пакет настроек транспорта не знает.
	if err := cfg.ValidateTestHeaders(
		httpcontroller.IdentityHeaderNames(), httpcontroller.LoginHeader,
	); err != nil {
		return err
	}

	// Представление предиката «подставляем ли» одно — непустота перечня, — и
	// судят его одинаково строка журнала ниже, слой подстановки и проверка выше.
	// Второе выражение того же предиката разошлось бы с первым молча.
	substitution := cfg.HeaderSubstitution()
	if len(substitution) > 0 {
		// Уровень предупреждающий: сервис называет пришедшего сам, никого не
		// спросив, — ровно то «может стать проблемой», ради которого заведён
		// этот уровень. Идут имена заголовков; значений нет — логин это ключ к
		// чужому архиву.
		logger.Warn("Identity headers are substituted from configuration",
			"headers", httpcontroller.SubstitutedHeaderNames(substitution),
			"capability", "access")
	}

	// Числа конвейера проверяются здесь же: ноль воркеров — объявленный режим, а
	// отрицательное число и нулевой предел простоя — опечатка, и подниматься с
	// ней значит остановить всякую запись первым же захватом.
	if err := cfg.Pipeline.Validate(); err != nil {
		return err
	}
	if err := cfg.Storage.Validate(); err != nil {
		return err
	}

	db, err := sqliterepo.Open(cfg.Storage.DataDir, sqliterepo.Settings{
		BusyTimeoutMs:   cfg.Storage.BusyTimeoutMs,
		ReadConnections: cfg.Storage.ReadConnections,
	})
	if err != nil {
		return err
	}
	defer func() {
		if err := db.Close(); err != nil {
			logger.Error("Failed to close the database", "error", err)
		}
	}()

	// Создаем контекст для graceful shutdown
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Схема накатывается **до** подъёма входов и до старта воркеров, а её отказ
	// роняет старт: сервис, поднявшийся на неприведённой схеме, отвечает отказом
	// на каждый запрос и на каждый прогон воркера — вместо одной строки о
	// причине их становятся сотни.
	if err := sqliterepo.Migrate(ctx, db, cfg.Storage.DataDir, logger); err != nil {
		return err
	}

	store := sqliterepo.NewStore(cfg.Storage.DataDir)
	recordRepo := sqliterepo.NewAudioRecordRepository(db)
	fileRepo := sqliterepo.NewFileRepository(db, store)
	repos := service.Repositories{
		Records:      recordRepo,
		Files:        fileRepo,
		Texts:        sqliterepo.NewTextRepository(db),
		Structures:   sqliterepo.NewStructureRepository(db),
		Recognitions: sqliterepo.NewRecognitionRepository(db, store),
		Events:       sqliterepo.NewRecordEventRepository(db),
	}
	users := sqliterepo.NewUserRepository(db)

	// Создаем адаптеры
	metaviewer := ffmpegmv.NewFfmpegMetaViewer()
	converter := ffmpegconv.NewFfmpegConverter()

	recognizer, err := yandex.NewYandexAudioRecognizerService(yandex.YandexAudioRecognizerConfig{
		Region:     cfg.Yandex.ObjStorageRegion,
		AccessKey:  cfg.Yandex.ObjStorageAccessKey,
		SecretKey:  cfg.Yandex.ObjStorageSecretKey,
		BucketName: cfg.Yandex.ObjStorageBucketName,
		Endpoint:   cfg.Yandex.ObjStorageEndpoint,
		ApiKey:     cfg.Yandex.SpeechKitAPIKey,
		FolderID:   cfg.Yandex.FolderID,
	})
	if err != nil {
		return fmt.Errorf("failed to create audio recognizer: %w", err)
	}
	// Отдавать отказ закрытия некому — процесс заканчивается, — поэтому он идёт
	// в журнал владельца. Что он означает: gRPC-клиент отдаёт здесь отказ лишь
	// при повторном закрытии, то есть запись говорит о нашей ошибке, а не о
	// недоступности Yandex.
	defer func() {
		if err := recognizer.Close(); err != nil {
			logger.Error("failed to close audio recognizer", "error", err)
		}
	}()

	transcribeService := service.NewTranscribeService(
		repos,
		metaviewer,
		converter,
		recognizer,
		cfg.Pipeline.StuckLimits(),
		logger,
	)

	// Создаем WaitGroup для ожидания завершения всех воркеров
	var wg sync.WaitGroup

	// Пул одинаковых воркеров: специализации у них нет, шаг выбирается по рубежу
	// самой записи. Число приходит настройкой, ноль — законное значение.
	pool := worker.NewPool(cfg.Pipeline.Workers, transcribeService.RunStep, logger)
	wg.Add(1)
	go func() {
		defer wg.Done()
		pool.Start(ctx)
	}()

	// Вход у сервиса один — приём по HTTP, — и метка ставится только ему.
	metrics.IntakeUpGauge.WithLabelValues("http").Set(1)

	appHandler := httpcontroller.NewAppHandler(
		recordRepo, repos.Texts, repos.Structures, fileRepo, transcribeService, logger,
	)

	// Адресное пространство сервиса объявлено одним перечнем, и он порождает
	// регистрацию, а не описывает её: корень, заведённый мимо перечня, не
	// получит обработчика вовсе. Отсюда же уровень журнала для адресов
	// наблюдения, правило неизвестного пути у раздачи приложения и область
	// действия узнавания.
	mounts := httpcontroller.ServiceMounts(
		httpcontroller.AppChain(appHandler.Routes(), users, trustedNetworks, substitution, logger),
		promhttp.Handler(),
	)

	dist, appBuilt := web.Dist()
	webappHandler := httpcontroller.NewWebappHandler(dist, appBuilt, logger)

	srv := &http.Server{
		Addr:    fmt.Sprintf(":%d", cfg.Server.Port),
		Handler: httpcontroller.BuildHandler(mounts, webappHandler, logger),
		// Шесть часов записи по медленному каналу переживают любой фиксированный
		// таймаут чтения. Стойкость к целенаправленной нагрузке объявлена вне
		// модели угроз проекта.
		ReadTimeout: 0,
	}

	serveErr := make(chan error, 1)
	wg.Add(1)
	go func() {
		defer wg.Done()
		logger.Info("Starting HTTP server", "port", cfg.Server.Port)
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			serveErr <- err
		}
	}()

	// Настраиваем обработку сигналов для graceful shutdown
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	logger.Info("Transcriber service started", "pipeline_workers", pool.Size())
	logger.Info("Press Ctrl+C to stop...")

	// Ждем сигнал завершения либо отказ сервера
	var startupErr error
	select {
	case <-sigChan:
		logger.Info("Received shutdown signal, initiating graceful shutdown...")
	case err := <-serveErr:
		logger.Error("HTTP server stopped unexpectedly, shutting down", "error", err)
		startupErr = err
	}

	// Останавливаем HTTP сервер
	shutdownCtx, shutdownCancel := context.WithTimeout(
		context.Background(), time.Duration(cfg.Server.ShutdownTimeout)*time.Second)
	defer shutdownCancel()

	logger.Info("Shutting down HTTP server...")
	if err := srv.Shutdown(shutdownCtx); err != nil {
		logger.Error("HTTP server forced to shutdown", "error", err)
	} else {
		logger.Info("HTTP server stopped gracefully")
	}

	// Отменяем контекст для остановки воркеров
	cancel()

	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		logger.Info("All workers stopped gracefully")
	case <-time.After(time.Duration(cfg.Server.ForceShutdownTimeout) * time.Second):
		logger.Warn("Timeout reached, forcing shutdown")
	}

	logger.Info("Transcriber service stopped")

	return startupErr
}
