package backendclient

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"galaxy/gateway/internal/downstream"
	usermodel "galaxy/model/user"
	"galaxy/transcoder"
)

const (
	userCommandResultCodeOK = "ok"
	defaultUserErrorCode    = "internal_error"
)

var stableUserErrorMessages = map[string]string{
	"invalid_request":   "request is invalid",
	"subject_not_found": "subject not found",
	"conflict":          "request conflicts with current state",
	defaultUserErrorCode: "internal server error",
}

// ExecuteUserCommand routes one authenticated user-surface command into
// backend's `/api/v1/user/*` endpoints. The function is registered for
// the message types listed in `galaxy/model/user`.
func (c *RESTClient) ExecuteUserCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
	if c == nil || c.httpClient == nil {
		return downstream.UnaryResult{}, errors.New("backendclient: execute user command: nil client")
	}
	if ctx == nil {
		return downstream.UnaryResult{}, errors.New("backendclient: execute user command: nil context")
	}
	if err := ctx.Err(); err != nil {
		return downstream.UnaryResult{}, err
	}
	if strings.TrimSpace(command.UserID) == "" {
		return downstream.UnaryResult{}, errors.New("backendclient: execute user command: user_id must not be empty")
	}

	switch command.MessageType {
	case usermodel.MessageTypeGetMyAccount:
		if _, err := transcoder.PayloadToGetMyAccountRequest(command.PayloadBytes); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
		}
		return c.executeUserAccountGet(ctx, command.UserID)
	case usermodel.MessageTypeUpdateMyProfile:
		req, err := transcoder.PayloadToUpdateMyProfileRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
		}
		return c.executeUserAccountUpdateProfile(ctx, command.UserID, req)
	case usermodel.MessageTypeUpdateMySettings:
		req, err := transcoder.PayloadToUpdateMySettingsRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
		}
		return c.executeUserAccountUpdateSettings(ctx, command.UserID, req)
	case usermodel.MessageTypeListMySessions:
		if _, err := transcoder.PayloadToListMySessionsRequest(command.PayloadBytes); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
		}
		return c.executeUserSessionsList(ctx, command.UserID)
	case usermodel.MessageTypeRevokeMySession:
		req, err := transcoder.PayloadToRevokeMySessionRequest(command.PayloadBytes)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
		}
		return c.executeUserSessionsRevoke(ctx, command.UserID, req)
	case usermodel.MessageTypeRevokeAllMySessions:
		if _, err := transcoder.PayloadToRevokeAllMySessionsRequest(command.PayloadBytes); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
		}
		return c.executeUserSessionsRevokeAll(ctx, command.UserID)
	default:
		return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command: unsupported message type %q", command.MessageType)
	}
}

func (c *RESTClient) executeUserAccountGet(ctx context.Context, userID string) (downstream.UnaryResult, error) {
	body, status, err := c.do(ctx, http.MethodGet, c.baseURL+"/api/v1/user/account", userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.account.get: %w", err)
	}
	return projectUserResponse(status, body)
}

func (c *RESTClient) executeUserAccountUpdateProfile(ctx context.Context, userID string, req *usermodel.UpdateMyProfileRequest) (downstream.UnaryResult, error) {
	body, status, err := c.do(ctx, http.MethodPatch, c.baseURL+"/api/v1/user/account/profile", userID, req)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.profile.update: %w", err)
	}
	return projectUserResponse(status, body)
}

func (c *RESTClient) executeUserAccountUpdateSettings(ctx context.Context, userID string, req *usermodel.UpdateMySettingsRequest) (downstream.UnaryResult, error) {
	body, status, err := c.do(ctx, http.MethodPatch, c.baseURL+"/api/v1/user/account/settings", userID, req)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.settings.update: %w", err)
	}
	return projectUserResponse(status, body)
}

func (c *RESTClient) executeUserSessionsList(ctx context.Context, userID string) (downstream.UnaryResult, error) {
	body, status, err := c.do(ctx, http.MethodGet, c.baseURL+"/api/v1/user/sessions", userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.sessions.list: %w", err)
	}
	return projectUserSessionsListResponse(status, body)
}

func (c *RESTClient) executeUserSessionsRevoke(ctx context.Context, userID string, req *usermodel.RevokeMySessionRequest) (downstream.UnaryResult, error) {
	if strings.TrimSpace(req.DeviceSessionID) == "" {
		return downstream.UnaryResult{}, errors.New("execute user.sessions.revoke: device_session_id must not be empty")
	}
	target := c.baseURL + "/api/v1/user/sessions/" + url.PathEscape(req.DeviceSessionID) + "/revoke"
	body, status, err := c.do(ctx, http.MethodPost, target, userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.sessions.revoke: %w", err)
	}
	return projectUserSessionRevokeResponse(status, body)
}

