package dialectsfcrm

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

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

const rollupServingAttestationRequestHeader = "NexusCRM-If-Serving-Attestation"

func AdminRollupRecalculateNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	maintenance := deps.Contracts.RollupMaintenance
	if maintenance == nil {
		return jsonResponse(http.StatusServiceUnavailable, map[string]any{
			"error": "rollup service not initialized",
		}), nil
	}

	objectName := strings.TrimSpace(req.Params["object"])
	if objectName == "" {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, "object is required"),
		}, nil
	}

	expectedServingAttestation, attestationErr := rollupExpectedServingAttestation(req)
	if attestationErr != nil {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, attestationErr.Error()),
		}, nil
	}
	if expectedServingAttestation == "" {
		return rollupServingAttestationRequiredResponse(), nil
	}

	options, optionsErr := rollupRecalculateOptions(req)
	if optionsErr != nil {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, optionsErr.Error()),
		}, nil
	}

	result, err := maintenance.RecalculateRollups(ctx, contract.RollupRecalculateRequest{
		Object:                     objectName,
		ExpectedServingAttestation: expectedServingAttestation,
		AfterID:                    options.AfterID,
		MaxParents:                 options.MaxParents,
	})
	if err != nil {
		return rollupMaintenanceErrorResponse(req, err), nil
	}
	if result == nil {
		// A nil result with no error violates the contract, so there is nothing
		// truthful to say about the scan. Synthesising an empty success was
		// harmless while the response carried no completion signal; with one it
		// would have to claim either "done" — telling a driver to stop on an
		// object that may still be drifted — or "not done" with no cursor to
		// resume from, which is the ambiguous state this wire shape exists to
		// prevent. Fail closed instead, so an operator sees it.
		return &dialect.Response{
			Status: http.StatusInternalServerError,
			Body: wire.SFErrorResponse(wire.SFCodeInternalError,
				"An internal server error occurred. Contact your system administrator."),
		}, nil
	}
	// "done" is this dialect's existing word for "there is no more to fetch",
	// and a null cursor is how it says "no next page" — records.go serves the
	// pair as {"done": true, "nextRecordsUrl": nil}. A paged maintenance scan
	// says both the same way rather than inventing a second vocabulary. The
	// cursor keeps its own name because it is a record Id, not a URL.
	var nextAfterID any
	if result.NextAfterID != "" {
		nextAfterID = result.NextAfterID
	}
	return jsonResponse(http.StatusOK, map[string]any{
		"object":       result.Object,
		"recalculated": result.Recalculated,
		"nextAfterId":  nextAfterID,
		"done":         result.Complete,
		"status":       "recalculated",
	}), nil
}

// rollupRecalculateRequestBody is the optional paging envelope this route
// accepts. Both fields are optional, and so is the body itself.
type rollupRecalculateRequestBody struct {
	AfterID    string `json:"afterId"`
	MaxParents int    `json:"maxParents"`
}

// rollupRecalculateOptions reads the paging envelope. An absent or empty body
// means "no options", not a malformed request: this route accepted a bodyless
// POST before it took any input, and the downstream caller that exists today
// sends a bare {}. Only a body that is present and unparseable is an error.
func rollupRecalculateOptions(req *dialect.Request) (rollupRecalculateRequestBody, error) {
	var body rollupRecalculateRequestBody
	if req == nil || len(bytes.TrimSpace(req.Body)) == 0 {
		return body, nil
	}
	if err := json.Unmarshal(req.Body, &body); err != nil {
		return body, errors.New("invalid request body: " + err.Error())
	}
	body.AfterID = strings.TrimSpace(body.AfterID)
	return body, nil
}

