package http_router

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth"
	"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth/keycloak"
	"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
	"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
	"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/object_storage"
	"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/types"
	"github.com/aws/smithy-go/ptr"
	"github.com/gorilla/mux"
)

// =============== FILE MANAGEMENT ===============

// TempFileManager управляет временными файлами с автоматической очисткой
type TempFileManager struct {
	baseDir string
	mu      sync.Mutex
	files   map[string]*os.File
}

// Cleanup очищает все временные файлы при завершении работы
func (h *handler) Cleanup() {
	h.tempFileManager.CleanupAll()
}

func NewTempFileManager(baseDir string) (*TempFileManager, error) {
	if baseDir == "" {
		baseDir = filepath.Join(os.TempDir(), "logo-uploads")
	}

	if err := os.MkdirAll(baseDir, constants.TempDirPermission); err != nil {
		return nil, fmt.Errorf("failed to create temp directory: %w", err)
	}

	return &TempFileManager{
		baseDir: baseDir,
		files:   make(map[string]*os.File),
	}, nil
}

func (tm *TempFileManager) CreateTempFile(pattern string) (*os.File, error) {
	tempFile, err := os.CreateTemp(tm.baseDir, pattern)
	if err != nil {
		return nil, err
	}

	tm.mu.Lock()
	tm.files[tempFile.Name()] = tempFile
	tm.mu.Unlock()

	return tempFile, nil
}

func (tm *TempFileManager) CleanupFile(file *os.File) error {
	if file == nil {
		return nil
	}

	tm.mu.Lock()
	delete(tm.files, file.Name())
	tm.mu.Unlock()

	name := file.Name()
	if err := file.Close(); err != nil {
		slog.Warn("failed to close temp file", slog.String("file", name), slog.String("error", err.Error()))
	}

	if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to remove temp file %s: %w", name, err)
	}

	return nil
}

func (tm *TempFileManager) CleanupAll() {
	tm.mu.Lock()
	defer tm.mu.Unlock()

	for name, file := range tm.files {
		_ = file.Close()
		_ = os.Remove(name)
	}

	tm.files = make(map[string]*os.File)
}

type LogoFile struct {
	File        *os.File
	ContentType string
	Size        int64
	manager     *TempFileManager
}

// Close закрывает файл и удаляет временный файл
func (lf *LogoFile) Close() error {
	if lf.manager != nil {
		return lf.manager.CleanupFile(lf.File)
	}

	if lf.File != nil {
		name := lf.File.Name()
		_ = lf.File.Close()

		return os.Remove(name)
	}

	return nil
}

// Reset сбрасывает позицию чтения в начало файла
func (lf *LogoFile) Reset() error {
	_, err := lf.File.Seek(0, io.SeekStart)
	return err
}

// =============== LOGO HANDLERS ===============

func (h *handler) getCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
	const handlerName = "getCompanyLogoHandler"

	distributorID, companyID, token := h.extractLogoRequestData(r)
	if err := h.validateUserAccess(token, distributorID); err != nil {
		h.handleAccessError(w, err, handlerName)
		return
	}

	logoLink, err := h.getLogoLinkWithCache(r.Context(), companyID)
	if err != nil {
		if errors.Is(err, object_storage.ErrObjectNotFound) {
			response := types.GetLogoResponse{
				Link:      "",
				ExpiresIn: time.Time{},
			}

			w.Header().Set("Content-Type", "application/json")

			if err := json.NewEncoder(w).Encode(response); err != nil {
				h.logger.Error("error encoding response",
					slog.String("error", err.Error()),
					slog.String("handler", handlerName))
			}

			return
		}

		http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
		h.logger.Error("error getting logo link",
			slog.String("error", err.Error()),
			slog.String("handler", handlerName))

		return
	}

	response := types.GetLogoResponse{
		Link:      logoLink,
		ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
	}

	w.Header().Set("Content-Type", "application/json")

	if err := json.NewEncoder(w).Encode(response); err != nil {
		h.logger.Error("error encoding response",
			slog.String("error", err.Error()),
			slog.String("handler", handlerName))
	}
}

