package main

import (
	"context"
	_ "embed"
	"errors"
	"evening_detective_server/internal/app"
	"evening_detective_server/internal/modules/cleaner"
	"evening_detective_server/internal/modules/email_sender"
	"evening_detective_server/internal/modules/file_storage"
	"evening_detective_server/internal/modules/password_generator"
	"evening_detective_server/internal/modules/processor_jwt"
	"evening_detective_server/internal/modules/scenario_archive"
	"evening_detective_server/internal/modules/storytelling"
	"evening_detective_server/internal/modules/string_tools"
	"evening_detective_server/internal/repos/actions_repo"
	"evening_detective_server/internal/repos/applications_repo"
	"evening_detective_server/internal/repos/games_repo"
	"evening_detective_server/internal/repos/refresh_tokens_repo"
	"evening_detective_server/internal/repos/scenarios_repo"
	"evening_detective_server/internal/repos/teams_repo"
	"evening_detective_server/internal/repos/users_repo"
	"evening_detective_server/internal/services/file_service"
	"evening_detective_server/internal/services/game_service"
	"evening_detective_server/internal/services/mcp_service"
	"evening_detective_server/internal/services/scenarios_service"
	"evening_detective_server/internal/services/ui_service"
	"evening_detective_server/internal/services/users_service"
	proto "evening_detective_server/proto"
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/joho/godotenv"
	"github.com/mark3labs/mcp-go/server"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"

	"github.com/swaggest/swgui/v5emb"
)

//go:embed main.swagger.json
var swaggerJSON []byte

// maxFileSize — максимальный размер файла, передаваемого через gRPC
const maxFileSize = 64 << 20 // 64 МБ

// maxFileUploadBody — лимит HTTP-тела POST /api/files/upload (JSON с base64):
// ceil(maxFileSize·4/3) — точный размер base64, +2 КБ запаса на JSON-обёртку
// {"filename":...,"data":...} с коротким именем файла.
const maxFileUploadBody = (int64(maxFileSize)+2)/3*4 + 2*1024

// grpcMsgLimit — лимит размера gRPC-сообщения (сервер и клиент gateway):
// maxFileSize + 1 MiB на прото-оверхед (tag+varint) поверх ровно 64 MiB
// данных; авторитетная проверка размера — в file_service.
const grpcMsgLimit = maxFileSize + (1 << 20)

