package dialectsfcrm

import (
	"archive/zip"
	"bytes"
	"context"
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"mime"
	"mime/multipart"
	"net/http"
	"regexp"
	"strings"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

const (
	metadataDeployIDPrefix      = "0Af"
	maxMetadataDeploySizeBytes  = 100 * 1024 * 1024
	metadataDeployComponentType = "ApprovalProcess"
)

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

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"`
}

type deployDetails struct {
	ComponentFailures []componentFailure `json:"componentFailures,omitempty"`
}

type componentFailure struct {
	Problem       string `json:"problem,omitempty"`
	FileName      string `json:"fileName,omitempty"`
	ComponentType string `json:"componentType,omitempty"`
}

type metadataDeployManifestXML struct {
	XMLName xml.Name                        `xml:"Package"`
	Types   []metadataDeployManifestTypeXML `xml:"types"`
}

type metadataDeployManifestTypeXML struct {
	Members []string `xml:"members"`
	Name    string   `xml:"name"`
}

func MetadataDeployRequestNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	deploy := deps.Contracts.MetadataDeploy
	if deploy == nil {
		return metadataDeployInternalErrorResponse("service unavailable", nil), nil
	}
	upload, resp := readMetadataDeployMultipart(req)
	if resp != nil {
		return resp, nil
	}
	defer upload.Close()

	fileBytes, err := io.ReadAll(upload.File)
	if err != nil {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeUnknownException, "Failed to read zip file")}, nil
	}
	zipReader, err := zip.NewReader(bytes.NewReader(fileBytes), int64(len(fileBytes)))
	if err != nil {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeUnknownException, "Invalid zip file")}, nil
	}

	jobID, err := deploy.StartMetadataDeploy(ctx)
	if err != nil {
		return metadataDeployInternalErrorResponse("failed to start deploy", err), nil
	}

	go runMetadataDeploy(context.Background(), deploy, jobID, zipReader, req.User)

	return jsonResponse(http.StatusCreated, map[string]any{
		"id":   metadataDeployIDPrefix + jobID,
		"done": false,
	}), nil
}

func MetadataDeployStatusNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	deploy := deps.Contracts.MetadataDeploy
	if deploy == nil {
		return metadataDeployInternalErrorResponse("service unavailable", nil), nil
	}
	jobID := strings.TrimPrefix(strings.TrimSpace(req.Params["id"]), metadataDeployIDPrefix)
	result, found, err := deploy.GetMetadataDeployResult(ctx, jobID)
	if err != nil {
		return metadataDeployInternalErrorResponse(fmt.Sprintf("failed to load deploy status for %s", jobID), err), nil
	}
	if !found {
		return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, "Deploy ID not found. It may have expired or may be invalid.")}, nil
	}
	if result == nil {
		return metadataDeployInternalErrorResponse(fmt.Sprintf("corrupt stored deploy status for %s", jobID), nil), nil
	}
	return jsonResponse(http.StatusOK, map[string]any{"deployResult": metadataDeployWireResult(*result)}), nil
}

func metadataDeployInternalErrorResponse(operation string, err error) *dialect.Response {
	if err != nil {
		log.Printf("metadata deploy: %s: %v", operation, err)
	} else {
		log.Printf("metadata deploy: %s", operation)
	}
	return &dialect.Response{Status: http.StatusInternalServerError, Body: wire.SFGenericInternalErrorResponse()}
}

func runMetadataDeploy(ctx context.Context, deploy contract.MetadataDeploy, jobID string, zipReader *zip.Reader, user *models.UserSession) {
	failures := processMetadataDeployZip(ctx, deploy, zipReader, user)
	success := len(failures) == 0
	status := "Succeeded"
	if !success {
		status = "Failed"
	}
	_ = deploy.CompleteMetadataDeploy(ctx, jobID, contract.MetadataDeployResult{
		Done:              true,
		Status:            status,
		Success:           success,
		ComponentFailures: metadataDeployContractFailures(failures),
	})
}