func (h *handler) createCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
	const handlerName = "createCompanyLogoHandler"

	distributorID, companyID, token := h.extractLogoRequestData(r)

	if err := h.validateUserAccess(token, distributorID); err != nil {
		h.handleAccessError(w, err, handlerName)
		return
	}

	if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
		h.handleAccessError(w, err, handlerName)
		return
	}

	logoFile, err := h.extractAndValidateLogoFile(r)
	if err != nil {
		h.handleUploadError(w, err, handlerName)
		return
	}
	defer logoFile.Close()

	// Сбрасываем позицию после валидации, чтобы не потерять данные
	if err := logoFile.Reset(); err != nil {
		h.logger.Error("failed to reset file position",
			slog.String("error", err.Error()),
			slog.String("handler", handlerName))
		http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)

		return
	}

	// может стоит добавить таймаут записи в кэш
	err = h.objectStorageClient.PutNewObject(
		r.Context(),
		companyID,
		object_storage.LogoCategory,
		logoFile.File,
		object_storage.PutOptions{
			ContentType: logoFile.ContentType,
		},
	)
	if err != nil {
		if errors.Is(err, object_storage.ErrObjectAlreadyExists) {
			http.Error(w, "Logo already exists. Use PUT to update", http.StatusConflict)
			return
		}

		http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
		h.logger.Error("error uploading logo",
			slog.String("error", err.Error()),
			slog.String("handler", handlerName))

		return
	}

	h.refreshLogoCacheAsync(companyID)
	w.WriteHeader(http.StatusCreated)
}

func (h *handler) updateCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
	const handlerName = "updateCompanyLogoHandler"

	distributorID, companyID, token := h.extractLogoRequestData(r)

	if err := h.validateUserAccess(token, distributorID); err != nil {
		h.handleAccessError(w, err, handlerName)
		return
	}

	if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
		h.handleAccessError(w, err, handlerName)
		return
	}

	logoFile, err := h.extractAndValidateLogoFile(r)
	if err != nil {
		h.handleUploadError(w, err, handlerName)
		return
	}
	defer logoFile.Close()

	// Сбрасываем позицию после валидации, чтобы не потерять данные
	if err := logoFile.Reset(); err != nil {
		h.logger.Error("failed to reset file position",
			slog.String("error", err.Error()),
			slog.String("handler", handlerName))
		http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)

		return
	}

	// Обновляем в хранилище
	err = h.objectStorageClient.UpdateObject(
		r.Context(),
		companyID,
		object_storage.LogoCategory,
		logoFile.File,
		object_storage.UpdateOptions{
			ContentType: logoFile.ContentType,
		},
	)
	if err != nil {
		if errors.Is(err, object_storage.ErrObjectNotFound) {
			http.Error(w, "Logo not found. Use POST to create", http.StatusNotFound)
			return
		}

		http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
		h.logger.Error("error updating logo",
			slog.String("error", err.Error()),
			slog.String("handler", handlerName))

		return
	}

	h.refreshLogoCacheAsync(companyID)
	w.WriteHeader(http.StatusNoContent)
}

func (h *handler) deleteCompanyLogoHandler(w http.ResponseWriter, r *http.Request) {
	const handlerName = "deleteCompanyLogoHandler"

	distributorID, companyID, token := h.extractLogoRequestData(r)

	if err := h.validateUserAccess(token, distributorID); err != nil {
		h.handleAccessError(w, err, handlerName)
		return
	}

	if err := h.validateUserPermissions(r.Context(), distributorID, keycloak.PermissionCompany, keycloak.PermissionLevelCanEdit); err != nil {
		h.handleAccessError(w, err, handlerName)
		return
	}

	err := h.objectStorageClient.DeleteObject(r.Context(), companyID, object_storage.LogoCategory)
	if err != nil {
		if errors.Is(err, object_storage.ErrObjectNotFound) {
			http.Error(w, constants.ErrNotFound.Error(), http.StatusNotFound)
			return
		}

		http.Error(w, constants.ErrInternalServerError.Error(), http.StatusInternalServerError)
		h.logger.Error("error deleting logo",
			slog.String("error", err.Error()),
			slog.String("handler", handlerName))

		return
	}

	// Очищаем кэш
	if err := h.cacheClient.Del(r.Context(), companyID, cache.LogoValueType); err != nil {
		h.logger.Error("error deleting logo from cache",
			slog.String("error", err.Error()),
			slog.String("company_id", companyID))
	}

	w.WriteHeader(http.StatusNoContent)
}

