package rest

import (
	"bytes"
	"context"
	"encoding/xml"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	"archive/zip"
	"encoding/json"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/nexuscrm/backend/pkg/apierr"
	appErrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

var safeIdentifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)

// --- Deploy Request Handlers ---

// DeployPollResult represents the result of a metadata deployment.
type DeployPollResult struct {
	Done         bool           `json:"done"`
	Status       string         `json:"status"`
	Success      bool           `json:"success,omitempty"`
	ErrorMessage string         `json:"errorMessage,omitempty"`
	Details      *DeployDetails `json:"details,omitempty"`
}

// DeployDetails contains deployment failure information.
type DeployDetails struct {
	ComponentFailures []ComponentFailure `json:"componentFailures,omitempty"`
}

// ComponentFailure describes a single component failure during deployment.
type ComponentFailure struct {
	Problem       string `json:"problem,omitempty"`
	FileName      string `json:"fileName,omitempty"`
	ComponentType string `json:"componentType,omitempty"`
}

// ApprovalProcessXML represents the XML structure of an approval process metadata file.
type ApprovalProcessXML struct {
	XMLName                  xml.Name            `xml:"ApprovalProcess"`
	Label                    string              `xml:"label"`
	Active                   bool                `xml:"active"`
	Description              string              `xml:"description"`
	RecordEditability        string              `xml:"recordEditability"`
	FinalApprovalRecordLock  string              `xml:"finalApprovalRecordLock"`
	FinalRejectionRecordLock string              `xml:"finalRejectionRecordLock"`
	EntryCriteria            *EntryCriteriaXML   `xml:"entryCriteria,omitempty"`
	Steps                    []ApprovalStepXML   `xml:"approvalStep"`
	InitialSubmissionActions *ApprovalActionsXML `xml:"initialSubmissionActions,omitempty"`
	StepApprovalActions      *ApprovalActionsXML `xml:"stepApprovalActions,omitempty"`
	FinalApprovalActions     *ApprovalActionsXML `xml:"finalApprovalActions,omitempty"`
	FinalRejectionActions    *ApprovalActionsXML `xml:"finalRejectionActions,omitempty"`
	RecallActions            *ApprovalActionsXML `xml:"recallActions,omitempty"`
	ReassignActions          *ApprovalActionsXML `xml:"reassignActions,omitempty"`
}

// EntryCriteriaXML represents the entry criteria block in approval process XML.
type EntryCriteriaXML struct {
	Formula       string            `xml:"formula,omitempty"`
	BooleanFilter string            `xml:"booleanFilter,omitempty"`
	CriteriaItems []CriteriaItemXML `xml:"criteriaItems,omitempty"`
}

// CriteriaItemXML represents a single criteria item in entry criteria.
type CriteriaItemXML struct {
	Field     string `xml:"field"`
	Operation string `xml:"operation"`
	Value     string `xml:"value"`
}

// EntryCriteriaToFilterString converts parsed XML entry criteria to a human-readable filter string.
func EntryCriteriaToFilterString(ec *EntryCriteriaXML) string {
	if ec == nil {
		return ""
	}
	if ec.Formula != "" {
		return ec.Formula
	}
	if len(ec.CriteriaItems) == 0 {
		return ""
	}
	parts := make([]string, 0, len(ec.CriteriaItems))
	for _, item := range ec.CriteriaItems {
		parts = append(parts, fmt.Sprintf("%s %s %s", item.Field, item.Operation, item.Value))
	}
	return strings.Join(parts, " AND ")
}

// ApprovalActionsXML wraps a list of actions for a specific lifecycle event.
type ApprovalActionsXML struct {
	Actions []ApprovalActionXML `xml:"actions"`
}

// ApprovalActionXML represents a single action entry in deployment XML.
type ApprovalActionXML struct {
	Name            string `xml:"name"`
	Type            string `xml:"type"`
	FieldApiName    string `xml:"fieldApiName,omitempty"`
	FieldValue      string `xml:"fieldValue,omitempty"`
	EmailTemplateId string `xml:"emailTemplateId,omitempty"`
	ApiName         string `xml:"apiName,omitempty"`
}

// ApprovalStepXML represents a single step in an approval process XML.
type ApprovalStepXML struct {
	Name                 string                     `xml:"name"`
	Label                string                     `xml:"label"`
	Description          string                     `xml:"description,omitempty"`
	AllowComments        *bool                      `xml:"allowComments,omitempty"`
	RequireComments      *bool                      `xml:"requireComments,omitempty"`
	RejectBehavior       *string                    `xml:"rejectBehavior,omitempty"`
	AssignedApprover     *DeployAssignedApproverXML `xml:"assignedApprover,omitempty"`
	StepApprovalActions  *ApprovalActionsXML        `xml:"stepApprovalActions,omitempty"`
	StepRejectionActions *ApprovalActionsXML        `xml:"stepRejectionActions,omitempty"`
}