func processMetadataDeployZip(ctx context.Context, deploy contract.MetadataDeploy, zipReader *zip.Reader, user *models.UserSession) []componentFailure {
	var failures []componentFailure
	if zipReader == nil {
		return failures
	}
	var destructiveChanges *zip.File
	duplicateDestructiveChanges := false
	var approvalProcessFiles []*zip.File
	for _, f := range zipReader.File {
		if f.Name == "destructiveChanges.xml" {
			if destructiveChanges != nil {
				failures = append(failures, componentFailure{
					Problem:       "Duplicate destructiveChanges.xml",
					FileName:      f.Name,
					ComponentType: metadataDeployComponentType,
				})
				duplicateDestructiveChanges = true
				continue
			}
			destructiveChanges = f
			continue
		}
		if !strings.HasPrefix(f.Name, "approvalProcesses/") || !strings.HasSuffix(f.Name, ".approvalProcess") {
			continue
		}
		approvalProcessFiles = append(approvalProcessFiles, f)
	}
	if duplicateDestructiveChanges {
		return failures
	}
	if destructiveChanges != nil {
		if len(approvalProcessFiles) != 0 {
			return append(failures, componentFailure{
				Problem:       "ApprovalProcess source and destructive members cannot be deployed together",
				FileName:      destructiveChanges.Name,
				ComponentType: metadataDeployComponentType,
			})
		}
		return append(failures, processApprovalProcessDestructiveChanges(ctx, deploy, destructiveChanges, user)...)
	}
	for _, f := range approvalProcessFiles {
		failures = append(failures, processApprovalProcessDeployFile(ctx, deploy, f, user)...)
	}
	return failures
}

func processApprovalProcessDeployFile(ctx context.Context, deploy contract.MetadataDeploy, f *zip.File, user *models.UserSession) []componentFailure {
	parts := strings.Split(f.Name, "/")
	if len(parts) != 2 {
		return []componentFailure{{Problem: "Invalid ApprovalProcess file path", FileName: f.Name}}
	}
	objectName, devName, ok := approvalProcessDeployIdentity(strings.TrimSuffix(parts[1], ".approvalProcess"))
	if !ok {
		return []componentFailure{{Problem: "Invalid object or developer name in file path", FileName: f.Name}}
	}
	rc, err := f.Open()
	if err != nil {
		return []componentFailure{{Problem: err.Error(), FileName: f.Name}}
	}
	xmlBytes, err := io.ReadAll(rc)
	_ = rc.Close()
	if err != nil {
		return []componentFailure{{Problem: err.Error(), FileName: f.Name}}
	}
	metadata, err := decodeApprovalProcessDeployMetadata(string(xmlBytes))
	if err != nil {
		return []componentFailure{{Problem: "Invalid XML format", FileName: f.Name}}
	}
	return applyMetadataDeployRecord(ctx, deploy, approvalProcessDeployRequest(metadata, xmlBytes, devName, objectName), f.Name, user)
}

func processApprovalProcessDestructiveChanges(ctx context.Context, deploy contract.MetadataDeploy, file *zip.File, user *models.UserSession) []componentFailure {
	xmlBytes, err := readMetadataDeployZipFile(file)
	if err != nil {
		return []componentFailure{{Problem: err.Error(), FileName: file.Name}}
	}
	var manifest metadataDeployManifestXML
	if err := xml.Unmarshal(xmlBytes, &manifest); err != nil {
		return []componentFailure{{Problem: "Invalid XML format", FileName: file.Name}}
	}

	if len(manifest.Types) != 1 || strings.TrimSpace(manifest.Types[0].Name) != metadataDeployComponentType {
		return []componentFailure{{
			Problem:       "destructiveChanges.xml must declare exactly one ApprovalProcess type",
			FileName:      file.Name,
			ComponentType: metadataDeployComponentType,
		}}
	}
	members := manifest.Types[0].Members
	if len(members) != 1 {
		return []componentFailure{{
			Problem:       "destructiveChanges.xml must declare exactly one ApprovalProcess member",
			FileName:      file.Name,
			ComponentType: metadataDeployComponentType,
		}}
	}
	objectName, devName, ok := approvalProcessDeployIdentity(members[0])
	if !ok {
		return []componentFailure{{
			Problem:       "Invalid ApprovalProcess member name",
			FileName:      file.Name,
			ComponentType: metadataDeployComponentType,
		}}
	}
	return applyMetadataDeployRecord(ctx, deploy, approvalProcessDeleteRequest(devName, objectName), file.Name, user)
}