// =============== CORE LOGIC FUNCTIONS ===============

// extractAndValidateLogoFile извлекает файл из запроса и сохраняет во временный файл
func (h *handler) extractAndValidateLogoFile(r *http.Request) (*LogoFile, error) {
	err := r.ParseMultipartForm(constants.ParseMultipartFormAllFile) //сохраняем на диск весь файл
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrMultipartParse, err)
	}

	defer func() { _ = r.MultipartForm.RemoveAll() }()

	file, header, err := r.FormFile("logo")
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrFileNotFound, err)
	}
	defer file.Close()

	// Проверяем размер файла
	if header.Size == 0 {
		return nil, ErrEmptyFile
	}

	if header.Size > constants.LogoMaxSize {
		return nil, &FileTooLargeError{Size: header.Size, MaxSize: constants.LogoMaxSize}
	}

	contentType := header.Header.Get("Content-Type")
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	// Создаем временный файл для копирования
	tempFile, err := h.tempFileManager.CreateTempFile(constants.TempLogoFilePattern)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTempFileCreate, err)
	}

	// Копируем содержимое во временный файл
	copiedBytes, err := io.CopyN(tempFile, file, constants.LogoMaxSize+1)
	if err != nil && err != io.EOF {
		_ = h.tempFileManager.CleanupFile(tempFile)
		return nil, fmt.Errorf("%w: failed to copy file: %v", ErrFileOperation, err)
	}

	logoFile := &LogoFile{
		File:        tempFile,
		ContentType: contentType,
		Size:        copiedBytes,
		manager:     h.tempFileManager,
	}

	if err := h.validateLogoFile(logoFile); err != nil {
		_ = logoFile.Close()
		return nil, err
	}

	return logoFile, nil
}

// validateLogoFile валидирует файл изображения
func (h *handler) validateLogoFile(logoFile *LogoFile) error {
	// Проверяем заявленный тип
	if !isValidImageType(logoFile.ContentType) {
		h.logger.Error("invalid image type",
			slog.String("image_type", logoFile.ContentType),
			slog.String("handler", "validateLogoFile"))

		return &InvalidImageTypeError{ContentType: logoFile.ContentType}
	}

	if err := logoFile.Reset(); err != nil {
		return fmt.Errorf("%w: failed to reset file position before validation: %v", ErrFileOperation, err)
	}

	// Читаем первые 512 байт для определения реального типа
	buffer := make([]byte, constants.BytesToDetectContentType)

	firstBytes, err := logoFile.File.Read(buffer)
	if err != nil && err != io.EOF {
		return fmt.Errorf("%w: failed to read file header: %v", ErrFileOperation, err)
	}

	detectedType := http.DetectContentType(buffer[:firstBytes])
	if !isValidImageType(detectedType) {
		h.logger.Error("invalid image type after detecting",
			slog.String("image_type", detectedType),
			slog.String("handler", "validateLogoFile"))

		return &InvalidImageTypeError{ContentType: detectedType}
	}

	// Возвращаем позицию в начало
	if err := logoFile.Reset(); err != nil {
		return fmt.Errorf("%w: failed to reset file position: %v", ErrFileOperation, err)
	}

	return nil
}

// extractLogoRequestData извлекает данные из HTTP запроса
func (h *handler) extractLogoRequestData(r *http.Request) (distributorID, companyID, token string) {
	vars := mux.Vars(r)
	distributorID = vars["distributor_id"]
	companyID = vars["company_id"]
	token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")

	return
}

// =============== VALIDATION FUNCTIONS ===============

// validateUserAccess проверяет авторизацию и соответствие пользователя
func (h *handler) validateUserAccess(token, expectedUserID string) error {
	if token == "" {
		return constants.ErrUnauthorized
	}

	userInfo, err := h.jwtManager.GetUserInfoFromToken(token)
	if err != nil {
		return fmt.Errorf("invalid token: %w", err)
	}

	if expectedUserID != userInfo.UserId {
		return constants.ErrForbidden
	}

	return nil
}

// validateUserPermissions проверяет права доступа пользователя
func (h *handler) validateUserPermissions(ctx context.Context, userID, permission, level string) error {
	ok, err := h.authManager.CheckPermissions(ctx, auth.CheckPermissionsRequest{
		UserId:                  userID,
		RequiredPermission:      permission,
		RequiredPermissionLevel: level,
	})
	if err != nil {
		return fmt.Errorf("permission check failed: %w", err)
	}

	if !ok {
		return constants.ErrForbidden
	}

	return nil
}