// DeployAssignedApproverXML represents the assigned approver block in deployment XML.
type DeployAssignedApproverXML struct {
	Approvers []DeployApproverXML `xml:"approver"`
}

// DeployApproverXML represents a single approver entry in deployment XML.
type DeployApproverXML struct {
	Name string `xml:"name"`
	Type string `xml:"type"`
}

func approvalProcessState(active bool) string {
	if active {
		return "Active"
	}
	return "Inactive"
}

func buildProcessDefinitionUpdateRecord(xmlData *ApprovalProcessXML, xmlBytes []byte, state string) models.SObject {
	rec := models.SObject{
		"Name":        xmlData.Label,
		"Description": xmlData.Description,
		"State":       state,
		"IsActive":    xmlData.Active,
		"EntryFilter": EntryCriteriaToFilterString(xmlData.EntryCriteria),
		"Metadata":    string(xmlBytes),
	}
	applyApprovalLockFields(rec, xmlData)
	return rec
}

func buildProcessDefinitionInsertRecord(xmlData *ApprovalProcessXML, xmlBytes []byte, devName, objectName, state string) models.SObject {
	rec := models.SObject{
		"Name":          xmlData.Label,
		"DeveloperName": devName,
		"TableEnumOrId": objectName,
		"Description":   xmlData.Description,
		"State":         state,
		"IsActive":      xmlData.Active,
		"EntryFilter":   EntryCriteriaToFilterString(xmlData.EntryCriteria),
		"Metadata":      string(xmlBytes),
		"AllowRecall":   true,
	}
	applyApprovalLockFields(rec, xmlData)
	return rec
}

// applyApprovalLockFields maps XML record-editability and lock fields onto the DB record.
func applyApprovalLockFields(rec models.SObject, xmlData *ApprovalProcessXML) {
	if xmlData.RecordEditability != "" {
		rec["LockType"] = xmlData.RecordEditability
	}
	// Default: unlock on both final approval and final rejection (false)
	rec["FinalApprovalRecordLock"] = strings.EqualFold(xmlData.FinalApprovalRecordLock, "true")
	rec["FinalRejectionRecordLock"] = strings.EqualFold(xmlData.FinalRejectionRecordLock, "true")
}