// rollupExpectedServingAttestation parses this dialect's application-specific
// precondition field. The field is deliberately not If-Match: its value
// attests server process and app-pack state, not a selected representation, so
// HTTP intermediaries must not weaken or otherwise rewrite it when transforming
// a response body.
//
// Each field is an RFC 9651 Item Structured Field whose bare item is a
// non-empty String and whose parameter section is empty. Returning the decoded
// value keeps HTTP syntax at the L1 wire boundary; the neutral L0 contract
// alone owns and compares the opaque attestation value.
func rollupExpectedServingAttestation(req *dialect.Request) (string, error) {
	if req == nil {
		return "", nil
	}
	var values []string
	for name, fieldValues := range req.Header {
		if strings.EqualFold(name, rollupServingAttestationRequestHeader) {
			values = append(values, fieldValues...)
		}
	}
	if len(values) == 0 {
		return "", nil
	}
	if len(values) != 1 {
		return "", invalidRollupServingAttestationError()
	}

	// RFC 9651 permits optional leading and trailing SP around a bare Item. It
	// does not permit HTAB or other whitespace in this position.
	fieldValue := strings.Trim(values[0], " ")
	if len(fieldValue) < 2 || fieldValue[0] != '"' {
		return "", invalidRollupServingAttestationError()
	}

	var attestation strings.Builder
	for index := 1; index < len(fieldValue); index++ {
		char := fieldValue[index]
		switch {
		case char == '"':
			// Although RFC 9651 permits an empty String, this field-specific
			// contract does not: an empty attestation is malformed, not missing.
			if index != len(fieldValue)-1 || attestation.Len() == 0 {
				return "", invalidRollupServingAttestationError()
			}
			return attestation.String(), nil
		case char == '\\':
			index++
			if index >= len(fieldValue) || (fieldValue[index] != '"' && fieldValue[index] != '\\') {
				return "", invalidRollupServingAttestationError()
			}
			attestation.WriteByte(fieldValue[index])
		case char < 0x20 || char > 0x7e:
			return "", invalidRollupServingAttestationError()
		default:
			attestation.WriteByte(char)
		}
	}
	return "", invalidRollupServingAttestationError()
}

func invalidRollupServingAttestationError() error {
	return errors.New(rollupServingAttestationRequestHeader +
		" must be exactly one parameter-free RFC 9651 Item carrying a non-empty String")
}

func rollupServingAttestationRequiredResponse() *dialect.Response {
	return &dialect.Response{
		Status: http.StatusPreconditionRequired,
		Body: wire.SFErrorResponse(
			wire.SFCodeInvalidRequest,
			rollupServingAttestationRequestHeader+" is required for rollup maintenance; "+
				"GET /health through the same routing affinity and copy "+
				"NexusCRM-Serving-Attestation into this request field",
		),
	}
}

func rollupMaintenanceErrorResponse(req *dialect.Request, err error) *dialect.Response {
	var rollupErr *contract.RollupMaintenanceError
	if !errors.As(err, &rollupErr) {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, err.Error()),
		}
	}

	switch rollupErr.Kind {
	case contract.RollupMaintenanceErrorUnavailable:
		return jsonResponse(http.StatusServiceUnavailable, map[string]any{
			"error": rollupErr.Error(),
		})
	case contract.RollupMaintenanceErrorPreconditionRequired:
		return rollupServingAttestationRequiredResponse()
	case contract.RollupMaintenanceErrorPreconditionFailed:
		return &dialect.Response{
			Status: http.StatusPreconditionFailed,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, rollupErr.Error()),
		}
	case contract.RollupMaintenanceErrorNotFound:
		return &dialect.Response{
			Status: http.StatusNotFound,
			Body:   wire.SFErrorResponse(wire.SFCodeNotFound, rollupErr.Error()),
		}
	case contract.RollupMaintenanceErrorLockContention:
		return &dialect.Response{
			Status: http.StatusConflict,
			Body:   wire.SFErrorResponse(wire.SFCodeUnableToLockRow, rollupErr.Error()),
		}
	case contract.RollupMaintenanceErrorDomainWire:
		if sfErr, status, ok := wire.SFErrorFromDomainForVersion(rollupErr, apiVersionOrDefault(req.Version)); ok {
			return &dialect.Response{Status: status, Body: []wire.SFError{sfErr}}
		}
		return &dialect.Response{
			Status: http.StatusInternalServerError,
			Body:   wire.SFErrorResponse(wire.SFCodeInternalError, "An internal server error occurred. Contact your system administrator."),
		}
	case contract.RollupMaintenanceErrorPermission:
		return &dialect.Response{
			Status: http.StatusForbidden,
			Body:   wire.SFErrorResponse(wire.SFCodeForbidden, rollupErr.Error()),
		}
	case contract.RollupMaintenanceErrorFieldDenied:
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorWithFields(wire.SFCodeInvalidFieldForInsertUpdate, rollupErr.Error(), rollupErr.Fields...),
		}
	case contract.RollupMaintenanceErrorValidationRule:
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeFieldCustomValidationException, rollupErr.Error()),
		}
	case contract.RollupMaintenanceErrorValidation:
		if len(rollupErr.Fields) > 0 {
			return &dialect.Response{
				Status: http.StatusBadRequest,
				Body:   wire.SFErrorWithFields(wire.SFCodeInvalidRequest, rollupErr.Error(), rollupErr.Fields...),
			}
		}
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, rollupErr.Error()),
		}
	case contract.RollupMaintenanceErrorInternal:
		return &dialect.Response{
			Status: http.StatusInternalServerError,
			Body:   wire.SFErrorResponse(wire.SFCodeInternalError, rollupErr.Error()),
		}
	default:
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, rollupErr.Error()),
		}
	}
}
