package response

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

	"apps/backend/internal/domain"
	appnotifDomain "apps/backend/internal/module/appnotif/domain"
	billingDomain "apps/backend/internal/module/billing"
	crmDomain "apps/backend/internal/module/crm/domain"
	growthDomain "apps/backend/internal/module/growth/domain"
	inspireDomain "apps/backend/internal/module/inspire/domain"
	jobDomain "apps/backend/internal/module/job/domain"
	memberDomain "apps/backend/internal/module/member/domain"
	radarDomain "apps/backend/internal/module/radar/domain"
	scoutDomain "apps/backend/internal/module/scout/domain"
	studioDomain "apps/backend/internal/module/studio/domain"
	threadsDomain "apps/backend/internal/module/threads/domain"
	tokenDomain "apps/backend/internal/module/token/domain"
	usageDomain "apps/backend/internal/module/usage/domain"

	"github.com/zeromicro/go-zero/rest/httpx"
)

// cleanBizMessage strips domain error prefixes so UI can show Chinese hints directly.
func cleanBizMessage(msg string) string {
	msg = strings.TrimSpace(msg)
	for _, p := range []string{
		"studio validation: ",
		"inspire validation: ",
		"scout validation: ",
		"validation: ",
	} {
		if strings.HasPrefix(msg, p) {
			return strings.TrimSpace(strings.TrimPrefix(msg, p))
		}
		// also case where errors.Is wraps as "prefix: detail"
		if i := strings.Index(msg, ": "); i > 0 {
			head := msg[:i]
			if strings.Contains(head, "validation") || strings.Contains(head, "ErrValidation") {
				return strings.TrimSpace(msg[i+2:])
			}
		}
	}
	return msg
}