// isValidImageType проверяет допустимость типа изображения
func isValidImageType(contentType string) bool {
	contentType = strings.ToLower(strings.TrimSpace(contentType))

	if idx := strings.Index(contentType, ";"); idx != -1 {
		contentType = contentType[:idx]
	}

	return constants.ValidImageTypes[contentType]
}

// =============== CACHE FUNCTIONS ===============

// refreshLogoCacheAsync асинхронно обновляет кэш логотипа
func (h *handler) refreshLogoCacheAsync(companyID string) {
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), constants.RefreshLogoCacheAsyncTimeout)
		defer cancel()

		// Получаем новую ссылку на логотип
		logoLink, err := h.objectStorageClient.GetPresignedLink(ctx,
			companyID, object_storage.LogoCategory, object_storage.LinkOptions{
				TTL: ptr.Duration(constants.LogoLinkTTL),
			})
		if err != nil {
			h.logger.Warn("failed to get logo link for cache refresh",
				slog.String("error", err.Error()),
				slog.String("company_id", companyID))

			return
		}

		if err = h.cacheClient.Del(ctx, companyID, cache.LogoValueType); err != nil {
			h.logger.Warn("failed to delete logo from cache, it's ok if it first time creation",
				slog.String("error", err.Error()),
				slog.String("company_id", companyID))
		}

		response := types.GetLogoResponse{
			Link:      logoLink,
			ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
		}

		body, err := json.Marshal(response)
		if err != nil {
			h.logger.Warn("failed to marshal logo response for cache",
				slog.String("error", err.Error()),
				slog.String("company_id", companyID))

			return
		}

		if err := h.cacheClient.Set(ctx, companyID, cache.LogoValueType, string(body), constants.LogoLinkTTL); err != nil {
			h.logger.Warn("failed to update logo cache",
				slog.String("error", err.Error()),
				slog.String("company_id", companyID))
		} else {
			h.logger.Debug("logo cache refreshed successfully",
				slog.String("company_id", companyID))
		}
	}()
}

// getLogoLinkWithCache получает ссылку на логотип из кэша или хранилища
func (h *handler) getLogoLinkWithCache(ctx context.Context, companyID string) (string, error) {
	// Проверяем кэш
	cachedData, err := h.cacheClient.Get(ctx, companyID, cache.LogoValueType)
	if err == nil {
		var response types.GetLogoResponse
		if err := json.Unmarshal([]byte(cachedData), &response); err == nil {
			return response.Link, nil
		}

		h.logger.Warn("failed to unmarshal logo response from cache",
			slog.String("company_id", companyID))
	}

	if !errors.Is(err, cache.ErrKeyNotFound) {
		h.logger.Warn("failed to get logo from cache",
			slog.String("error", err.Error()),
			slog.String("company_id", companyID))
	}

	// Получаем из хранилища ссылку на логотип
	logoLink, err := h.objectStorageClient.GetPresignedLink(ctx,
		companyID, object_storage.LogoCategory, object_storage.LinkOptions{
			TTL: ptr.Duration(constants.LogoLinkTTL),
		})
	if err != nil {
		return "", err
	}

	response := types.GetLogoResponse{
		Link:      logoLink,
		ExpiresIn: time.Now().Add(constants.LogoLinkTTL),
	}

	body, err := json.Marshal(response)
	if err != nil {
		h.logger.Warn("failed to marshal logo response for cache",
			slog.String("error", err.Error()),
			slog.String("company_id", companyID))
	} else {
		if err := h.cacheClient.Set(ctx, companyID, cache.LogoValueType, string(body), constants.LogoLinkTTL); err != nil {
			h.logger.Warn("failed to cache logo response",
				slog.String("error", err.Error()),
				slog.String("company_id", companyID))
		}
	}

	return logoLink, nil
}

// =============== GRACEFUL SHUTDOWN ===============

// SetupGracefulShutdown делает graceful shutdown)
func (h *handler) SetupGracefulShutdown(ctx context.Context) {
	go func() {
		<-ctx.Done()
		h.logger.Info("shutting down, cleaning up temporary files")
		h.Cleanup()
	}()
}
