package dialectsfcrm

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

	"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"
	"github.com/nexuscrm/shared/pkg/models"
)

const flowActionGenericInternalErrorMessage = "An internal server error occurred. Contact your system administrator."

type flowExecutionRequest struct {
	Inputs []map[string]interface{} `json:"inputs"`
}

type resumeFlowRequest struct {
	InterviewID string `json:"interviewId"`
}

// ListInvocableFlowsNeutralHandler handles GET /actions/custom/flow.
func ListInvocableFlowsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	catalog := deps.Contracts.FlowCatalog
	if catalog == nil {
		log.Printf("flow catalog unavailable: missing FlowCatalog contract")
		return flowActionInternalErrorResponse(), nil
	}
	protected, err := protectedFlowActionSet(ctx, catalog)
	if err != nil {
		log.Printf("flow action protected-catalog lookup failed: %v", err)
		return flowActionInternalErrorResponse(), nil
	}
	flows, err := catalog.ListFlows(ctx)
	if err != nil {
		log.Printf("flow definition catalog list failed: %v", err)
		return flowActionInternalErrorResponse(), nil
	}
	actions := make([]models.InvocableFlow, 0, len(flows))
	for _, flow := range flows {
		if _, hidden := protected[protectedFlowActionKey(flow.Name)]; hidden {
			continue
		}
		if strings.TrimSpace(flow.ActiveRevisionID) == "" {
			continue
		}
		actions = append(actions, invocableFlowToV63(req, flow, nil))
	}
	return jsonResponse(http.StatusOK, map[string]any{"actions": actions}), nil
}

// GetFlowSchemaNeutralHandler handles GET /actions/custom/flow/:name.
func GetFlowSchemaNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	flowName := strings.TrimSpace(req.Params["name"])
	if flowName == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "flow name is required")}, nil
	}
	catalog := deps.Contracts.FlowCatalog
	if catalog == nil {
		log.Printf("flow catalog unavailable: missing FlowCatalog contract")
		return flowActionInternalErrorResponse(), nil
	}
	protected, err := isProtectedFlowAction(ctx, catalog, flowName)
	if err != nil {
		log.Printf("flow action protected-catalog lookup failed: %v", err)
		return flowActionInternalErrorResponse(), nil
	}
	if protected {
		return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, "Flow")}, nil
	}
	flow, err := catalog.GetFlow(ctx, flowName)
	if err != nil {
		log.Printf("flow definition catalog lookup failed for %q: %v", flowName, err)
		return flowActionInternalErrorResponse(), nil
	}
	if flow == nil {
		return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, "Flow")}, nil
	}
	var version *contract.FlowRevisionDescriptor
	if strings.TrimSpace(flow.ActiveRevisionID) != "" {
		version, err = catalog.GetFlowRevision(ctx, flow.ActiveRevisionID)
		if err != nil {
			log.Printf("flow version catalog lookup failed for %q version %q: %v", flowName, flow.ActiveRevisionID, err)
			return flowActionInternalErrorResponse(), nil
		}
	}
	return jsonResponse(http.StatusOK, invocableFlowToV63(req, *flow, version)), nil
}