// Envelope is the standard JSON body (AGENTS.md).
type Envelope struct {
	Code    int64       `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
	Error   interface{} `json:"error"`
}

// Write is used by goctl-generated handlers.
func Write(ctx context.Context, w http.ResponseWriter, data interface{}, err error) {
	if err != nil {
		status, env := mapError(err)
		httpx.WriteJsonCtx(ctx, w, status, env)
		return
	}
	httpx.WriteJsonCtx(ctx, w, http.StatusOK, Envelope{
		Code:    domain.SuccessCode,
		Message: domain.SuccessMessage,
		Data:    data,
		Error:   nil,
	})
}

func WrapRequestError(err error) error {
	if err == nil {
		return nil
	}
	return &bizError{http: http.StatusBadRequest, code: 400001, msg: err.Error()}
}

type bizError struct {
	http int
	code int64
	msg  string
}

func (e *bizError) Error() string { return e.msg }

func Biz(httpStatus int, code int64, msg string) error {
	return &bizError{http: httpStatus, code: code, msg: msg}
}

func mapError(err error) (int, Envelope) {
	var be *bizError
	if errors.As(err, &be) {
		return be.http, Envelope{Code: be.code, Message: be.msg}
	}
	switch {
	case errors.Is(err, billingDomain.ErrDisabled):
		return http.StatusServiceUnavailable, Envelope{Code: 503010, Message: err.Error()}
	case errors.Is(err, billingDomain.ErrInvalidPlan), errors.Is(err, billingDomain.ErrInvalidRequest):
		return http.StatusBadRequest, Envelope{Code: 400080, Message: err.Error()}
	case errors.Is(err, billingDomain.ErrInvalidSignature):
		return http.StatusBadRequest, Envelope{Code: 400081, Message: "invalid webhook signature"}
	case errors.Is(err, billingDomain.ErrNotFound):
		return http.StatusNotFound, Envelope{Code: 404080, Message: err.Error()}
	case errors.Is(err, billingDomain.ErrConflict):
		return http.StatusConflict, Envelope{Code: 409080, Message: err.Error()}
	case errors.Is(err, billingDomain.ErrCustomerRequired):
		return http.StatusConflict, Envelope{Code: 409081, Message: err.Error()}
	case errors.Is(err, memberDomain.ErrBadPassword):
		return http.StatusUnauthorized, Envelope{Code: 401010, Message: "invalid email or password"}
	case errors.Is(err, memberDomain.ErrSuspended):
		return http.StatusForbidden, Envelope{Code: 403001, Message: "account suspended"}
	case errors.Is(err, memberDomain.ErrEmailTaken):
		return http.StatusConflict, Envelope{Code: 409001, Message: "email already registered"}
	case errors.Is(err, memberDomain.ErrWeakPassword):
		// 與 domain.PasswordPolicyEN、FE api.err.400003 / password.policy.hint 同一句
		return http.StatusBadRequest, Envelope{Code: 400003, Message: memberDomain.PasswordPolicyEN}

	case errors.Is(err, memberDomain.ErrInvalidCode):
		return http.StatusBadRequest, Envelope{Code: 400004, Message: "invalid or expired code"}
	case errors.Is(err, memberDomain.ErrCodeRateLimited):
		return http.StatusTooManyRequests, Envelope{Code: 429001, Message: "verification code requested too frequently"}
	case errors.Is(err, memberDomain.ErrEmailNotRegistered):
		return http.StatusNotFound, Envelope{Code: 404002, Message: "email not registered"}
	case errors.Is(err, memberDomain.ErrNotFound):
		return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
	case errors.Is(err, memberDomain.ErrLastAdmin):
		return http.StatusBadRequest, Envelope{Code: 400021, Message: err.Error()}
	case errors.Is(err, memberDomain.ErrSelfSuspend):
		return http.StatusBadRequest, Envelope{Code: 400020, Message: err.Error()}
	// 邀請：message 為 FE i18n key（invite.err.*）
	case errors.Is(err, memberDomain.ErrInviteAlreadyBound),
		errors.Is(err, memberDomain.ErrInviteCodeRequired),
		errors.Is(err, memberDomain.ErrInviteCodeNotFound),
		errors.Is(err, memberDomain.ErrInviteCodeSelf),
		errors.Is(err, memberDomain.ErrInviteCycle),
		errors.Is(err, memberDomain.ErrInviteSelfParent),
		errors.Is(err, memberDomain.ErrInviteParentMiss):
		return http.StatusBadRequest, Envelope{Code: 400070, Message: err.Error()}
	case errors.Is(err, memberDomain.ErrInviteNeedAdmin):
		return http.StatusForbidden, Envelope{Code: 403002, Message: err.Error()}
	case errors.Is(err, tokenDomain.ErrInvalidToken):
		return http.StatusUnauthorized, Envelope{Code: 401002, Message: "invalid token"}
	case errors.Is(err, threadsDomain.ErrNotFound), errors.Is(err, jobDomain.ErrNotFound),
		errors.Is(err, appnotifDomain.ErrNotFound):
		return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
	case errors.Is(err, threadsDomain.ErrForbidden), errors.Is(err, jobDomain.ErrForbidden),
		errors.Is(err, appnotifDomain.ErrForbidden):
		return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
	case errors.Is(err, threadsDomain.ErrOAuthDenied):
		return http.StatusBadRequest, Envelope{Code: 400031, Message: "oauth denied"}
	case errors.Is(err, threadsDomain.ErrOAuthExchange), errors.Is(err, threadsDomain.ErrInvalidState):
		return http.StatusBadRequest, Envelope{Code: 400032, Message: err.Error()}
	case errors.Is(err, threadsDomain.ErrRefreshFailed):
		return http.StatusBadRequest, Envelope{Code: 400033, Message: "session refresh failed"}
	case errors.Is(err, jobDomain.ErrIllegalStatus):
		return http.StatusConflict, Envelope{Code: 409010, Message: "illegal job status transition"}
	case errors.Is(err, jobDomain.ErrTooManyActive):
		return http.StatusTooManyRequests, Envelope{Code: 429010, Message: "已有一個人設在爬取公開貼文，請等該任務完成後再試"}
	case errors.Is(err, usageDomain.ErrNoKey):
		return http.StatusBadRequest, Envelope{Code: 400040, Message: "no api key configured (platform or byok)"}
	case errors.Is(err, usageDomain.ErrQuotaExceeded):
		return http.StatusPaymentRequired, Envelope{Code: 402001, Message: "platform credits insufficient"}
	case errors.Is(err, usageDomain.ErrMeterCapExceeded):
		// message = FE i18n key
		return http.StatusPaymentRequired, Envelope{Code: 402002, Message: err.Error()}
	case errors.Is(err, usageDomain.ErrPlatformCapacity):
		// 平台 key 限流／無容量：建議改 BYOK；message = FE i18n key
		return http.StatusServiceUnavailable, Envelope{Code: 503002, Message: err.Error()}
	case errors.Is(err, usageDomain.ErrForbidden):
		return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
	case errors.Is(err, usageDomain.ErrInvalidPlan):
		return http.StatusBadRequest, Envelope{Code: 400041, Message: "invalid plan"}
	case errors.Is(err, usageDomain.ErrPurchaseDisabled):
		return http.StatusGone, Envelope{Code: 410003, Message: err.Error()}
	case errors.Is(err, studioDomain.ErrNotFound):
		return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
	case errors.Is(err, studioDomain.ErrForbidden):
		return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
	case errors.Is(err, studioDomain.ErrValidation), errors.Is(err, studioDomain.ErrBadURL):
		return http.StatusBadRequest, Envelope{Code: 400050, Message: cleanBizMessage(err.Error())}
	case errors.Is(err, studioDomain.ErrNoAccount):
		return http.StatusBadRequest, Envelope{Code: 400051, Message: "no usable threads account"}
	case errors.Is(err, studioDomain.ErrIllegalStatus), errors.Is(err, studioDomain.ErrRootBlocked):
		return http.StatusConflict, Envelope{Code: 409020, Message: err.Error()}
	case errors.Is(err, studioDomain.ErrFormulaRemove):
		return http.StatusGone, Envelope{Code: 410001, Message: "generateFromFormula removed"}
	case errors.Is(err, inspireDomain.ErrNotFound), errors.Is(err, scoutDomain.ErrNotFound):
		return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
	case errors.Is(err, inspireDomain.ErrForbidden), errors.Is(err, scoutDomain.ErrForbidden):
		return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
	case errors.Is(err, inspireDomain.ErrValidation), errors.Is(err, scoutDomain.ErrValidation):
		return http.StatusBadRequest, Envelope{Code: 400060, Message: cleanBizMessage(err.Error())}
	case errors.Is(err, inspireDomain.ErrRemoved), errors.Is(err, scoutDomain.ErrTopicRemoved):
		return http.StatusGone, Envelope{Code: 410002, Message: err.Error()}
	case errors.Is(err, inspireDomain.ErrNotImplemented):
		return http.StatusNotImplemented, Envelope{Code: 501000, Message: cleanBizMessage(err.Error())}
	case errors.Is(err, scoutDomain.ErrNoCrawlerSession):
		return http.StatusBadRequest, Envelope{Code: 400061, Message: "crawler session required when dev_mode enabled"}
	case errors.Is(err, scoutDomain.ErrHasProducts):
		return http.StatusConflict, Envelope{Code: 409030, Message: "brand has products; remove products first"}
	case errors.Is(err, scoutDomain.ErrIllegalRunStatus):
		return http.StatusConflict, Envelope{Code: 409031, Message: "scout run is not in a removable state"}
	case errors.Is(err, growthDomain.ErrNotFound):
		return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
	case errors.Is(err, growthDomain.ErrForbidden):
		return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
	case errors.Is(err, growthDomain.ErrValidation):
		return http.StatusBadRequest, Envelope{Code: 400090, Message: cleanBizMessage(err.Error())}
	case errors.Is(err, growthDomain.ErrThrottle):
		return http.StatusConflict, Envelope{Code: 409040, Message: err.Error()}
	case errors.Is(err, growthDomain.ErrReviewPending):
		return http.StatusConflict, Envelope{Code: 409041, Message: "draft review required before auto send"}
	case errors.Is(err, growthDomain.ErrRateLimited):
		return http.StatusTooManyRequests, Envelope{Code: 429020, Message: "checkup regenerate limit (max 2 per 7 days)"}
	case errors.Is(err, radarDomain.ErrNotReady), errors.Is(err, crmDomain.ErrNotReady):
		return http.StatusNotImplemented, Envelope{Code: 501010, Message: cleanBizMessage(err.Error())}
	case errors.Is(err, radarDomain.ErrNotFound), errors.Is(err, crmDomain.ErrNotFound):
		return http.StatusNotFound, Envelope{Code: 404001, Message: "not found"}
	case errors.Is(err, radarDomain.ErrForbidden), errors.Is(err, crmDomain.ErrForbidden):
		return http.StatusForbidden, Envelope{Code: 403003, Message: "forbidden"}
	case errors.Is(err, radarDomain.ErrConflict):
		return http.StatusConflict, Envelope{Code: 409002, Message: err.Error()}
	case errors.Is(err, radarDomain.ErrValidation), errors.Is(err, crmDomain.ErrValidation):
		return http.StatusBadRequest, Envelope{Code: 400100, Message: cleanBizMessage(err.Error())}
	default:
		return http.StatusInternalServerError, Envelope{Code: 500000, Message: "internal server error"}
	}
}

func OK(w http.ResponseWriter, data interface{}) {
	Write(context.Background(), w, data, nil)
}

func Fail(w http.ResponseWriter, httpStatus int, code int64, message string, errDetail interface{}) {
	if code == domain.SuccessCode {
		code = 400000
	}
	httpx.WriteJson(w, httpStatus, Envelope{Code: code, Message: message, Data: nil, Error: errDetail})
}

func NotImplemented(w http.ResponseWriter, feature string) {
	Fail(w, http.StatusNotImplemented, 501000, "not implemented: "+feature, map[string]string{"feature": feature})
}

func IsSuccessCode(code int64) bool { return code == domain.SuccessCode }
