package rest

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/pkg/apierr"
	"github.com/nexuscrm/backend/pkg/utils"
	"github.com/nexuscrm/shared/pkg/constants"
	sharedEvents "github.com/nexuscrm/shared/pkg/events"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

// Maximum file upload size for bulk operations (100MB)
const maxBulkUploadSize = 100 * 1024 * 1024

func shouldDeferBulkJobObjectPermissionCheck(schema *models.ObjectMetadata, operation models.BulkOperation, permissionSvc *permissions.PermissionService) bool {
	if schema == nil || permissionSvc == nil || !schema.UsesDerivedAccessPolicy() {
		return false
	}
	switch operation {
	case models.OperationUpdate:
		return permissionSvc.AccessPolicyRequiresRecordContext(schema, constants.PermEdit)
	case models.OperationUpsert:
		// Upsert still validates create permission up front, but existing-row
		// updates must defer the edit gate until the target record is loaded.
		return permissionSvc.AccessPolicyRequiresRecordContext(schema, constants.PermEdit)
	case models.OperationDelete:
		return permissionSvc.AccessPolicyRequiresRecordContext(schema, constants.PermDelete)
	default:
		return false
	}
}

// CreateJob godoc
// @Summary      Create Bulk Job
// @Description  Create a new bulk ingest job.
// @Tags         bulk
// @Accept       json
// @Produce      json
// @Param        job body models.BulkJob true "Job Definition"
// @Success      200  {object}  models.BulkJob
// @Failure      400  {object}  map[string]interface{}
// @Router       /jobs/ingest [post]
// @Security     BearerAuth
func (h *BulkHandler) CreateJob(c *gin.Context) {
	var req models.BulkJob
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid request body"))
		return
	}

	user := MustGetUser(c)

	schema := h.metadataSvc.GetSchema(c.Request.Context(), req.Object)
	if schema == nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidObject, fmt.Sprintf("Object '%s' does not exist", req.Object)))
		return
	}

	// Security: Check user has appropriate permission on the target object based on operation
	var requiredPerm string
	deferRecordAwareObjectGate := false
	switch req.Operation {
	case models.OperationInsert:
		requiredPerm = constants.PermCreate
	case models.OperationUpdate:
		requiredPerm = constants.PermEdit
		deferRecordAwareObjectGate = shouldDeferBulkJobObjectPermissionCheck(schema, req.Operation, h.permissionSvc)
	case models.OperationUpsert:
		// Upsert requires both create and edit permissions
		if !h.permissionSvc.CheckObjectPermissionWithUser(c.Request.Context(), req.Object, constants.PermCreate, user) {
			c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeInsufficientPerms, fmt.Sprintf("Upsert requires create permission on '%s'", req.Object)))
			return
		}
		deferRecordAwareObjectGate = shouldDeferBulkJobObjectPermissionCheck(schema, req.Operation, h.permissionSvc)
		requiredPerm = constants.PermEdit // Check edit below
	case models.OperationDelete:
		requiredPerm = constants.PermDelete
		deferRecordAwareObjectGate = shouldDeferBulkJobObjectPermissionCheck(schema, req.Operation, h.permissionSvc)
	default:
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidOperation, fmt.Sprintf("Invalid operation: '%s'. Valid operations: insert, update, upsert, delete", req.Operation)))
		return
	}

	if !deferRecordAwareObjectGate && !h.permissionSvc.CheckObjectPermissionWithUser(c.Request.Context(), req.Object, requiredPerm, user) {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeInsufficientPerms, fmt.Sprintf("You do not have %s permission on '%s'", requiredPerm, req.Object)))
		return
	}

	job := &models.BulkJob{
		ID:              utils.GenerateID(),
		Object:          req.Object,
		Operation:       req.Operation,
		State:           models.JobStateOpen,
		ConcurrencyMode: models.ConcurrencyParallel, // Default
		ContentType:     "CSV",                      // Salesforce default
		ColumnDelimiter: "COMMA",                    // Salesforce default
		LineEnding:      "LF",                       // Salesforce default
		ApiVersion:      63.0,                       // Current API version
		CreatedById:     user.ID,
		CreatedDate:     time.Now(),
		SystemModstamp:  time.Now(),
	}

	if err := h.bulkSvc.CreateJob(c.Request.Context(), job); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	c.JSON(http.StatusCreated, job)
}

// GetJob godoc
// @Summary      Get Job Status
// @Description  Get the status of a bulk job.
// @Tags         bulk
// @Accept       json
// @Produce      json
// @Param        jobId path string true "Job ID"
// @Success      200  {object}  models.BulkJob
// @Failure      404  {object}  map[string]interface{}
// @Router       /jobs/ingest/{jobId} [get]
// @Security     BearerAuth
func (h *BulkHandler) GetJob(c *gin.Context) {
	jobId := c.Param("jobId")
	user := MustGetUser(c)

	job, err := h.bulkSvc.GetJob(c.Request.Context(), jobId)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}
	if job == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Job not found"))
		return
	}

	if !user.IsSystemAdmin && job.CreatedById != user.ID {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, "Access denied to this job"))
		return
	}

	c.JSON(http.StatusOK, job)
}