func main() {
	_ = godotenv.Load()

	// PostgreSQL
	dbpool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatalf("Unable to create connection pool: %v\n", err)
	}
	defer dbpool.Close()

	// Test request to DB
	var greeting string
	if err = dbpool.QueryRow(context.Background(), "select 'Hello, world!'").Scan(&greeting); err != nil {
		log.Fatalf("QueryRow failed: %v\n", err)
	}

	usersRepo := users_repo.NewUserRepo(dbpool)
	passwordGenerator := password_generator.NewGenerator(12)
	smtpTimeout := 10 * time.Second
	if v := os.Getenv("SMTP_TIMEOUT"); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d > 0 {
			smtpTimeout = d
		}
	}
	emailSender := email_sender.NewSender(
		os.Getenv("SMTP_HOST"),
		os.Getenv("SMTP_PORT"),
		os.Getenv("SMTP_USER"),
		os.Getenv("SMTP_PASSWORD"),
		os.Getenv("SMTP_FROM_NAME"),
		os.Getenv("SMTP_REPLY_TO"),
		smtpTimeout,
	)
	processorJWT := processor_jwt.NewProcessor(os.Getenv("JWT_SECRET"))
	refreshTokensRepo := refresh_tokens_repo.NewRefreshTokensRepo(dbpool)
	usersService := users_service.NewUsersService(usersRepo, passwordGenerator, emailSender, processorJWT, refreshTokensRepo, os.Getenv("APP_BASE_URL"))
	uiService := ui_service.NewUiService()
	fileStorage, err := file_storage.NewRustFSStorage(
		os.Getenv("S3_HOST"),
		os.Getenv("S3_USER"),
		os.Getenv("S3_PASSWORD"),
		os.Getenv("S3_BUCKET"),
	)
	if err != nil {
		log.Fatalf("Unable to create connection rustfs: %v\n", err)
	}
	fileService := file_service.NewFileService(fileStorage, string_tools.NewStringTools(), maxFileSize)
	scenariosRepo := scenarios_repo.NewScenariosRepo(dbpool)
	cleaner := cleaner.NewCleaner()
	scenarioService := scenarios_service.NewScenarioService(
		scenariosRepo,
		cleaner,
		os.Getenv("FILE_PREFIX_DOMAIN"),
		fileStorage,
		scenario_archive.NewScenarioArchive(),
	)
	gameRepo := games_repo.NewGamesRepo(dbpool)
	teamRepo := teams_repo.NewTeamsRepo(dbpool)
	storyteller := storytelling.NewStory(cleaner)
	actionsRepo := actions_repo.NewActionsRepo(dbpool)
	applicationsRepo := applications_repo.NewApplicationsRepo(dbpool)
	gameService := game_service.NewGameService(
		gameRepo,
		teamRepo,
		scenariosRepo,
		scenarioService,
		os.Getenv("FILE_PREFIX_DOMAIN"),
		passwordGenerator,
		storyteller,
		actionsRepo,
		applicationsRepo,
		cleaner,
	)

	// Create a listener on TCP port
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalln("Failed to listen:", err)
	}

	// Create a gRPC server object
	s := grpc.NewServer(
		grpc.MaxRecvMsgSize(grpcMsgLimit),
		grpc.UnaryInterceptor(
			processor_jwt.NewAuthorizationInterceptor(
				map[string]bool{
					"/crabs.evening_detective_server.EveningDetectiveServer/Ping":                true,
					"/crabs.evening_detective_server.EveningDetectiveServer/Echo":                true,
					"/crabs.evening_detective_server.EveningDetectiveServer/Signup":              true,
					"/crabs.evening_detective_server.EveningDetectiveServer/RefreshPassword":     true,
					"/crabs.evening_detective_server.EveningDetectiveServer/Login":               true,
					"/crabs.evening_detective_server.EveningDetectiveServer/Refresh":             true,
					"/crabs.evening_detective_server.EveningDetectiveServer/DownloadFile":        true,
					"/crabs.evening_detective_server.EveningDetectiveServer/GetScenario":         true,
					"/crabs.evening_detective_server.EveningDetectiveServer/GetScenariosCatalog": true,
					"/crabs.evening_detective_server.EveningDetectiveServer/GetTeamStory":        true,
					"/crabs.evening_detective_server.EveningDetectiveServer/AddTeamAction":       true,
					"/crabs.evening_detective_server.EveningDetectiveServer/GetTerms":            true,
					"/crabs.evening_detective_server.EveningDetectiveServer/GetPrivacy":          true,
					"/crabs.evening_detective_server.EveningDetectiveServer/GetConsent":          true,
				},
				processorJWT,
			),
		),
	)
	// Attach the Greeter service to the server
	proto.RegisterEveningDetectiveServerServer(
		s,
		app.NewServer(
			usersService,
			uiService,
			fileService,
			scenarioService,
			gameService,
		),
	)

	// Serve gRPC server
	log.Println("Serving gRPC on 0.0.0.0:8080")
	go func() {
		log.Fatalln(s.Serve(lis))
	}()

	// Create a client connection to the gRPC server we just started
	// This is where the gRPC-Gateway proxies the requests
	conn, err := grpc.NewClient(
		"0.0.0.0:8080",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultCallOptions(
			grpc.MaxCallSendMsgSize(grpcMsgLimit),
			grpc.MaxCallRecvMsgSize(grpcMsgLimit),
		),
	)
	if err != nil {
		log.Fatalln("Failed to dial server:", err)
	}

	// X-Real-IP пробрасывается от доверенного обратного прокси (если фронт-
	// контур развёрнут перед gateway, см. docs/COMPLIANCE.md п. 6.6) и
	// используется в clientInfoFromContext для фиксации IP клиента при
	// акцепте соглашений (ст. 9 152-ФЗ). X-Password — подтверждение паролем
	// при удалении аккаунта (не в query, чтобы не светить креденшел в URL
	// и логах).
	// Сырая загрузка архива (без base64) для распространённых Content-Type;
	// JSON-вариант {"data": "<base64>"} работает через application/json.
	rawBody := &rawBodyMarshaler{
		Marshaler: &runtime.HTTPBodyMarshaler{
			Marshaler: &runtime.JSONPb{
				UnmarshalOptions: protojson.UnmarshalOptions{DiscardUnknown: true},
			},
		},
	}
	gwmux := runtime.NewServeMux(
		runtime.WithIncomingHeaderMatcher(func(key string) (string, bool) {
			switch {
			case strings.EqualFold(key, "X-Real-IP"):
				return "x-real-ip", true
			case strings.EqualFold(key, "X-Password"):
				return "x-password", true
			}
			return runtime.DefaultHeaderMatcher(key)
		}),
		// Проброс Content-Disposition и X-Content-Type-Options (nosniff)
		// от сервиса в HTTP-ответ.
		runtime.WithOutgoingHeaderMatcher(func(key string) (string, bool) {
			switch {
			case strings.EqualFold(key, "Content-Disposition"):
				return "Content-Disposition", true
			case strings.EqualFold(key, "X-Content-Type-Options"):
				return "X-Content-Type-Options", true
			}
			return runtime.DefaultHeaderMatcher(key)
		}),
		runtime.WithErrorHandler(customErrorHandler),
		runtime.WithMarshalerOption("application/zip", rawBody),
		runtime.WithMarshalerOption("application/octet-stream", rawBody),
		runtime.WithMarshalerOption("application/x-zip-compressed", rawBody),
	)
	// Register Greeter
	err = proto.RegisterEveningDetectiveServerHandler(context.Background(), gwmux, conn)
	if err != nil {
		log.Fatalln("Failed to register gateway:", err)
	}

	mainMux := http.NewServeMux()

	docsHandler := v5emb.New("API", "/swagger/doc.json", "/docs/")
	mainMux.Handle("/docs/", docsHandler)
	mainMux.HandleFunc("/swagger/doc.json", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write(swaggerJSON)
	})
	// MaxBytesReader на HTTP-слое: gateway буферизует тело целиком, без лимита
	// проверки в хендлере не спасут от OOM. Лимиты по маршрутам: файлы —
	// maxFileUploadBody, архивы — MaxArchiveSize().
	mainMux.Handle("/api/", limitUploadBody(
		"/api/files/upload",
		maxFileUploadBody,
		limitUploadBody(
			"/api/scenarios/archive",
			int64(scenario_archive.MaxArchiveSize()),
			gwmux,
		),
	))

	// MCP-сервер (Model Context Protocol): эндпоинт /api/mcp (streamable HTTP).
	// Инструменты MCP вызывают игровые сервисы напрямую (см. mcp_service).
	// WithDisableLocalhostProtection: mcp-go отклоняет loopback-запросы с
	// не-localhost Host-заголовком (защита от DNS-rebinding), что ломает
	// работу за reverse-proxy (nginx/caddy) — контур деплоя проекта.
	// Эндпоинт публичный, секрет — пароль команды (аналогично публичному
	// REST /api/teams/{id}/story). Путь /api/mcp длиннее /api/, поэтому
	// http.ServeMux отдаёт MCP-запросы этому хендлеру, а не gateway.
	mcpHTTP := server.NewStreamableHTTPServer(
		mcp_service.NewMCPService(gameService).Server(),
		server.WithDisableLocalhostProtection(true),
	)
	mainMux.Handle("/api/mcp", mcpHTTP)

	gwServer := &http.Server{
		Addr:    ":8090",
		Handler: cors(mainMux),
	}

	log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
	log.Println("Serving docs on http://0.0.0.0:8090/docs")
	log.Fatalln(gwServer.ListenAndServe())
}

