package handlers

import (
	"context" // Ensure context is imported
	"encoding/json"
	"errors" // Added for errors.As
	"fmt"
	"net/http"
	"strconv"

	"git.linuxforward.com/byop/byop-engine/dbstore"
	"git.linuxforward.com/byop/byop-engine/models"
	"git.linuxforward.com/byop/byop-engine/services"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
)

// AppsHandler handles app-related operations and contains integrated service logic
type AppsHandler struct {
	store          *dbstore.SQLiteStore
	entry          *logrus.Entry
	previewService services.PreviewService
	appImporter    *services.AppImporter
}

// NewAppsHandler creates a new AppsHandler
func NewAppsHandler(store *dbstore.SQLiteStore, previewService services.PreviewService, appImporter *services.AppImporter) *AppsHandler {
	return &AppsHandler{
		store:          store,
		entry:          logrus.WithField("component", "AppsHandler"),
		previewService: previewService,
		appImporter:    appImporter,
	}
}

// ListApps returns all apps with optional filtering
func (h *AppsHandler) ListApps(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	filter := make(map[string]interface{})

	// Attempt to bind query parameters, but allow empty filters
	if err := c.ShouldBindQuery(&filter); err != nil && len(filter) > 0 {
		models.RespondWithError(c, models.NewErrValidation("Invalid query parameters", nil, err))
		return
	}

	// Get apps directly from store
	apps, err := h.store.GetAllApps(ctx) // Pass context
	if err != nil {
		models.RespondWithError(c, err) // Pass db error directly
		return
	}

	// If empty, return an empty list
	if len(apps) == 0 {
		c.JSON(http.StatusOK, []*models.App{}) // Return empty slice of pointers
		return
	}

	c.JSON(http.StatusOK, apps)
}

// CreateApp creates a new deployment app
func (h *AppsHandler) CreateApp(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	app := &models.App{}

	if err := c.ShouldBindJSON(&app); err != nil {
		h.entry.WithField("error", err).Error("Failed to bind JSON to App struct")
		models.RespondWithError(c, models.NewErrValidation("Invalid request body", nil, err))
		return
	}

	// Get the user ID from the context (set by auth middleware)
	userIDInterface, exists := c.Get("user_id")
	if !exists {
		models.RespondWithError(c, models.NewErrUnauthorized("User ID not found in context", nil))
		return
	}

	// Convert user_id to int - it might be a string from JWT
	var userID int
	switch v := userIDInterface.(type) {
	case string:
		if parsedID, err := strconv.Atoi(v); err == nil {
			userID = parsedID
		} else {
			h.entry.Warnf("Failed to parse user_id string '%s' to int, defaulting to 1. Error: %v", v, err)
			userID = 1
		}
	case int:
		userID = v
	case int64:
		userID = int(v)
	default:
		h.entry.Warnf("User_id in context is of unexpected type %T, defaulting to 1.", v)
		userID = 1
	}

	// Set the user ID on the app
	app.UserID = uint(userID)
	h.entry.WithField("app", app).Info("JSON binding successful, starting validation")

	// Validate app configuration
	if err := h.validateAppConfig(ctx, app.Components); err != nil { // Pass context
		h.entry.WithField("error", err).Error("App configuration validation failed")
		// Check if the error from validateAppConfig is already a CustomError
		var customErr models.CustomError
		if errors.As(err, &customErr) {
			models.RespondWithError(c, customErr)
		} else {
			models.RespondWithError(c, models.NewErrValidation(fmt.Sprintf("Invalid app configuration: %v", err), nil, err))
		}
		return
	}

	h.entry.Info("App configuration validation passed")

	// Set initial status
	app.Status = "building"

	h.entry.WithField("app", app).Info("About to create app in database")

	// Create the app
	err := h.store.CreateApp(ctx, app) // Pass context
	if err != nil {
		h.entry.WithField("error", err).Error("Failed to create app in database")
		models.RespondWithError(c, err) // Pass db error directly
		return
	}

	// Automatically create preview - this happens async
	h.entry.WithField("app_id", app.ID).Info("Starting automatic preview creation")
	go h.createAppPreviewAsync(context.Background(), app.ID) // Use background context for async operation

	c.JSON(http.StatusCreated, app)
}