// CloseJob godoc
// @Summary      Close Job
// @Description  Close a bulk job to start processing.
// @Tags         bulk
// @Accept       json
// @Produce      json
// @Param        jobId path string true "Job ID"
// @Param        state body map[string]string true "Job State (UploadComplete)"
// @Success      200  {object}  models.BulkJob
// @Failure      400  {object}  map[string]interface{}
// @Router       /jobs/ingest/{jobId} [patch]
// @Security     BearerAuth
func (h *BulkHandler) CloseJob(c *gin.Context) {
	jobId := c.Param("jobId")
	user := MustGetUser(c)

	job, err := h.bulkSvc.GetJob(c.Request.Context(), jobId)
	if err != nil || job == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Job not found"))
		return
	}
	if !user.IsSystemAdmin && job.CreatedById != user.ID {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, "Access denied to this job"))
		return
	}

	var req struct {
		State models.BulkJobState `json:"state"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Invalid request body"))
		return
	}

	if req.State != models.JobStateUploadComplete && req.State != models.JobStateAborted {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "Only UploadComplete or Aborted state changes are allowed"))
		return
	}

	if err := h.bulkSvc.UpdateJobState(c.Request.Context(), jobId, req.State); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	if req.State == models.JobStateUploadComplete {
		log.Info().Str("job_id", jobId).Msg("Job closed. Publishing bulk.job.closed event...")
		if h.eventBus != nil {
			if err := h.eventBus.Publish(c.Request.Context(), sharedEvents.BulkJobClosed, map[string]interface{}{
				"jobId": jobId,
			}); err != nil {
				log.Error().Err(err).Str("job_id", jobId).Msg("Failed to publish bulk.job.closed event")
			}
		}
	}

	// Return updated job (refresh to get current state)
	updatedJob, err := h.bulkSvc.GetJob(c.Request.Context(), jobId)
	if err != nil {
		log.Warn().Err(err).Str("job_id", jobId).Msg("Failed to refresh job after close")
		c.JSON(http.StatusOK, job) // Return original job if refresh fails
		return
	}
	c.JSON(http.StatusOK, updatedJob)
}

// UploadData godoc
// @Summary      Upload Batch Data
// @Description  Upload CSV data for a bulk job.
// @Tags         bulk
// @Accept       text/csv
// @Produce      json
// @Param        jobId path string true "Job ID"
// @Param        file formData file true "CSV Content"
// @Success      201  {object}  map[string]interface{}
// @Failure      400  {object}  map[string]interface{}
// @Router       /jobs/ingest/{jobId}/batches [put]
// @Security     BearerAuth
func (h *BulkHandler) UploadData(c *gin.Context) {
	jobId := c.Param("jobId")
	user := MustGetUser(c)

	// Check if job exists and is Open
	job, err := h.bulkSvc.GetJob(c.Request.Context(), jobId)
	if err != nil || job == nil {
		c.JSON(http.StatusNotFound, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Job not found"))
		return
	}

	if !user.IsSystemAdmin && job.CreatedById != user.ID {
		c.JSON(http.StatusForbidden, apierr.SFErrorResponse(apierr.SFCodeForbidden, "Access denied to this job"))
		return
	}

	if job.State != models.JobStateOpen {
		c.JSON(http.StatusBadRequest, apierr.SFErrorResponse(apierr.SFCodeInvalidJobState, "Job is not in Open state"))
		return
	}

	if c.Request.ContentLength > maxBulkUploadSize {
		c.JSON(http.StatusRequestEntityTooLarge, apierr.SFErrorResponse(apierr.SFCodeFileTooLarge, fmt.Sprintf("File size exceeds maximum allowed size of %d MB", maxBulkUploadSize/(1024*1024))))
		return
	}

	// Generate Batch ID early
	batchID := utils.GenerateID()
	filePath := fmt.Sprintf("data/uploads/%s.csv", batchID)

	// Ensure directory exists
	if err := os.MkdirAll("data/uploads", 0755); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "Failed to create upload directory"))
		return
	}

	// Create file
	out, err := os.Create(filePath)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "Failed to create upload file"))
		return
	}
	defer out.Close()

	// Stream to file with size limit (defense in depth)
	limitedReader := io.LimitReader(c.Request.Body, maxBulkUploadSize+1)
	written, err := io.Copy(out, limitedReader)
	if err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, "Failed to save body"))
		return
	}
	if written > maxBulkUploadSize {
		// Cleanup the file if it exceeded the limit
		_ = os.Remove(filePath)
		c.JSON(http.StatusRequestEntityTooLarge, apierr.SFErrorResponse(apierr.SFCodeFileTooLarge, fmt.Sprintf("File size exceeds maximum allowed size of %d MB", maxBulkUploadSize/(1024*1024))))
		return
	}

	// Create Batch Record
	batch := &models.BulkBatch{
		ID:             batchID,
		JobID:          jobId,
		State:          "Queued",
		CreatedDate:    time.Now(),
		SystemModstamp: time.Now(),
	}

	if err := h.bulkSvc.CreateBatch(c.Request.Context(), batch); err != nil {
		c.JSON(http.StatusInternalServerError, apierr.SFErrorResponse(apierr.SFCodeInternalError, err.Error()))
		return
	}

	c.Status(http.StatusCreated)
}