// ExecuteCustomFlowNeutralHandler handles POST /actions/custom/flow/:name.
func ExecuteCustomFlowNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	flowName := strings.TrimSpace(req.Params["name"])
	if flowName == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "flow name is required")}, nil
	}

	flowExecutor := deps.Contracts.FlowExecutor
	if flowExecutor == nil {
		log.Printf("flow action execution unavailable: missing FlowExecutor contract")
		return flowActionInternalErrorResponse(), nil
	}
	if resp := rejectProtectedFlowAction(ctx, req, deps.Contracts.FlowCatalog, flowName); resp != nil {
		return resp, nil
	}

	var body flowExecutionRequest
	if err := json.Unmarshal(req.Body, &body); err != nil {
		return flowActionInvalidJSONResponse(err), nil
	}

	results := make([]models.FlowExecutionResult, 0, len(body.Inputs))
	for _, inputs := range body.Inputs {
		result, err := flowExecutor.ExecuteFlow(ctx, flowName, inputs, req.User)
		if err != nil {
			message := err.Error()
			if sharedErrors.IsInternal(err) {
				log.Printf("flow action execution failed with internal error: %v", err)
				message = flowActionGenericInternalErrorMessage
			}
			results = append(results, models.FlowExecutionResult{
				ActionName: flowName,
				IsSuccess:  false,
				Errors: []models.FlowExecutionError{{
					StatusCode: "FLOW_EXECUTION_ERROR",
					Message:    message,
				}},
			})
			continue
		}
		if result == nil {
			results = append(results, models.FlowExecutionResult{
				ActionName: flowName,
				IsSuccess:  false,
				Errors: []models.FlowExecutionError{{
					StatusCode: "UNKNOWN_ERROR",
					Message:    "Flow execution returned no result",
				}},
			})
			continue
		}
		results = append(results, flowExecutionResultToV63(result))
	}

	return jsonResponse(http.StatusOK, results), nil
}

// ResumeCustomFlowNeutralHandler handles POST /actions/custom/resumeFlow.
func ResumeCustomFlowNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	flowExecutor := deps.Contracts.FlowExecutor
	if flowExecutor == nil {
		log.Printf("flow resume unavailable: missing FlowExecutor contract")
		return flowActionInternalErrorResponse(), nil
	}

	var body resumeFlowRequest
	if err := json.Unmarshal(req.Body, &body); err != nil {
		return flowActionInvalidJSONResponse(err), nil
	}
	interviewID := strings.TrimSpace(body.InterviewID)
	if interviewID == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "interviewId is required")}, nil
	}

	if err := flowExecutor.ResumeInterview(ctx, interviewID, req.User); err != nil {
		if resp := flowActionServiceErrorResponse(req, err); resp != nil {
			return resp, nil
		}
		log.Printf("flow resume failed: %v", err)
		return flowActionInternalErrorResponse(), nil
	}

	return successResponse(), nil
}

func rejectProtectedFlowAction(ctx context.Context, req *dialect.Request, catalog contract.FlowCatalog, flowName string) *dialect.Response {
	protected, err := isProtectedFlowAction(ctx, catalog, flowName)
	if err != nil {
		log.Printf("flow action protected-catalog lookup failed: %v", err)
		return flowActionInternalErrorResponse()
	}
	if !protected {
		return nil
	}
	return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, "Flow")}
}

func isProtectedFlowAction(ctx context.Context, catalog contract.FlowCatalog, flowName string) (bool, error) {
	if strings.TrimSpace(flowName) == "" {
		return false, nil
	}
	protected, err := protectedFlowActionSet(ctx, catalog)
	if err != nil {
		return false, err
	}
	_, ok := protected[protectedFlowActionKey(flowName)]
	return ok, nil
}

func protectedFlowActionSet(ctx context.Context, catalog contract.FlowCatalog) (map[string]struct{}, error) {
	if catalog == nil {
		return nil, fmt.Errorf("protected flow catalog is not configured")
	}
	flowNames, err := catalog.ProtectedFlowNames(ctx)
	if err != nil {
		return nil, err
	}
	out := make(map[string]struct{}, len(flowNames))
	for _, protectedName := range flowNames {
		out[protectedFlowActionKey(protectedName)] = struct{}{}
	}
	return out, nil
}

func protectedFlowActionKey(flowName string) string {
	return strings.ToLower(strings.TrimSpace(flowName))
}

func invocableFlowToV63(req *dialect.Request, flow contract.FlowDescriptor, version *contract.FlowRevisionDescriptor) models.InvocableFlow {
	return models.InvocableFlow{
		Label:       flow.Label,
		Name:        flow.Name,
		Type:        "FLOW",
		Url:         fmt.Sprintf("/services/data/%s/actions/custom/flow/%s", apiVersionOrDefault(req.Version), flow.Name),
		Description: flow.Description,
		Inputs:      flowVariablesToV63(version, true),
		Outputs:     flowVariablesToV63(version, false),
	}
}