// GetApp returns a specific app
func (h *AppsHandler) GetApp(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	idStr := c.Param("id")
	parsedID, err := strconv.ParseUint(idStr, 10, 32)
	if err != nil {
		models.RespondWithError(c, models.NewErrValidation("Invalid app ID format", nil, err))
		return
	}
	id := uint(parsedID)

	// Get app directly from store
	app, err := h.store.GetAppByID(ctx, id) // Pass context and use uint id
	if err != nil {
		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
		return
	}
	// GetAppByID now returns models.NewErrNotFound, so this check might be redundant if RespondWithError handles nil correctly
	// However, GetAppByID returns (*models.App, error), so if err is nil, app could still be nil (though current db logic prevents this for NotFound)
	if app == nil && err == nil { // Explicitly handle if GetAppByID somehow returns nil, nil without specific error
		models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
		return
	}

	c.JSON(http.StatusOK, app)
}

// Updated UpdateApp method
func (h *AppsHandler) UpdateApp(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	id, err := parseUintID(c, "id")
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	updatedApp := &models.App{}
	if err := c.ShouldBindJSON(updatedApp); err != nil {
		models.RespondWithError(c, models.NewErrValidation("Invalid request body", nil, err))
		return
	}

	// Ensure the ID matches the URL parameter
	updatedApp.ID = id

	// Validate app data
	if err := h.validateAppConfig(ctx, updatedApp.Components); err != nil { // Pass context
		var customErr models.CustomError
		if errors.As(err, &customErr) {
			models.RespondWithError(c, customErr)
		} else {
			models.RespondWithError(c, models.NewErrValidation(fmt.Sprintf("Invalid app data: %v", err), nil, err))
		}
		return
	}

	// Check if app exists (though UpdateApp in store will also do this and return ErrNotFound)
	// This is more for a clear early exit if needed, but store.UpdateApp handles it.
	// For consistency, we can rely on store.UpdateApp's error.

	// Validate all components exist and are valid
	var componentIDs []uint
	if updatedApp.Components != "" {
		if err := json.Unmarshal([]byte(updatedApp.Components), &componentIDs); err != nil {
			models.RespondWithError(c, models.NewErrValidation("Invalid components JSON format", nil, err))
			return
		}
	}

	for _, componentID := range componentIDs {
		component, err := h.store.GetComponentByID(ctx, componentID) // Pass context
		if err != nil {
			models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
			return
		}
		// GetComponentByID now returns models.NewErrNotFound, so this check might be redundant
		if component == nil && err == nil { // Explicitly handle if GetComponentByID somehow returns nil, nil
			models.RespondWithError(c, models.NewErrValidation(fmt.Sprintf("Component %d not found during app update validation", componentID), nil, nil))
			return
		}
		if component.Status != "valid" {
			models.RespondWithError(c, models.NewErrValidation(fmt.Sprintf("Component %d is not valid (status: %s)", componentID, component.Status), nil, nil))
			return
		}
	}

	// Set status to building (will be updated by preview creation)
	updatedApp.Status = "building"

	// Update the app
	if err := h.store.UpdateApp(ctx, updatedApp); err != nil { // Pass context
		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
		return
	}

	// Stop any existing previews for this app
	go h.stopExistingPreviews(context.Background(), updatedApp.ID) // Use background context

	// Automatically create new preview
	h.entry.WithField("app_id", updatedApp.ID).Info("Starting automatic preview creation after update")
	go h.createAppPreviewAsync(context.Background(), updatedApp.ID) // Use background context

	c.JSON(http.StatusOK, updatedApp)
}

// DeleteApp deletes an app
func (h *AppsHandler) DeleteApp(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	id, err := parseUintID(c, "id")
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	// Call the store delete method
	if err := h.store.DeleteApp(ctx, id); err != nil { // Pass context
		models.RespondWithError(c, err) // Pass db error directly (handles NotFound, Conflict)
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "App deleted successfully"})
}

// GetAppDeployments returns all deployments for an app
func (h *AppsHandler) GetAppDeployments(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	id, err := parseUintID(c, "id")
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	// Check if app exists first
	app, err := h.store.GetAppByID(ctx, id) // Pass context
	if err != nil {
		models.RespondWithError(c, err) // Pass db error directly (handles NotFound)
		return
	}
	if app == nil && err == nil { // Should be caught by GetAppByID's error handling
		models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
		return
	}

	// Get deployments for this app
	deployments, err := h.store.GetDeploymentsByAppID(ctx, id) // Pass context
	if err != nil {
		models.RespondWithError(c, err) // Pass db error directly
		return
	}

	c.JSON(http.StatusOK, deployments)
}