func readMetadataDeployZipFile(file *zip.File) ([]byte, error) {
	rc, err := file.Open()
	if err != nil {
		return nil, err
	}
	defer rc.Close()
	return io.ReadAll(rc)
}

func approvalProcessDeployIdentity(member string) (string, string, bool) {
	parts := strings.Split(strings.TrimSpace(member), ".")
	if len(parts) != 2 || !metadataDeploySafeIdentifier.MatchString(parts[0]) || !metadataDeploySafeIdentifier.MatchString(parts[1]) {
		return "", "", false
	}
	return parts[0], parts[1], true
}

func applyMetadataDeployRecord(ctx context.Context, deploy contract.MetadataDeploy, req contract.MetadataDeployRecordRequest, fileName string, user *models.UserSession) []componentFailure {
	result, err := deploy.ApplyMetadataDeployRecord(ctx, req, user)
	var failures []componentFailure
	if result != nil {
		for _, diagnostic := range result.Diagnostics {
			failures = append(failures, componentFailure{
				Problem:       diagnostic.Problem,
				FileName:      fileName,
				ComponentType: diagnostic.ComponentType,
			})
		}
	}
	if err != nil {
		failures = append(failures, componentFailure{Problem: err.Error(), FileName: fileName})
	}
	return failures
}

func approvalProcessDeployRequest(metadata *ApprovalProcessMetadata, xmlBytes []byte, devName, objectName string) contract.MetadataDeployRecordRequest {
	state := approvalProcessState(metadata.Active)
	return contract.MetadataDeployRecordRequest{
		ObjectAPIName: constants.TableProcessDefinition,
		MatchFields: map[string]string{
			"DeveloperName": devName,
			"TableEnumOrId": objectName,
			"OwnerPack":     "",
		},
		CreateRecord:                      buildProcessDefinitionInsertRecord(metadata, xmlBytes, devName, objectName, state),
		UpdateRecord:                      buildProcessDefinitionUpdateRecord(metadata, xmlBytes, state),
		RequiredCreateLifecycleCapability: ApprovalProcessChildSyncCapability,
		RequiredUpdateLifecycleCapability: ApprovalProcessChildSyncCapability,
		DiagnosticComponentType:           metadataDeployComponentType,
	}
}

func approvalProcessDeleteRequest(devName, objectName string) contract.MetadataDeployRecordRequest {
	return contract.MetadataDeployRecordRequest{
		ObjectAPIName: constants.TableProcessDefinition,
		MatchFields: map[string]string{
			"DeveloperName": devName,
			"TableEnumOrId": objectName,
			"OwnerPack":     "",
		},
		Operation:               contract.MetadataDeployRecordOperationDelete,
		DiagnosticComponentType: metadataDeployComponentType,
	}
}

func readMetadataDeployMultipart(req *dialect.Request) (*multipartUpload, *dialect.Response) {
	if req == nil || req.BodyStream == nil {
		return nil, &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "No deploy zip file provided")}
	}
	contentType := firstFileHeader(req.Header, "Content-Type")
	mediaType, params, err := mime.ParseMediaType(contentType)
	if err != nil || !strings.HasPrefix(strings.ToLower(mediaType), "multipart/") || strings.TrimSpace(params["boundary"]) == "" {
		return nil, &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "No deploy zip file provided")}
	}
	reader := multipart.NewReader(req.BodyStream, params["boundary"])
	form, err := reader.ReadForm(maxMetadataDeploySizeBytes)
	if err != nil {
		if dialect.IsBodyStreamTooLarge(err) {
			return nil, &dialect.Response{Status: http.StatusRequestEntityTooLarge, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, fmt.Sprintf("Request body exceeds the maximum allowed size of %d MB", maxMetadataDeploySizeBytes/(1024*1024)))}
		}
		return nil, &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "No deploy zip file provided")}
	}
	fileHeaders := form.File["file"]
	if len(fileHeaders) == 0 || fileHeaders[0] == nil {
		_ = form.RemoveAll()
		return nil, &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "No deploy zip file provided")}
	}
	file, err := fileHeaders[0].Open()
	if err != nil {
		_ = form.RemoveAll()
		return nil, &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeUnknownException, "Failed to open zip file")}
	}
	return &multipartUpload{form: form, File: file, FileName: fileHeaders[0].Filename, Size: fileHeaders[0].Size}, nil
}