func flowVariablesToV63(version *contract.FlowRevisionDescriptor, input bool) []models.FlowInputOutput {
	if version == nil {
		return nil
	}
	out := make([]models.FlowInputOutput, 0, len(version.Variables))
	for _, variable := range version.Variables {
		if input && !variable.IsInput {
			continue
		}
		if !input && !variable.IsOutput {
			continue
		}
		out = append(out, models.FlowInputOutput{
			Name:     variable.Name,
			Label:    variable.Name,
			DataType: variable.DataType,
		})
	}
	return out
}

func flowExecutionResultToV63(result *contract.FlowExecutionResult) models.FlowExecutionResult {
	if result == nil {
		return models.FlowExecutionResult{}
	}
	return models.FlowExecutionResult{
		ActionName:    result.ActionName,
		IsSuccess:     result.IsSuccess,
		Errors:        flowExecutionErrorsToV63(result.Errors),
		OutputValues:  flowOutputValuesToV63(result.OutputValues),
		IsCustomError: result.IsCustomError,
	}
}

func flowExecutionErrorsToV63(errors []contract.FlowExecutionError) []models.FlowExecutionError {
	if len(errors) == 0 {
		return nil
	}
	out := make([]models.FlowExecutionError, 0, len(errors))
	for _, err := range errors {
		out = append(out, models.FlowExecutionError{
			StatusCode: flowExecutionErrorCodeToV63(err.Code),
			Message:    err.Message,
			Fields:     append([]string(nil), err.Fields...),
		})
	}
	return out
}

// flowExecutionErrorCodeToV63 is the L1 projection from neutral Flow execution
// conditions to Salesforce v63 status-code spelling. Codes not owned by this
// dialect remain byte-identical, including user-authored Flow error codes.
func flowExecutionErrorCodeToV63(code string) string {
	switch code {
	case contract.FlowExecutionErrorCodeFieldWriteDenied:
		return wire.SFCodeInvalidFieldForInsertUpdate
	default:
		return code
	}
}

func flowOutputValuesToV63(values map[string]any) map[string]interface{} {
	if len(values) == 0 {
		return nil
	}
	out := make(map[string]interface{}, len(values))
	for key, value := range values {
		out[key] = projectRecordValueToV63(value, v63RecordProjectionFull)
	}
	return out
}

func flowActionServiceErrorResponse(req *dialect.Request, err error) *dialect.Response {
	if err == nil {
		return nil
	}
	if sharedErrors.IsInternal(err) {
		log.Printf("flow action internal error: %v", err)
		return flowActionInternalErrorResponse()
	}
	if sfErr, status, ok := wire.SFErrorFromDomainForVersion(err, apiVersionOrDefault(req.Version)); ok {
		return &dialect.Response{Status: status, Body: []wire.SFError{sfErr}}
	}
	var statusErr httpStatusError
	if errors.As(err, &statusErr) {
		switch statusErr.HTTPStatus() {
		case http.StatusForbidden:
			return &dialect.Response{Status: http.StatusForbidden, Body: wire.SFErrorResponse(wire.SFCodeForbidden, err.Error())}
		case http.StatusConflict:
			return &dialect.Response{Status: http.StatusConflict, Body: wire.SFErrorResponse(wire.SFCodeUnableToLockRow, err.Error())}
		case http.StatusNotFound:
			return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, err.Error())}
		}
	}
	if sharedErrors.IsNotFound(err) {
		return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, err.Error())}
	}
	return nil
}

func flowActionInvalidJSONResponse(err error) *dialect.Response {
	return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, err.Error())}
}

func flowActionInternalErrorResponse() *dialect.Response {
	return &dialect.Response{
		Status: http.StatusInternalServerError,
		Body:   wire.SFErrorResponse(wire.SFCodeInternalError, flowActionGenericInternalErrorMessage),
	}
}