// GetAppByVersion handles retrieval of an app by name and version
func (h *AppsHandler) GetAppByVersion(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	name := c.Query("name")
	version := c.Query("version")

	if name == "" || version == "" {
		models.RespondWithError(c, models.NewErrValidation("Both name and version query parameters are required", nil, nil))
		return
	}

	// Get app by name and version directly from store
	app, err := h.getAppByNameAndVersion(ctx, name, version) // Pass context
	if err != nil {
		var customErr models.CustomError
		if errors.As(err, &customErr) {
			models.RespondWithError(c, customErr)
		} else {
			models.RespondWithError(c, models.NewErrInternalServer(fmt.Sprintf("Failed to fetch app by name '%s' and version '%s'", name, version), err))
		}
		return
	}

	if app == nil {
		models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with name '%s' and version '%s' not found", name, version), nil))
		return
	}

	c.JSON(http.StatusOK, app)
}

// validateAppConfig checks if the app configuration is valid
func (h *AppsHandler) validateAppConfig(ctx context.Context, componentsJSON string) error { // Added context
	if componentsJSON == "" {
		return models.NewErrValidation("App must have at least one component", nil, nil) // Return custom error
	}

	// Parse the JSON string of component IDs
	var componentIds []uint
	if err := json.Unmarshal([]byte(componentsJSON), &componentIds); err != nil {
		return models.NewErrValidation("Invalid components JSON format", nil, err)
	}

	if len(componentIds) == 0 {
		return models.NewErrValidation("App must have at least one component", nil, nil) // Return custom error
	}

	for _, compId := range componentIds {
		if compId == 0 {
			return models.NewErrValidation("Component ID cannot be zero", nil, nil) // Return custom error
		}

		comp, err := h.store.GetComponentByID(ctx, compId) // Pass context
		if err != nil {
			return err
		}
		if comp == nil && err == nil {
			return models.NewErrValidation(fmt.Sprintf("Component with ID %d does not exist (validation check)", compId), nil, nil)
		}

		if comp.Name == "" {
			return models.NewErrValidation(fmt.Sprintf("Component with ID %d has an empty name", compId), nil, nil) // Return custom error
		}
		if comp.Type == "" {
			return models.NewErrValidation(fmt.Sprintf("Component with ID %d has an empty type", compId), nil, nil) // Return custom error
		}
	}

	return nil
}

// getAppByNameAndVersion retrieves an app by name and version from the store
func (h *AppsHandler) getAppByNameAndVersion(ctx context.Context, name, version string) (*models.App, error) { // Added context
	apps, err := h.store.GetAllApps(ctx) // Pass context
	if err != nil {
		return nil, err
	}

	for _, app := range apps {
		if app.Name == name && app.Description == version { // Using description as version for now
			return app, nil
		}
	}

	return nil, models.NewErrNotFound(fmt.Sprintf("App with name '%s' and version '%s' not found (helper)", name, version), nil) // Return custom error
}

// createAppPreviewAsync creates a preview automatically and updates app status
func (h *AppsHandler) createAppPreviewAsync(ctx context.Context, appId uint) { // Added context
	preview, err := h.previewService.CreatePreview(ctx, appId) // Pass context
	if err != nil {
		h.entry.WithField("app_id", appId).Errorf("Failed to create preview: %v", err)
		if updateErr := h.store.UpdateAppStatus(ctx, appId, "failed", fmt.Sprintf("Failed to create preview: %v", err)); updateErr != nil {
			h.entry.WithField("app_id", appId).Errorf("Additionally failed to update app status after preview failure: %v", updateErr)
		}
		return
	}

	if err := h.store.UpdateAppPreview(ctx, appId, preview.ID, preview.URL); err != nil { // Pass context
		h.entry.WithField("app_id", appId).Errorf("Failed to update app with preview info: %v", err)
	}

	h.entry.WithField("app_id", appId).WithField("preview_id", preview.ID).Info("Automatic preview creation initiated")
}

// stopExistingPreviews stops any running previews for an app
func (h *AppsHandler) stopExistingPreviews(ctx context.Context, appID uint) { // Added context
	previews, err := h.store.GetPreviewsByAppID(ctx, appID) // Pass context
	if err != nil {
		h.entry.WithField("app_id", appID).Errorf("Failed to get existing previews: %v", err)
		return
	}

	for _, preview := range previews {
		if preview.Status == "running" {
			if err := h.previewService.StopPreview(ctx, preview.ID); err != nil { // Pass context
				h.entry.WithField("preview_id", preview.ID).Errorf("Failed to stop existing preview: %v", err)
			}
		}
	}
}

// CreateAppPreview creates a new preview for an app
func (h *AppsHandler) CreateAppPreview(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	id, err := parseUintID(c, "id")
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	app, err := h.store.GetAppByID(ctx, id) // Pass context
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	if app == nil {
		models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
		return
	}

	h.entry.WithField("app_id", app.ID).Info("Creating manual preview for app")
	go h.createAppPreviewAsync(context.Background(), app.ID) // Use background context for async operation

	c.JSON(http.StatusOK, gin.H{"message": "Preview creation started"})
}