func metadataDeployWireResult(result contract.MetadataDeployResult) deployPollResult {
	wireResult := deployPollResult{
		Done:         result.Done,
		Status:       result.Status,
		Success:      result.Success,
		ErrorMessage: result.ErrorMessage,
	}
	if result.Done {
		wireResult.Details = &deployDetails{ComponentFailures: metadataDeployWireFailures(result.ComponentFailures)}
	}
	return wireResult
}

func metadataDeployWireFailures(failures []contract.MetadataDeployComponentFailure) []componentFailure {
	out := make([]componentFailure, 0, len(failures))
	for _, failure := range failures {
		out = append(out, componentFailure{
			Problem:       failure.Problem,
			FileName:      failure.FileName,
			ComponentType: failure.ComponentType,
		})
	}
	return out
}

func metadataDeployContractFailures(failures []componentFailure) []contract.MetadataDeployComponentFailure {
	out := make([]contract.MetadataDeployComponentFailure, 0, len(failures))
	for _, failure := range failures {
		out = append(out, contract.MetadataDeployComponentFailure{
			Problem:       failure.Problem,
			FileName:      failure.FileName,
			ComponentType: failure.ComponentType,
		})
	}
	return out
}

func entryCriteriaToFilterString(ec *ApprovalProcessCriteria) 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 ")
}

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

func decodeApprovalProcessDeployMetadata(raw string) (*ApprovalProcessMetadata, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" || trimmed[0] != '<' {
		return nil, fmt.Errorf("approval metadata deploy requires XML")
	}
	metadata, err := DecodeApprovalProcessMetadata(trimmed)
	if err != nil {
		return nil, err
	}
	if metadata == nil {
		return nil, fmt.Errorf("approval metadata deploy requires non-empty XML")
	}
	return metadata, nil
}

func buildProcessDefinitionUpdateRecord(metadata *ApprovalProcessMetadata, xmlBytes []byte, state string) contract.Record {
	rec := contract.Record{
		"Name":        metadata.Label,
		"Description": metadata.Description,
		"State":       state,
		"IsActive":    metadata.Active,
		"EntryFilter": entryCriteriaToFilterString(metadata.EntryCriteria),
		"Metadata":    string(xmlBytes),
		"AllowRecall": metadata.EffectiveAllowRecall(),
	}
	applyApprovalLockFields(rec, metadata)
	return rec
}

func buildProcessDefinitionInsertRecord(metadata *ApprovalProcessMetadata, xmlBytes []byte, devName, objectName, state string) contract.Record {
	rec := contract.Record{
		"Name":          metadata.Label,
		"DeveloperName": devName,
		"TableEnumOrId": objectName,
		"OwnerPack":     "",
		"Description":   metadata.Description,
		"State":         state,
		"IsActive":      metadata.Active,
		"EntryFilter":   entryCriteriaToFilterString(metadata.EntryCriteria),
		"Metadata":      string(xmlBytes),
		"AllowRecall":   metadata.EffectiveAllowRecall(),
	}
	applyApprovalLockFields(rec, metadata)
	return rec
}

func applyApprovalLockFields(rec contract.Record, metadata *ApprovalProcessMetadata) {
	if metadata.RecordEditability != "" {
		rec["LockType"] = metadata.RecordEditability
	}
	rec["FinalApprovalRecordLock"] = metadata.FinalApprovalRecordLock
	rec["FinalRejectionRecordLock"] = metadata.FinalRejectionRecordLock
}