// DeployRequest handles POST /services/data/v63.0/metadata/deployRequest
// @Summary      Deploy Metadata
// @Description  Deploy metadata components in a ZIP file
// @Tags         metadata
// @Accept       multipart/form-data
// @Produce      json
// @Success      201  {object}  map[string]interface{}
// @Failure      400  {object}  map[string]interface{}
// @Router       /metadata/deployRequest [post]
// @Security     BearerAuth
func (h *MetadataHandler) DeployRequest(c *gin.Context) {
	user := MustGetUser(c)

	fileHeader, err := c.FormFile("file")
	if err != nil {
		c.JSON(400, apierr.SFErrorResponse(apierr.SFCodeInvalidRequest, "No deploy zip file provided"))
		return
	}

	file, err := fileHeader.Open()
	if err != nil {
		c.JSON(400, apierr.SFErrorResponse(apierr.SFCodeUnknownException, "Failed to open zip file"))
		return
	}
	defer file.Close()

	fileBytes, err := io.ReadAll(file)
	if err != nil {
		c.JSON(400, apierr.SFErrorResponse(apierr.SFCodeUnknownException, "Failed to read zip file"))
		return
	}

	zipReader, err := zip.NewReader(bytes.NewReader(fileBytes), int64(len(fileBytes)))
	if err != nil {
		c.JSON(400, apierr.SFErrorResponse(apierr.SFCodeUnknownException, "Invalid zip file"))
		return
	}

	deployId := strings.ReplaceAll(uuid.New().String(), "-", "")[:15]
	var componentFailures []ComponentFailure

	// Run processing asynchronously background
	go func() {
		bgCtx := context.Background()
		// Process files
		for _, f := range zipReader.File {
			if strings.HasPrefix(f.Name, "approvalProcesses/") && strings.HasSuffix(f.Name, ".approvalProcess") {
				parts := strings.Split(f.Name, "/")
				if len(parts) != 2 {
					continue
				}
				nameParts := strings.Split(strings.TrimSuffix(parts[1], ".approvalProcess"), ".")
				if len(nameParts) != 2 {
					continue
				}
				objectName := nameParts[0]
				devName := nameParts[1]

				// Validate objectName and devName to prevent filter injection.
				// These come from zip file paths (user-supplied) and are interpolated
				// into a filter string; reject anything that isn't a valid identifier.
				if !safeIdentifier.MatchString(objectName) || !safeIdentifier.MatchString(devName) {
					componentFailures = append(componentFailures, ComponentFailure{
						Problem:  "Invalid object or developer name in file path",
						FileName: f.Name,
					})
					continue
				}

				rc, err := f.Open()
				if err != nil {
					componentFailures = append(componentFailures, ComponentFailure{Problem: err.Error(), FileName: f.Name})
					continue
				}
				xmlBytes, err := io.ReadAll(rc)
				rc.Close()
				if err != nil {
					componentFailures = append(componentFailures, ComponentFailure{Problem: err.Error(), FileName: f.Name})
					continue
				}

				xmlData, err := parseApprovalProcessXML(string(xmlBytes))
				if err != nil {
					componentFailures = append(componentFailures, ComponentFailure{Problem: "Invalid XML format", FileName: f.Name})
					continue
				}

				// Upsert ProcessDefinition
				state := approvalProcessState(xmlData.Active)

				// Process the entire ApprovalProcess deploy inside a single database transaction
				err = h.svc.Persistence.RunInTransaction(bgCtx, func(txCtx context.Context) error {
					// 1. Check if a ProcessDefinition exists
					query := fmt.Sprintf("DeveloperName == '%s' && TableEnumOrId == '%s'", devName, objectName)
					res, err := h.svc.QuerySvc.QueryWithFilter(txCtx, "ProcessDefinition", query, user, "", "", 1)
					if err != nil && !strings.Contains(err.Error(), "not found") {
						return appErrors.NewInternalError("failed to query ProcessDefinition", err)
					}

					var defId string
					if err == nil && len(res.Records) > 0 {
						defId, _ = res.Records[0][constants.FieldID].(string)
						updates := buildProcessDefinitionUpdateRecord(xmlData, xmlBytes, state)
						if err := h.svc.Persistence.Update(txCtx, "ProcessDefinition", defId, updates, user); err != nil {
							return appErrors.NewInternalError("failed to update ProcessDefinition", err)
						}
					} else {
						defRecord := buildProcessDefinitionInsertRecord(xmlData, xmlBytes, devName, objectName, state)
						created, err := h.svc.Persistence.Insert(txCtx, "ProcessDefinition", defRecord, user)
						if err != nil {
							return appErrors.NewInternalError("failed to insert ProcessDefinition", err)
						}
						defId, _ = created[constants.FieldID].(string)
					}

					return syncApprovalProcessChildren(
						txCtx,
						h.svc.Persistence,
						h.svc.QuerySvc,
						defId,
						string(xmlBytes),
						f.Name,
						user,
						func(failure ComponentFailure) {
							componentFailures = append(componentFailures, failure)
						},
					)
				})

				if err != nil {
					componentFailures = append(componentFailures, ComponentFailure{Problem: err.Error(), FileName: f.Name})
					continue
				}
			}
		}

		success := len(componentFailures) == 0

		status := "Succeeded"
		if !success {
			status = "Failed"
		}

		res := &DeployPollResult{
			Done:    true,
			Status:  status,
			Success: success,
			Details: &DeployDetails{
				ComponentFailures: componentFailures,
			},
		}

		if jsonBytes, err := json.Marshal(res); err == nil {
			_ = h.svc.System.SetConfig(bgCtx, "DEPLOY_"+deployId, string(jsonBytes), false, "Metadata Deploy Result")
			// Best-effort auto-delete the deployment result after 24 hours to prevent table bloat
			time.AfterFunc(24*time.Hour, func() {
				_ = h.svc.System.DeleteConfig(context.Background(), "DEPLOY_"+deployId)
			})
		}
	}()

	// Pre-fill config with Pending status immediately
	initialRes := &DeployPollResult{
		Done:   false,
		Status: "InProgress",
	}
	if initialBytes, err := json.Marshal(initialRes); err == nil {
		_ = h.svc.System.SetConfig(c.Request.Context(), "DEPLOY_"+deployId, string(initialBytes), false, "Metadata Deploy InProgress")
	}

	c.JSON(201, gin.H{
		"id":   "0Af" + deployId,
		"done": false,
	})
}

// GetDeployStatus handles GET /services/data/v63.0/metadata/deployRequest/:id
func (h *MetadataHandler) GetDeployStatus(c *gin.Context) {
	jobId := c.Param("id")
	jobId = strings.TrimPrefix(jobId, "0Af")

	jsonStr, err := h.svc.System.GetConfig(c.Request.Context(), "DEPLOY_"+jobId)
	if err != nil {
		c.JSON(500, apierr.SFErrorResponse(apierr.SFCodeUnknownException, "Database connection error"))
		return
	}

	if jsonStr == nil {
		c.JSON(404, apierr.SFErrorResponse(apierr.SFCodeNotFound, "Deploy ID not found. It may have expired or may be invalid."))
		return
	}

	var res DeployPollResult
	if err := json.Unmarshal([]byte(*jsonStr), &res); err == nil {
		c.JSON(200, gin.H{
			"deployResult": res,
		})
		return
	}

	c.JSON(500, apierr.SFErrorResponse(apierr.SFCodeUnknownException, "Failed to parse deploy result"))
}