func (c *RESTClient) executeUserSessionsRevokeAll(ctx context.Context, userID string) (downstream.UnaryResult, error) {
	body, status, err := c.do(ctx, http.MethodPost, c.baseURL+"/api/v1/user/sessions/revoke-all", userID, nil)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("execute user.sessions.revoke_all: %w", err)
	}
	return projectUserSessionsRevokeAllResponse(status, body)
}

func projectUserSessionsListResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
	switch {
	case statusCode == http.StatusOK:
		var response usermodel.ListMySessionsResponse
		if err := decodeStrictJSON(payload, &response); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
		}
		payloadBytes, err := transcoder.ListMySessionsResponseToPayload(&response)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   userCommandResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	case statusCode == http.StatusServiceUnavailable:
		return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
	case statusCode >= 400 && statusCode <= 599:
		return projectUserBackendError(statusCode, payload)
	default:
		return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
	}
}

func projectUserSessionRevokeResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
	switch {
	case statusCode == http.StatusOK:
		var session usermodel.DeviceSession
		if err := decodeStrictJSON(payload, &session); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
		}
		payloadBytes, err := transcoder.RevokeMySessionResponseToPayload(&usermodel.RevokeMySessionResponse{Session: session})
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   userCommandResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	case statusCode == http.StatusServiceUnavailable:
		return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
	case statusCode >= 400 && statusCode <= 599:
		return projectUserBackendError(statusCode, payload)
	default:
		return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
	}
}

func projectUserSessionsRevokeAllResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
	switch {
	case statusCode == http.StatusOK:
		var summary usermodel.DeviceSessionRevocationSummary
		if err := decodeStrictJSON(payload, &summary); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
		}
		payloadBytes, err := transcoder.RevokeAllMySessionsResponseToPayload(&usermodel.RevokeAllMySessionsResponse{Summary: summary})
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   userCommandResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	case statusCode == http.StatusServiceUnavailable:
		return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
	case statusCode >= 400 && statusCode <= 599:
		return projectUserBackendError(statusCode, payload)
	default:
		return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
	}
}

// projectUserBackendError shares the error-projection path between every
// user-command projector. The error envelope is identical regardless of
// the success-path payload shape.
func projectUserBackendError(statusCode int, payload []byte) (downstream.UnaryResult, error) {
	errResp, err := decodeUserError(statusCode, payload)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("decode error response: %w", err)
	}
	payloadBytes, err := transcoder.ErrorResponseToPayload(errResp)
	if err != nil {
		return downstream.UnaryResult{}, fmt.Errorf("encode error response payload: %w", err)
	}
	return downstream.UnaryResult{
		ResultCode:   errResp.Error.Code,
		PayloadBytes: payloadBytes,
	}, nil
}

func projectUserResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
	switch {
	case statusCode == http.StatusOK:
		var response usermodel.AccountResponse
		if err := decodeStrictJSON(payload, &response); err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
		}
		payloadBytes, err := transcoder.AccountResponseToPayload(&response)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   userCommandResultCodeOK,
			PayloadBytes: payloadBytes,
		}, nil
	case statusCode == http.StatusServiceUnavailable:
		return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
	case statusCode >= 400 && statusCode <= 599:
		errResp, err := decodeUserError(statusCode, payload)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("decode error response: %w", err)
		}
		payloadBytes, err := transcoder.ErrorResponseToPayload(errResp)
		if err != nil {
			return downstream.UnaryResult{}, fmt.Errorf("encode error response payload: %w", err)
		}
		return downstream.UnaryResult{
			ResultCode:   errResp.Error.Code,
			PayloadBytes: payloadBytes,
		}, nil
	default:
		return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
	}
}

func decodeUserError(statusCode int, payload []byte) (*usermodel.ErrorResponse, error) {
	var response usermodel.ErrorResponse
	if err := decodeStrictJSON(payload, &response); err != nil {
		return nil, err
	}
	response.Error.Code = normalizeUserErrorCode(statusCode, response.Error.Code)
	response.Error.Message = normalizeUserErrorMessage(response.Error.Code, response.Error.Message)
	if strings.TrimSpace(response.Error.Code) == "" {
		return nil, errors.New("missing error code")
	}
	if strings.TrimSpace(response.Error.Message) == "" {
		return nil, errors.New("missing error message")
	}
	return &response, nil
}

func normalizeUserErrorCode(statusCode int, code string) string {
	if trimmed := strings.TrimSpace(code); trimmed != "" {
		return trimmed
	}
	switch statusCode {
	case http.StatusBadRequest:
		return "invalid_request"
	case http.StatusNotFound:
		return "subject_not_found"
	case http.StatusConflict:
		return "conflict"
	default:
		return defaultUserErrorCode
	}
}

func normalizeUserErrorMessage(code, message string) string {
	if trimmed := strings.TrimSpace(message); trimmed != "" {
		return trimmed
	}
	if stable, ok := stableUserErrorMessages[code]; ok {
		return stable
	}
	return stableUserErrorMessages[defaultUserErrorCode]
}