// GetAppPreview returns the preview for a specific app
func (h *AppsHandler) GetAppPreview(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	id, err := parseUintID(c, "id")
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	app, err := h.store.GetAppByID(ctx, id) // Pass context
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	if app == nil {
		models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
		return
	}

	preview, err := h.store.GetPreviewByAppID(ctx, app.ID) // Pass context
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	if preview == nil {
		models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("Preview for app ID %d not found", id), nil))
		return
	}

	c.JSON(http.StatusOK, preview)
}

// DeleteAppPreview deletes the preview for a specific app
func (h *AppsHandler) DeleteAppPreview(c *gin.Context) {
	ctx := c.Request.Context() // Get context
	id, err := parseUintID(c, "id")
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	app, err := h.store.GetAppByID(ctx, id) // Pass context
	if err != nil {
		models.RespondWithError(c, err)
		return
	}

	if app == nil {
		models.RespondWithError(c, models.NewErrNotFound(fmt.Sprintf("App with ID %d not found", id), nil))
		return
	}

	if err := h.previewService.DeletePreview(ctx, app.ID); err != nil { // Pass context
		models.RespondWithError(c, err)
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Preview deleted successfully"})
}

// ImportReview reviews a docker-compose file for importing an app
func (h *AppsHandler) ImportReview(c *gin.Context) {
	ctx := c.Request.Context()
	var req models.AppImportRequest

	if err := c.ShouldBindJSON(&req); err != nil {
		h.entry.WithField("error", err).Error("Failed to bind JSON to AppImportRequest struct")
		models.RespondWithError(c, models.NewErrValidation("Invalid request body", nil, err))
		return
	}

	// Set default branch if not provided
	if req.Branch == "" {
		req.Branch = "main"
	}

	h.entry.Infof("Reviewing compose import from %s (branch: %s)", req.SourceURL, req.Branch)

	review, err := h.appImporter.ReviewComposeFile(ctx, req)
	if err != nil {
		h.entry.WithField("error", err).Error("Failed to review compose file")
		models.RespondWithError(c, models.NewErrInternalServer("Failed to review compose file", err))
		return
	}

	c.JSON(http.StatusOK, review)
}

// ImportCreate creates an app and components from a docker-compose file
func (h *AppsHandler) ImportCreate(c *gin.Context) {
	ctx := c.Request.Context()
	var req models.AppImportCreateRequest

	if err := c.ShouldBindJSON(&req); err != nil {
		h.entry.WithField("error", err).Error("Failed to bind JSON to AppImportCreateRequest struct")
		models.RespondWithError(c, models.NewErrValidation("Invalid request body", nil, err))
		return
	}

	// Set default branch if not provided
	if req.Branch == "" {
		req.Branch = "main"
	}

	// Get the user ID from the context (set by auth middleware)
	userIDInterface, exists := c.Get("user_id")
	if !exists {
		models.RespondWithError(c, models.NewErrUnauthorized("User ID not found in context", nil))
		return
	}

	// Convert user_id to uint - it might be a string from JWT
	var userID uint
	switch v := userIDInterface.(type) {
	case string:
		if parsedID, err := strconv.ParseUint(v, 10, 32); err == nil {
			userID = uint(parsedID)
		} else {
			h.entry.Warnf("Failed to parse user_id string '%s' to uint, defaulting to 1. Error: %v", v, err)
			userID = 1
		}
	case int:
		userID = uint(v)
	case int64:
		userID = uint(v)
	case uint:
		userID = v
	default:
		h.entry.Warnf("User_id in context is of unexpected type %T, defaulting to 1.", v)
		userID = 1
	}

	h.entry.Infof("Creating app '%s' from compose import for user %d", req.ConfirmedAppName, userID)

	app, err := h.appImporter.CreateAppFromCompose(ctx, req, userID)
	if err != nil {
		h.entry.WithField("error", err).Error("Failed to create app from compose")
		models.RespondWithError(c, models.NewErrInternalServer("Failed to create app from compose", err))
		return
	}

	c.JSON(http.StatusCreated, app)
}

// parseUintID parses a string ID parameter to uint
func parseUintID(c *gin.Context, paramName string) (uint, error) {
	idStr := c.Param(paramName)
	parsedID, err := strconv.ParseUint(idStr, 10, 32)
	if err != nil {
		return 0, models.NewErrValidation(fmt.Sprintf("Invalid %s ID format", paramName), nil, err)
	}
	return uint(parsedID), nil
}
