package dialectsfcrm

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	sharedErrors "github.com/nexuscrm/shared/pkg/errors"
)

const (
	duplicateFlowVersionMessage                 = "A conflicting flow version already exists."
	duplicateFlowDefinitionDeveloperNameMessage = "A flow definition with this DeveloperName already exists."
	lockContentionFlowVersionMessage            = "The flow definition is locked by a concurrent change. Retry the request."
)

type flowInterviewRecord struct {
	ID              string     `json:"Id"`
	FlowVersionID   string     `json:"FlowVersionId"`
	InterviewLabel  string     `json:"InterviewLabel,omitempty"`
	InterviewStatus string     `json:"InterviewStatus"`
	CurrentElement  string     `json:"CurrentElement,omitempty"`
	OwnerID         string     `json:"OwnerId"`
	CreatedDate     time.Time  `json:"CreatedDate"`
	PausedDate      *time.Time `json:"PausedDate,omitempty"`
	PausedUntil     *time.Time `json:"PausedUntil,omitempty"`
}

type flowDefinitionRecord struct {
	ID                   string `json:"Id"`
	DeveloperName        string `json:"DeveloperName"`
	MasterLabel          string `json:"MasterLabel"`
	Description          string `json:"Description,omitempty"`
	ProcessType          string `json:"ProcessType"`
	TriggerType          string `json:"TriggerType,omitempty"`
	TriggerObjectOrEvent string `json:"TriggerObjectOrEvent,omitempty"`
	ActiveVersionID      string `json:"ActiveVersionId,omitempty"`
	LatestVersionID      string `json:"LatestVersionId,omitempty"`
	CreatedDate          string `json:"CreatedDate,omitempty"`
	LastModifiedDate     string `json:"LastModifiedDate,omitempty"`
	TriggerOrder         *int   `json:"TriggerOrder,omitempty"`
}

type flowDefinitionWrite struct {
	ID                   string `json:"Id"`
	DeveloperName        string `json:"DeveloperName"`
	MasterLabel          string `json:"MasterLabel"`
	Description          string `json:"Description"`
	ProcessType          string `json:"ProcessType"`
	TriggerType          string `json:"TriggerType"`
	TriggerObjectOrEvent string `json:"TriggerObjectOrEvent"`
	ActiveVersionID      string `json:"ActiveVersionId"`
	LatestVersionID      string `json:"LatestVersionId"`
}

type flowVersionRecord struct {
	ID               string          `json:"Id"`
	DefinitionID     string          `json:"DefinitionId"`
	MasterLabel      string          `json:"MasterLabel,omitempty"`
	VersionNumber    int             `json:"VersionNumber"`
	Status           string          `json:"Status"`
	Description      string          `json:"Description,omitempty"`
	ProcessType      string          `json:"ProcessType"`
	Metadata         json.RawMessage `json:"Metadata"`
	CreatedDate      string          `json:"CreatedDate,omitempty"`
	LastModifiedDate string          `json:"LastModifiedDate,omitempty"`
}

type flowVersionWrite struct {
	ID           string          `json:"Id"`
	DefinitionID string          `json:"DefinitionId"`
	MasterLabel  string          `json:"MasterLabel"`
	Status       string          `json:"Status"`
	Description  string          `json:"Description"`
	ProcessType  string          `json:"ProcessType"`
	Metadata     json.RawMessage `json:"Metadata"`
}

type flowToolingLegacyAPIError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func ListFlowInterviewsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	interviews, resp := flowInterviewsContract(deps)
	if resp != nil {
		return resp, nil
	}
	records, err := interviews.ListFlowInterviews(ctx, req.User)
	if err != nil {
		return flowToolingInternalErrorResponse("list flow interviews", err), nil
	}
	out := make([]flowInterviewRecord, 0, len(records))
	for _, record := range records {
		out = append(out, flowInterviewRecordFromContract(record))
	}
	return jsonResponse(http.StatusOK, map[string]any{
		"totalSize": len(out),
		"done":      true,
		"records":   out,
	}), nil
}

func GetFlowInterviewNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	interviews, resp := flowInterviewsContract(deps)
	if resp != nil {
		return resp, nil
	}
	record, err := interviews.GetFlowInterview(ctx, strings.TrimSpace(req.Params["id"]), req.User)
	if err != nil {
		return flowToolingInternalErrorResponse("get flow interview", err), nil
	}
	if record == nil {
		return flowToolingLegacyNotFoundResponse("FlowInterview"), nil
	}
	return jsonResponse(http.StatusOK, flowInterviewRecordFromContract(*record)), nil
}

func DeleteFlowInterviewNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	interviews, resp := flowInterviewsContract(deps)
	if resp != nil {
		return resp, nil
	}
	if err := interviews.DeleteFlowInterview(ctx, strings.TrimSpace(req.Params["id"]), req.User); err != nil {
		return flowToolingInternalErrorResponse("delete flow interview", err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func ListFlowDefinitionsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	defs, err := authoring.ListFlowDefinitions(ctx)
	if err != nil {
		return flowToolingErrorResponse(req, "list flow definitions", err), nil
	}
	records := make([]flowDefinitionRecord, 0, len(defs))
	for _, def := range defs {
		records = append(records, flowDefinitionRecordFromContract(def))
	}
	return jsonResponse(http.StatusOK, map[string]any{
		"done":      true,
		"totalSize": len(records),
		"records":   records,
	}), nil
}

func GetFlowDefinitionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	def, err := authoring.GetFlowDefinition(ctx, strings.TrimSpace(req.Params["id"]))
	if err != nil {
		return flowToolingErrorResponse(req, "get flow definition", err), nil
	}
	if def == nil {
		return flowToolingLegacyNotFoundResponse("FlowDefinition"), nil
	}
	return jsonResponse(http.StatusOK, flowDefinitionRecordFromContract(*def)), nil
}

func CreateFlowDefinitionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	var body flowDefinitionWrite
	if err := decodeFlowToolingJSON(req.Body, &body); err != nil {
		return flowToolingLegacyInvalidRequestResponse(err.Error()), nil
	}
	result, err := authoring.CreateFlowDefinition(ctx, flowDefinitionWriteToContract(body))
	if err != nil {
		return flowToolingErrorResponse(req, "create flow definition", err), nil
	}
	id := ""
	if result != nil {
		id = result.ID
	}
	return jsonResponse(http.StatusCreated, map[string]any{
		"id":      id,
		"success": true,
		"errors":  []interface{}{},
	}), nil
}

func UpdateFlowDefinitionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	intent, resp, err := flowToolingDefinitionPatchIntent(req.Body)
	if resp != nil {
		return resp, nil
	}
	if err != nil {
		return flowToolingErrorResponse(req, "patch flow definition", err), nil
	}
	if err := authoring.PatchFlowDefinition(ctx, strings.TrimSpace(req.Params["id"]), intent); err != nil {
		return flowToolingErrorResponse(req, "patch flow definition", err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func DeleteFlowDefinitionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	if err := authoring.DeleteFlowDefinition(ctx, strings.TrimSpace(req.Params["id"])); err != nil {
		return flowToolingErrorResponse(req, "delete flow definition", err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func ListFlowVersionsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	versions, err := authoring.ListFlowVersions(ctx, flowToolingQuery(req, "DefinitionId"))
	if err != nil {
		return flowToolingErrorResponse(req, "list flow versions", err), nil
	}
	records := make([]flowVersionRecord, 0, len(versions))
	for _, version := range versions {
		records = append(records, flowVersionRecordFromContract(version))
	}
	return jsonResponse(http.StatusOK, map[string]any{
		"done":      true,
		"totalSize": len(records),
		"records":   records,
	}), nil
}

func GetFlowVersionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	version, err := authoring.GetFlowVersion(ctx, strings.TrimSpace(req.Params["id"]))
	if err != nil {
		return flowToolingErrorResponse(req, "get flow version", err), nil
	}
	if version == nil {
		return flowToolingLegacyNotFoundResponse("Flow"), nil
	}
	return jsonResponse(http.StatusOK, flowVersionRecordFromContract(*version)), nil
}

func CreateFlowVersionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	if resp := rejectFlowVersionNumber(req.Body); resp != nil {
		return resp, nil
	}
	var body flowVersionWrite
	if err := decodeFlowToolingJSON(req.Body, &body); err != nil {
		return flowToolingWireError(http.StatusBadRequest, wire.SFCodeInvalidField, err.Error()), nil
	}
	result, err := authoring.CreateFlowVersion(ctx, flowVersionWriteToContract(body))
	if err != nil {
		return flowToolingErrorResponse(req, "create flow version", err), nil
	}
	id := ""
	versionNumber := 0
	if result != nil {
		id = result.ID
		versionNumber = result.VersionNumber
	}
	return jsonResponse(http.StatusCreated, map[string]any{
		"id":            id,
		"success":       true,
		"errors":        []interface{}{},
		"VersionNumber": versionNumber,
	}), nil
}

func UpdateFlowVersionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	intent, resp, err := flowToolingVersionPatchIntent(req.Body)
	if resp != nil {
		return resp, nil
	}
	if err != nil {
		return flowToolingErrorResponse(req, "patch flow version", err), nil
	}
	if err := authoring.PatchFlowVersion(ctx, strings.TrimSpace(req.Params["id"]), intent); err != nil {
		return flowToolingErrorResponse(req, "patch flow version", err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func DeleteFlowVersionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	if err := authoring.DeleteFlowVersion(ctx, strings.TrimSpace(req.Params["id"])); err != nil {
		return flowToolingErrorResponse(req, "delete flow version", err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func ActivateFlowVersionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	if err := authoring.ActivateFlowVersion(ctx, strings.TrimSpace(req.Params["id"])); err != nil {
		return flowToolingErrorResponse(req, "activate flow version", err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func DeactivateFlowVersionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	authoring, resp := flowAuthoringContract(deps)
	if resp != nil {
		return resp, nil
	}
	if err := authoring.DeactivateFlowVersion(ctx, strings.TrimSpace(req.Params["id"])); err != nil {
		return flowToolingErrorResponse(req, "deactivate flow version", err), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func flowInterviewsContract(deps dialect.Deps) (contract.FlowInterviews, *dialect.Response) {
	if deps.Contracts.FlowInterviews == nil {
		return nil, flowToolingInternalErrorResponse("flow interviews contract unavailable", nil)
	}
	return deps.Contracts.FlowInterviews, nil
}

func flowAuthoringContract(deps dialect.Deps) (contract.FlowAuthoring, *dialect.Response) {
	if deps.Contracts.FlowAuthoring == nil {
		return nil, flowToolingInternalErrorResponse("flow authoring contract unavailable", nil)
	}
	return deps.Contracts.FlowAuthoring, nil
}

func rejectFlowVersionNumber(body []byte) *dialect.Response {
	var fields map[string]json.RawMessage
	if err := decodeFlowToolingJSON(body, &fields); err != nil || fields == nil {
		return flowToolingWireError(http.StatusBadRequest, wire.SFCodeJSONParserError, "Invalid JSON")
	}
	for key := range fields {
		if strings.EqualFold(key, "VersionNumber") {
			return flowToolingWireErrorWithFields(
				http.StatusBadRequest,
				wire.SFCodeInvalidFieldForInsertUpdate,
				"Unable to create/update fields: VersionNumber. Please check the security settings of this field and verify that it is read/write for your profile or permission set.",
				"VersionNumber",
			)
		}
	}
	return nil
}

func decodeFlowToolingJSON(body []byte, target any) error {
	return json.NewDecoder(bytes.NewReader(body)).Decode(target)
}

func flowToolingQuery(req *dialect.Request, key string) string {
	if req == nil || len(req.Query) == 0 {
		return ""
	}
	values := req.Query[key]
	if len(values) == 0 {
		return ""
	}
	return values[0]
}

func flowToolingErrorResponse(req *dialect.Request, operation string, err error) *dialect.Response {
	if err == nil {
		return nil
	}
	if sharedErrors.IsInternal(err) {
		return flowToolingInternalErrorResponse(operation, err)
	}
	var flowErr *contract.FlowAuthoringError
	if errors.As(err, &flowErr) {
		switch flowErr.Kind {
		case contract.FlowAuthoringErrorFieldDenied:
			return flowToolingWireErrorWithFields(http.StatusBadRequest, wire.SFCodeInvalidFieldForInsertUpdate, flowErr.Error(), flowErr.Fields...)
		case contract.FlowAuthoringErrorDuplicateVersion:
			return flowToolingWireError(http.StatusConflict, "DUPLICATE_VALUE", duplicateFlowVersionMessage)
		case contract.FlowAuthoringErrorDuplicateDefinitionDeveloper:
			return flowToolingWireError(http.StatusConflict, "DUPLICATE_VALUE", duplicateFlowDefinitionDeveloperNameMessage)
		case contract.FlowAuthoringErrorDuplicateAutoCreatedDeveloper:
			return flowToolingWireError(
				http.StatusConflict,
				"DUPLICATE_DEVELOPER_NAME",
				fmt.Sprintf("A flow with DeveloperName '%s' already exists. Please use a different name.", flowErr.DeveloperName),
			)
		case contract.FlowAuthoringErrorLockContention:
			return flowToolingWireError(http.StatusConflict, wire.SFCodeUnableToLockRow, lockContentionFlowVersionMessage)
		case contract.FlowAuthoringErrorInternal:
			return flowToolingInternalErrorResponse(operation, err)
		}
	}
	if sfErr, status, ok := wire.SFErrorFromDomainForVersion(err, apiVersionOrDefault(req.Version)); ok {
		return &dialect.Response{Status: status, Body: []wire.SFError{sfErr}}
	}
	if sharedErrors.IsNotFound(err) {
		return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, err.Error())}
	}
	return flowToolingInternalErrorResponse(operation, err)
}

func flowToolingWireError(status int, code string, message string) *dialect.Response {
	return &dialect.Response{Status: status, Body: wire.SFErrorResponse(code, message)}
}

func flowToolingWireErrorWithFields(status int, code string, message string, fields ...string) *dialect.Response {
	return &dialect.Response{Status: status, Body: wire.SFErrorWithFields(code, message, fields...)}
}

func flowToolingLegacyInvalidRequestResponse(message string) *dialect.Response {
	return &dialect.Response{
		Status: http.StatusBadRequest,
		Body: flowToolingLegacyAPIError{
			Code:    "InvalidRequest",
			Message: message,
		},
	}
}

func flowToolingLegacyNotFoundResponse(message string) *dialect.Response {
	return &dialect.Response{
		Status: http.StatusNotFound,
		Body: flowToolingLegacyAPIError{
			Code:    "ResourceNotFound",
			Message: message,
		},
	}
}

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

func flowInterviewRecordFromContract(record contract.FlowInterviewRecord) flowInterviewRecord {
	return flowInterviewRecord{
		ID:              record.ID,
		FlowVersionID:   record.FlowRevisionID,
		InterviewLabel:  record.InterviewLabel,
		InterviewStatus: record.InterviewStatus,
		CurrentElement:  record.CurrentElement,
		OwnerID:         record.OwnerID,
		CreatedDate:     record.CreatedDate,
		PausedDate:      record.PausedDate,
		PausedUntil:     record.PausedUntil,
	}
}

func flowDefinitionRecordFromContract(def contract.FlowDefinitionRecord) flowDefinitionRecord {
	return flowDefinitionRecord{
		ID:                   def.ID,
		DeveloperName:        def.DeveloperName,
		MasterLabel:          def.MasterLabel,
		Description:          def.Description,
		ProcessType:          def.ProcessType,
		TriggerType:          def.TriggerType,
		TriggerObjectOrEvent: def.TriggerObjectOrEvent,
		ActiveVersionID:      def.ActiveVersionID,
		LatestVersionID:      def.LatestVersionID,
		CreatedDate:          def.CreatedDate,
		LastModifiedDate:     def.LastModifiedDate,
		TriggerOrder:         def.TriggerOrder,
	}
}

func flowDefinitionWriteToContract(def flowDefinitionWrite) contract.FlowDefinitionWrite {
	return contract.FlowDefinitionWrite{
		ID:                   def.ID,
		DeveloperName:        def.DeveloperName,
		MasterLabel:          def.MasterLabel,
		Description:          def.Description,
		ProcessType:          def.ProcessType,
		TriggerType:          def.TriggerType,
		TriggerObjectOrEvent: def.TriggerObjectOrEvent,
		ActiveVersionID:      def.ActiveVersionID,
		LatestVersionID:      def.LatestVersionID,
	}
}

func flowVersionRecordFromContract(version contract.FlowVersionRecord) flowVersionRecord {
	metadata := json.RawMessage(version.MetadataRaw)
	if len(metadata) == 0 {
		metadata = nil
	}
	return flowVersionRecord{
		ID:               version.ID,
		DefinitionID:     version.DefinitionID,
		MasterLabel:      version.MasterLabel,
		VersionNumber:    version.VersionNumber,
		Status:           version.Status,
		Description:      version.Description,
		ProcessType:      version.ProcessType,
		Metadata:         metadata,
		CreatedDate:      version.CreatedDate,
		LastModifiedDate: version.LastModifiedDate,
	}
}

func flowVersionWriteToContract(version flowVersionWrite) contract.FlowVersionWrite {
	processType := version.ProcessType
	if strings.TrimSpace(processType) == "" {
		processType = "AutoLaunchedFlow"
	}
	return contract.FlowVersionWrite{
		ID:           version.ID,
		DefinitionID: version.DefinitionID,
		MasterLabel:  version.MasterLabel,
		Status:       version.Status,
		Description:  version.Description,
		ProcessType:  processType,
		MetadataRaw:  append([]byte(nil), version.Metadata...),
	}
}