func cors(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE")
		// Mcp-Session-Id / Mcp-Protocol-Version / Last-Event-ID — заголовки
		// streamable HTTP MCP-эндпоинта /api/mcp (сессия, версия протокола,
		// SSE-реконнект); без них браузерные MCP-клиенты заблокируются preflight.
		w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, ResponseType, X-Id, X-Password, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID")
		// Content-Disposition нужен фронту, чтобы прочитать имя файла архива;
		// Mcp-Session-Id / Mcp-Protocol-Version — браузерному MCP-клиенту,
		// чтобы читать сессионные заголовки ответов /api/mcp.
		w.Header().Set("Access-Control-Expose-Headers", "Content-Disposition, Mcp-Session-Id, Mcp-Protocol-Version")
		if r.Method == "OPTIONS" {
			return
		}
		h.ServeHTTP(w, r)
	})
}

// limitUploadBody ограничивает тело POST-запроса на заданном маршруте
// (gateway читает тело в память целиком).
func limitUploadBody(route string, limit int64, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodPost && r.URL.Path == route {
			r.Body = http.MaxBytesReader(w, r.Body, limit)
		}
		next.ServeHTTP(w, r)
	})
}

// customErrorHandler мапит ошибки превышения размера тела в HTTP 413
// с понятным текстом:
//   - *http.MaxBytesError (raw) — только для юнит-тестов: сгенерированный
//     gateway-код строкифицирует ошибку декодера через %v и тип теряется;
//   - gRPC ResourceExhausted на путях загрузки (файл больше лимита);
//   - InvalidArgument с текстом MaxBytesError ("http: request body too
//     large") — единственная реальная защита 413 на HTTP-слое, ветку нельзя
//     удалять при «упрощении» обработчика.
//
// Остальные ошибки обрабатываются как обычно (DefaultHTTPErrorHandler).
func customErrorHandler(
	ctx context.Context,
	mux *runtime.ServeMux,
	marshaler runtime.Marshaler,
	w http.ResponseWriter,
	r *http.Request,
	err error,
) {
	var maxBytesErr *http.MaxBytesError
	if errors.As(err, &maxBytesErr) {
		writeTooLargeError(w, marshaler)
		return
	}
	if status.Code(err) == codes.ResourceExhausted {
		switch r.URL.Path {
		case "/api/files/upload", "/api/scenarios/archive":
			writeTooLargeError(w, marshaler)
			return
		}
	}
	if status.Code(err) == codes.InvalidArgument &&
		strings.Contains(status.Convert(err).Message(), "request body too large") {
		writeTooLargeError(w, marshaler)
		return
	}
	runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, err)
}

// writeTooLargeError — ответ 413 в том же формате {"error": ...}, что и
// DefaultHTTPErrorHandler. Текст лимита формируется из maxFileSize, чтобы
// не расходиться с реальным лимитом при его изменении.
func writeTooLargeError(w http.ResponseWriter, marshaler runtime.Marshaler) {
	w.Header().Set("Content-Type", marshaler.ContentType(nil))
	w.WriteHeader(http.StatusRequestEntityTooLarge)
	_ = marshaler.NewEncoder(w).Encode(map[string]string{
		"error": fmt.Sprintf("файл слишком большой (лимит %d МБ)", maxFileSize>>20),
	})
}
